(Feat-Fix): New Reporting system, more seeded data, fixed subdepartments and activity inversion, login page changes, etc etc
This commit is contained in:
@@ -61,7 +61,7 @@ npm run dev
|
|||||||
### Docker Management
|
### Docker Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop MySQL
|
# Stop MySQLclear
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
|
||||||
# Stop and remove all data
|
# Stop and remove all data
|
||||||
|
|||||||
82
activities.md
Normal file
82
activities.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Activity Departments and Units of Measurement
|
||||||
|
|
||||||
|
## GROUNDNUT Department
|
||||||
|
|
||||||
|
| # | Activity Name | Sub-Department | Unit of Measurement |
|
||||||
|
|---|---------------|----------------|---------------------|
|
||||||
|
| 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag |
|
||||||
|
| 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag |
|
||||||
|
| 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 4 | Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 5 | Guthali dhada Pala Tulai Silai Dhaang / Loading (Kernel Heap Weighing Stitching Stack/Loading) | Loading/Unloading | Per Bag |
|
||||||
|
| 6 | Mufali Patthar Bori silai Dhaang (Groundnut Stone Bag Stitching Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 7 | Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading) | Loading/Unloading | Per Bag |
|
||||||
|
| 8 | Bardana Bandal Loading (Gunny Bundle Loading) | Loading/Unloading | Per Bag |
|
||||||
|
| 9 | Bardana Gatthi Loading/Unloading (Gunny Bale Loading/Unloading) | Loading/Unloading | Per Bag |
|
||||||
|
| 10 | Black Dana Loading/Unloading | Loading/Unloading | Per Bag |
|
||||||
|
| 11 | Pre Cleaner | Pre Cleaning | Fixed Rate-Per Person |
|
||||||
|
| 12 | Destoner | Destoner | Fixed Rate-Per Person |
|
||||||
|
| 13 | Water | Water | Fixed Rate-Per Person |
|
||||||
|
| 14 | Decordicater | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
|
||||||
|
| 15 | Round Chalna (Round Sieving) | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
|
||||||
|
| 16 | Cleaning | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
|
||||||
|
| 17 | Round Chalna No.1 (Round Sieving No.1) | Round Chalna No.1 | Fixed Rate-Per Person |
|
||||||
|
| 18 | Dala - Chomu & Jaipur (Branch - Chomu & Jaipur) | Loading/Unloading | Per Bag |
|
||||||
|
|
||||||
|
## DANA Department
|
||||||
|
|
||||||
|
| # | Activity Name | Sub-Department | Unit of Measurement |
|
||||||
|
|---|---------------|----------------|---------------------|
|
||||||
|
| 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag |
|
||||||
|
| 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 4 | Tulai Silai Dhaang Ikai No. 2 Machine ke Pass (Weighing Stitching Stack Unit No. 2 Near Machine) | Loading/Unloading | Per Bag |
|
||||||
|
| 5 | Dana Unloading/Dhaang (Grain Unloading/Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 6 | Dana Aavak Keep Katai (Grain Arrival Hopper Cutting) | Loading/Unloading | Per Bag |
|
||||||
|
| 7 | Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg (Waste Heap Filling Weighing Stitching Load/Stack 70kg) | Loading/Unloading | Per Bag |
|
||||||
|
| 8 | Kachri Dhaang se loading (Waste Loading from Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 9 | Keep Katai Khulla Katta (Hopper Cutting Open Bag) | Loading/Unloading | Per Bag |
|
||||||
|
| 10 | Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched) | Loading/Unloading | Per Bag |
|
||||||
|
| 11 | Bardana Paltai (Gunny Turning) | Loading/Unloading | Per Bag |
|
||||||
|
| 12 | Ekai No. 2 me Keep Katai Khula Bag (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Open Bag with Dragging into Tank) | Loading/Unloading | Per Bag |
|
||||||
|
| 13 | Ekai No. 2 me Keep Katai Silai Kholkar (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Opening Stitched with Dragging into Tank) | Loading/Unloading | Per Bag |
|
||||||
|
| 14 | Silai Loading Company Gadi Dala Sahit (Stitching Loading Company Vehicle with Branch) | Loading/Unloading | Per Bag |
|
||||||
|
| 15 | Kachri Bharai Silai Dhaang Chatt Par (Waste Filling Stitching Stack on Roof) | Loading/Unloading | Per Bag |
|
||||||
|
| 16 | Bardana Unloading (Gunny Unloading) | Loading/Unloading | Per Bag |
|
||||||
|
| 17 | Grading | Loading/Unloading | Per Bag |
|
||||||
|
| 18 | Destoner | Destoner | Fixed Rate-Per Person |
|
||||||
|
| 19 | Gravity | Gravity | Fixed Rate-Per Person |
|
||||||
|
| 20 | Tank | Tank | Fixed Rate-Per Person |
|
||||||
|
| 21 | Sortex | Sortex | Fixed Rate-Per Person |
|
||||||
|
| 22 | X-Ray | X-Ray | Fixed Rate-Per Person |
|
||||||
|
| 23 | Kachri (Waste) | Kachri | Fixed Rate-Per Person |
|
||||||
|
| 24 | Other Works | Other Works | Fixed Rate-Per Person |
|
||||||
|
|
||||||
|
## TUKDI Department
|
||||||
|
|
||||||
|
| # | Activity Name | Sub-Department | Unit of Measurement |
|
||||||
|
|---|---------------|----------------|---------------------|
|
||||||
|
| 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag |
|
||||||
|
| 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag |
|
||||||
|
| 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 4 | Grading Chalne se Maal Bharai Tulai Silai Loading (Grading Running Material Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag |
|
||||||
|
| 5 | Chilka Bharai silai Dhaang (Husk Filling Stitching Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 6 | Keep katai Bahar Se (Hopper Cutting from Outside) | Loading/Unloading | Per Bag |
|
||||||
|
| 7 | Keep katai Andar Se (Hopper Cutting from Inside) | Loading/Unloading | Per Bag |
|
||||||
|
| 8 | Cartoon Banai Vacume Bharai Tulai Packing and Dhaang (Carton Making Vacuum Filling Weighing Packing and Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 9 | Cartoon Banai Vacume Bharai Tulai Packing and Loading (Carton Making Vacuum Filling Weighing Packing and Loading) | Loading/Unloading | Per Bag |
|
||||||
|
| 10 | Katta Paltai (Bag Turning) | Loading/Unloading | Per Bag |
|
||||||
|
| 11 | Dhada Pala Bharai Tulai Silai Dhaang (Heap Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |
|
||||||
|
| 12 | Dhada Pala Bharai Tulai Silai Loading (Heap Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag |
|
||||||
|
| 13 | Sike Maal Ki Silai Dhaang Andar (Roasted Material Stitching Stack Inside) | Loading/Unloading | Per Bag |
|
||||||
|
| 14 | Sike Maal Ki Silai Dhaang Bahar (Roasted Material Stitching Stack Outside) | Loading/Unloading | Per Bag |
|
||||||
|
| 15 | Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside) | Loading/Unloading | Per Bag |
|
||||||
|
| 16 | Tank | Tank | Fixed Rate-Per Person |
|
||||||
|
| 17 | Grader (Machine) | Grader (Machine) | Fixed Rate-Per Person |
|
||||||
|
| 18 | Sortex | Sortex | Fixed Rate-Per Person |
|
||||||
|
| 19 | X-Ray | X-Ray | Fixed Rate-Per Person |
|
||||||
|
| 20 | Rejection | Rejection | Fixed Rate-Per Person |
|
||||||
|
| 21 | Store | Store | Fixed Rate-Per Person |
|
||||||
|
| 22 | Roster | Roster | Fixed Rate-Per Person |
|
||||||
|
| 23 | Blancher | Blancher | Fixed Rate-Per Person |
|
||||||
|
| 24 | Other Works | Other Works | Fixed Rate-Per Person |
|
||||||
@@ -3,7 +3,7 @@ DB_HOST=localhost
|
|||||||
DB_USER=root
|
DB_USER=root
|
||||||
DB_PASSWORD=admin123
|
DB_PASSWORD=admin123
|
||||||
DB_NAME=work_allocation
|
DB_NAME=work_allocation
|
||||||
DB_PORT=3306
|
DB_PORT=3307
|
||||||
|
|
||||||
# JWT Configuration - CHANGE IN PRODUCTION!
|
# JWT Configuration - CHANGE IN PRODUCTION!
|
||||||
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024
|
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import workAllocationRoutes from "./routes/work-allocations.ts";
|
|||||||
import attendanceRoutes from "./routes/attendance.ts";
|
import attendanceRoutes from "./routes/attendance.ts";
|
||||||
import contractorRateRoutes from "./routes/contractor-rates.ts";
|
import contractorRateRoutes from "./routes/contractor-rates.ts";
|
||||||
import employeeSwapRoutes from "./routes/employee-swaps.ts";
|
import employeeSwapRoutes from "./routes/employee-swaps.ts";
|
||||||
|
import reportRoutes from "./routes/reports.ts";
|
||||||
|
import standardRateRoutes from "./routes/standard-rates.ts";
|
||||||
|
import activityRoutes from "./routes/activities.ts";
|
||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
await db.connect();
|
await db.connect();
|
||||||
@@ -63,6 +66,9 @@ router.use("/api/work-allocations", workAllocationRoutes.routes(), workAllocatio
|
|||||||
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods());
|
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods());
|
||||||
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods());
|
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods());
|
||||||
router.use("/api/employee-swaps", employeeSwapRoutes.routes(), employeeSwapRoutes.allowedMethods());
|
router.use("/api/employee-swaps", employeeSwapRoutes.routes(), employeeSwapRoutes.allowedMethods());
|
||||||
|
router.use("/api/reports", reportRoutes.routes(), reportRoutes.allowedMethods());
|
||||||
|
router.use("/api/standard-rates", standardRateRoutes.routes(), standardRateRoutes.allowedMethods());
|
||||||
|
router.use("/api/activities", activityRoutes.routes(), activityRoutes.allowedMethods());
|
||||||
|
|
||||||
// Apply routes
|
// Apply routes
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
|
|||||||
153
backend-deno/routes/activities.ts
Normal file
153
backend-deno/routes/activities.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { Router } from "@oak/oak";
|
||||||
|
import { db } from "../config/database.ts";
|
||||||
|
import { authenticateToken } from "../middleware/auth.ts";
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
interface Activity {
|
||||||
|
id: number;
|
||||||
|
sub_department_id: number;
|
||||||
|
name: string;
|
||||||
|
unit_of_measurement: string;
|
||||||
|
created_at: string;
|
||||||
|
sub_department_name?: string;
|
||||||
|
department_id?: number;
|
||||||
|
department_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all activities (with optional filters)
|
||||||
|
router.get("/", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const params = ctx.request.url.searchParams;
|
||||||
|
const subDepartmentId = params.get("subDepartmentId");
|
||||||
|
const departmentId = params.get("departmentId");
|
||||||
|
|
||||||
|
let query = `
|
||||||
|
SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
sd.department_id,
|
||||||
|
d.name as department_name
|
||||||
|
FROM activities a
|
||||||
|
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||||
|
JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const queryParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (subDepartmentId) {
|
||||||
|
query += " AND a.sub_department_id = ?";
|
||||||
|
queryParams.push(subDepartmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departmentId) {
|
||||||
|
query += " AND sd.department_id = ?";
|
||||||
|
queryParams.push(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " ORDER BY d.name, sd.name, a.name";
|
||||||
|
|
||||||
|
const activities = await db.query<Activity[]>(query, queryParams);
|
||||||
|
ctx.response.body = activities;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get activities error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get activity by ID
|
||||||
|
router.get("/:id", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const activityId = ctx.params.id;
|
||||||
|
|
||||||
|
const activities = await db.query<Activity[]>(
|
||||||
|
`SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
sd.department_id,
|
||||||
|
d.name as department_name
|
||||||
|
FROM activities a
|
||||||
|
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||||
|
JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE a.id = ?`,
|
||||||
|
[activityId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activities.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { error: "Activity not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.body = activities[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get activity error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create activity (SuperAdmin only)
|
||||||
|
router.post("/", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const body = await ctx.request.body.json();
|
||||||
|
const { sub_department_id, name, unit_of_measurement } = body;
|
||||||
|
|
||||||
|
if (!sub_department_id || !name) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "Sub-department ID and name are required" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.execute(
|
||||||
|
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
|
||||||
|
[sub_department_id, name, unit_of_measurement || "Per Bag"]
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = {
|
||||||
|
id: result.lastInsertId,
|
||||||
|
message: "Activity created successfully"
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Create activity error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update activity
|
||||||
|
router.put("/:id", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const activityId = ctx.params.id;
|
||||||
|
const body = await ctx.request.body.json();
|
||||||
|
const { name, unit_of_measurement } = body;
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE activities SET name = ?, unit_of_measurement = ? WHERE id = ?",
|
||||||
|
[name, unit_of_measurement, activityId]
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.response.body = { message: "Activity updated successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update activity error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete activity
|
||||||
|
router.delete("/:id", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const activityId = ctx.params.id;
|
||||||
|
|
||||||
|
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
|
||||||
|
|
||||||
|
ctx.response.body = { message: "Activity deleted successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Delete activity error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
183
backend-deno/routes/reports.ts
Normal file
183
backend-deno/routes/reports.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { Router } from "@oak/oak";
|
||||||
|
import { db } from "../config/database.ts";
|
||||||
|
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||||
|
import type { WorkAllocation } from "../types/index.ts";
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
// Get completed work allocations for reporting (with optional filters)
|
||||||
|
router.get("/completed-allocations", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const params = ctx.request.url.searchParams;
|
||||||
|
const startDate = params.get("startDate");
|
||||||
|
const endDate = params.get("endDate");
|
||||||
|
const departmentId = params.get("departmentId");
|
||||||
|
const contractorId = params.get("contractorId");
|
||||||
|
const employeeId = params.get("employeeId");
|
||||||
|
|
||||||
|
let query = `
|
||||||
|
SELECT wa.*,
|
||||||
|
e.name as employee_name, e.username as employee_username,
|
||||||
|
e.phone_number as employee_phone,
|
||||||
|
s.name as supervisor_name,
|
||||||
|
c.name as contractor_name,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
d.id as department_id
|
||||||
|
FROM work_allocations wa
|
||||||
|
JOIN users e ON wa.employee_id = e.id
|
||||||
|
JOIN users s ON wa.supervisor_id = s.id
|
||||||
|
JOIN users c ON wa.contractor_id = c.id
|
||||||
|
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON e.department_id = d.id
|
||||||
|
WHERE wa.status = 'Completed'
|
||||||
|
`;
|
||||||
|
const queryParams: unknown[] = [];
|
||||||
|
|
||||||
|
// Role-based filtering - Supervisors can only see their department
|
||||||
|
if (currentUser.role === "Supervisor") {
|
||||||
|
query += " AND e.department_id = ?";
|
||||||
|
queryParams.push(currentUser.departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range filter
|
||||||
|
if (startDate) {
|
||||||
|
query += " AND wa.completion_date >= ?";
|
||||||
|
queryParams.push(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
query += " AND wa.completion_date <= ?";
|
||||||
|
queryParams.push(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Department filter (for SuperAdmin)
|
||||||
|
if (departmentId && currentUser.role === "SuperAdmin") {
|
||||||
|
query += " AND e.department_id = ?";
|
||||||
|
queryParams.push(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contractor filter
|
||||||
|
if (contractorId) {
|
||||||
|
query += " AND wa.contractor_id = ?";
|
||||||
|
queryParams.push(contractorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employee filter
|
||||||
|
if (employeeId) {
|
||||||
|
query += " AND wa.employee_id = ?";
|
||||||
|
queryParams.push(employeeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " ORDER BY wa.completion_date DESC, wa.created_at DESC";
|
||||||
|
|
||||||
|
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
|
||||||
|
|
||||||
|
// Calculate summary stats
|
||||||
|
const totalAllocations = allocations.length;
|
||||||
|
const totalAmount = allocations.reduce((sum, a) => sum + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0), 0);
|
||||||
|
const totalUnits = allocations.reduce((sum, a) => sum + (parseFloat(String(a.units)) || 0), 0);
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
allocations,
|
||||||
|
summary: {
|
||||||
|
totalAllocations,
|
||||||
|
totalAmount: totalAmount.toFixed(2),
|
||||||
|
totalUnits: totalUnits.toFixed(2),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get completed allocations report error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get summary statistics for completed work
|
||||||
|
router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const params = ctx.request.url.searchParams;
|
||||||
|
const startDate = params.get("startDate");
|
||||||
|
const endDate = params.get("endDate");
|
||||||
|
|
||||||
|
let departmentFilter = "";
|
||||||
|
const queryParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (currentUser.role === "Supervisor") {
|
||||||
|
departmentFilter = " AND e.department_id = ?";
|
||||||
|
queryParams.push(currentUser.departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dateFilter = "";
|
||||||
|
if (startDate) {
|
||||||
|
dateFilter += " AND wa.completion_date >= ?";
|
||||||
|
queryParams.push(startDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
dateFilter += " AND wa.completion_date <= ?";
|
||||||
|
queryParams.push(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get summary by contractor
|
||||||
|
const byContractor = await db.query<any[]>(`
|
||||||
|
SELECT
|
||||||
|
c.id as contractor_id,
|
||||||
|
c.name as contractor_name,
|
||||||
|
COUNT(*) as total_allocations,
|
||||||
|
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
|
||||||
|
SUM(COALESCE(wa.units, 0)) as total_units
|
||||||
|
FROM work_allocations wa
|
||||||
|
JOIN users e ON wa.employee_id = e.id
|
||||||
|
JOIN users c ON wa.contractor_id = c.id
|
||||||
|
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||||
|
GROUP BY c.id, c.name
|
||||||
|
ORDER BY total_amount DESC
|
||||||
|
`, queryParams);
|
||||||
|
|
||||||
|
// Get summary by sub-department
|
||||||
|
const bySubDepartment = await db.query<any[]>(`
|
||||||
|
SELECT
|
||||||
|
sd.id as sub_department_id,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
COUNT(*) as total_allocations,
|
||||||
|
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
|
||||||
|
SUM(COALESCE(wa.units, 0)) as total_units
|
||||||
|
FROM work_allocations wa
|
||||||
|
JOIN users e ON wa.employee_id = e.id
|
||||||
|
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||||
|
GROUP BY sd.id, sd.name, d.name
|
||||||
|
ORDER BY total_amount DESC
|
||||||
|
`, queryParams);
|
||||||
|
|
||||||
|
// Get summary by activity type
|
||||||
|
const byActivity = await db.query<any[]>(`
|
||||||
|
SELECT
|
||||||
|
COALESCE(wa.activity, 'Standard') as activity,
|
||||||
|
COUNT(*) as total_allocations,
|
||||||
|
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
|
||||||
|
SUM(COALESCE(wa.units, 0)) as total_units
|
||||||
|
FROM work_allocations wa
|
||||||
|
JOIN users e ON wa.employee_id = e.id
|
||||||
|
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||||
|
GROUP BY wa.activity
|
||||||
|
ORDER BY total_amount DESC
|
||||||
|
`, queryParams);
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
byContractor,
|
||||||
|
bySubDepartment,
|
||||||
|
byActivity,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get report summary error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
479
backend-deno/routes/standard-rates.ts
Normal file
479
backend-deno/routes/standard-rates.ts
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
import { Router } from "@oak/oak";
|
||||||
|
import { db } from "../config/database.ts";
|
||||||
|
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||||
|
import { sanitizeInput } from "../middleware/security.ts";
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
// Standard Rate interface
|
||||||
|
interface StandardRate {
|
||||||
|
id: number;
|
||||||
|
sub_department_id: number | null;
|
||||||
|
activity: string | null;
|
||||||
|
rate: number;
|
||||||
|
effective_date: Date;
|
||||||
|
created_by: number;
|
||||||
|
created_at: Date;
|
||||||
|
sub_department_name?: string;
|
||||||
|
department_name?: string;
|
||||||
|
department_id?: number;
|
||||||
|
created_by_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all standard rates (default rates for comparison)
|
||||||
|
router.get("/", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const params = ctx.request.url.searchParams;
|
||||||
|
const departmentId = params.get("departmentId");
|
||||||
|
const subDepartmentId = params.get("subDepartmentId");
|
||||||
|
const activity = params.get("activity");
|
||||||
|
|
||||||
|
let query = `
|
||||||
|
SELECT sr.*,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
d.id as department_id,
|
||||||
|
u.name as created_by_name
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
LEFT JOIN users u ON sr.created_by = u.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const queryParams: unknown[] = [];
|
||||||
|
|
||||||
|
// Supervisors can only see rates for their department
|
||||||
|
if (currentUser.role === "Supervisor") {
|
||||||
|
query += " AND d.id = ?";
|
||||||
|
queryParams.push(currentUser.departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departmentId) {
|
||||||
|
query += " AND d.id = ?";
|
||||||
|
queryParams.push(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subDepartmentId) {
|
||||||
|
query += " AND sr.sub_department_id = ?";
|
||||||
|
queryParams.push(subDepartmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity) {
|
||||||
|
query += " AND sr.activity = ?";
|
||||||
|
queryParams.push(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " ORDER BY sr.effective_date DESC, sr.created_at DESC";
|
||||||
|
|
||||||
|
const rates = await db.query<StandardRate[]>(query, queryParams);
|
||||||
|
ctx.response.body = rates;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get standard rates error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date
|
||||||
|
router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const params = ctx.request.url.searchParams;
|
||||||
|
const departmentId = params.get("departmentId");
|
||||||
|
const startDate = params.get("startDate");
|
||||||
|
const endDate = params.get("endDate");
|
||||||
|
|
||||||
|
// Get contractor rates
|
||||||
|
let contractorQuery = `
|
||||||
|
SELECT
|
||||||
|
cr.id,
|
||||||
|
'contractor' as rate_type,
|
||||||
|
cr.contractor_id,
|
||||||
|
u.name as contractor_name,
|
||||||
|
cr.sub_department_id,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.id as department_id,
|
||||||
|
d.name as department_name,
|
||||||
|
cr.activity,
|
||||||
|
cr.rate,
|
||||||
|
cr.effective_date,
|
||||||
|
cr.created_at,
|
||||||
|
NULL as created_by,
|
||||||
|
NULL as created_by_name
|
||||||
|
FROM contractor_rates cr
|
||||||
|
JOIN users u ON cr.contractor_id = u.id
|
||||||
|
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const contractorParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (departmentId) {
|
||||||
|
contractorQuery += " AND d.id = ?";
|
||||||
|
contractorParams.push(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
contractorQuery += " AND cr.effective_date >= ?";
|
||||||
|
contractorParams.push(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
contractorQuery += " AND cr.effective_date <= ?";
|
||||||
|
contractorParams.push(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get standard rates
|
||||||
|
let standardQuery = `
|
||||||
|
SELECT
|
||||||
|
sr.id,
|
||||||
|
'standard' as rate_type,
|
||||||
|
NULL as contractor_id,
|
||||||
|
NULL as contractor_name,
|
||||||
|
sr.sub_department_id,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.id as department_id,
|
||||||
|
d.name as department_name,
|
||||||
|
sr.activity,
|
||||||
|
sr.rate,
|
||||||
|
sr.effective_date,
|
||||||
|
sr.created_at,
|
||||||
|
sr.created_by,
|
||||||
|
u.name as created_by_name
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
LEFT JOIN users u ON sr.created_by = u.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const standardParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (departmentId) {
|
||||||
|
standardQuery += " AND d.id = ?";
|
||||||
|
standardParams.push(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
standardQuery += " AND sr.effective_date >= ?";
|
||||||
|
standardParams.push(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
standardQuery += " AND sr.effective_date <= ?";
|
||||||
|
standardParams.push(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
|
||||||
|
const standardRates = await db.query<any[]>(standardQuery, standardParams);
|
||||||
|
|
||||||
|
// Combine and sort by date
|
||||||
|
const allRates = [...contractorRates, ...standardRates].sort((a, b) => {
|
||||||
|
const dateA = new Date(a.effective_date).getTime();
|
||||||
|
const dateB = new Date(b.effective_date).getTime();
|
||||||
|
return dateB - dateA; // Descending order
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
allRates,
|
||||||
|
summary: {
|
||||||
|
totalContractorRates: contractorRates.length,
|
||||||
|
totalStandardRates: standardRates.length,
|
||||||
|
totalRates: allRates.length,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get all rates error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare contractor rates with standard rates
|
||||||
|
router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const params = ctx.request.url.searchParams;
|
||||||
|
const contractorId = params.get("contractorId");
|
||||||
|
const subDepartmentId = params.get("subDepartmentId");
|
||||||
|
|
||||||
|
let departmentFilter = "";
|
||||||
|
const queryParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (currentUser.role === "Supervisor") {
|
||||||
|
departmentFilter = " AND d.id = ?";
|
||||||
|
queryParams.push(currentUser.departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get standard rates
|
||||||
|
let standardQuery = `
|
||||||
|
SELECT sr.*,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
d.id as department_id
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE 1=1 ${departmentFilter}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (subDepartmentId) {
|
||||||
|
standardQuery += " AND sr.sub_department_id = ?";
|
||||||
|
queryParams.push(subDepartmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
standardQuery += " ORDER BY sr.effective_date DESC";
|
||||||
|
|
||||||
|
const standardRates = await db.query<StandardRate[]>(standardQuery, queryParams);
|
||||||
|
|
||||||
|
// Get contractor rates for comparison
|
||||||
|
let contractorQuery = `
|
||||||
|
SELECT cr.*,
|
||||||
|
u.name as contractor_name,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
d.id as department_id
|
||||||
|
FROM contractor_rates cr
|
||||||
|
JOIN users u ON cr.contractor_id = u.id
|
||||||
|
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const contractorParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (currentUser.role === "Supervisor") {
|
||||||
|
contractorQuery += " AND d.id = ?";
|
||||||
|
contractorParams.push(currentUser.departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contractorId) {
|
||||||
|
contractorQuery += " AND cr.contractor_id = ?";
|
||||||
|
contractorParams.push(contractorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subDepartmentId) {
|
||||||
|
contractorQuery += " AND cr.sub_department_id = ?";
|
||||||
|
contractorParams.push(subDepartmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
contractorQuery += " ORDER BY cr.effective_date DESC";
|
||||||
|
|
||||||
|
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
|
||||||
|
|
||||||
|
// Build comparison data
|
||||||
|
const comparisons = contractorRates.map(cr => {
|
||||||
|
// Find matching standard rate
|
||||||
|
const matchingStandard = standardRates.find(sr =>
|
||||||
|
sr.sub_department_id === cr.sub_department_id &&
|
||||||
|
sr.activity === cr.activity
|
||||||
|
);
|
||||||
|
|
||||||
|
const standardRate = matchingStandard?.rate || 0;
|
||||||
|
const contractorRate = cr.rate || 0;
|
||||||
|
const difference = contractorRate - standardRate;
|
||||||
|
const percentageDiff = standardRate > 0 ? ((difference / standardRate) * 100).toFixed(2) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cr,
|
||||||
|
standard_rate: standardRate,
|
||||||
|
difference,
|
||||||
|
percentage_difference: percentageDiff,
|
||||||
|
is_above_standard: difference > 0,
|
||||||
|
is_below_standard: difference < 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
standardRates,
|
||||||
|
contractorRates,
|
||||||
|
comparisons,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Compare rates error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create standard rate (Supervisor or SuperAdmin)
|
||||||
|
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const body = await ctx.request.body.json() as {
|
||||||
|
subDepartmentId?: number;
|
||||||
|
activity?: string;
|
||||||
|
rate: number;
|
||||||
|
effectiveDate: string;
|
||||||
|
};
|
||||||
|
const { subDepartmentId, activity, rate, effectiveDate } = body;
|
||||||
|
|
||||||
|
if (!rate || !effectiveDate) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "Missing required fields (rate, effectiveDate)" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify sub-department belongs to supervisor's department if supervisor
|
||||||
|
if (subDepartmentId && currentUser.role === "Supervisor") {
|
||||||
|
const subDepts = await db.query<any[]>(
|
||||||
|
"SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?",
|
||||||
|
[subDepartmentId, currentUser.departmentId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subDepts.length === 0) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Sub-department not in your department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||||
|
|
||||||
|
const result = await db.execute(
|
||||||
|
"INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
[subDepartmentId || null, sanitizedActivity, rate, effectiveDate, currentUser.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newRate = await db.query<StandardRate[]>(
|
||||||
|
`SELECT sr.*,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
u.name as created_by_name
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
LEFT JOIN users u ON sr.created_by = u.id
|
||||||
|
WHERE sr.id = ?`,
|
||||||
|
[result.insertId]
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = newRate[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Create standard rate error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update standard rate
|
||||||
|
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const rateId = ctx.params.id;
|
||||||
|
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
|
||||||
|
const { rate, activity, effectiveDate } = body;
|
||||||
|
|
||||||
|
// Verify rate exists and user has access
|
||||||
|
let query = `
|
||||||
|
SELECT sr.*, d.id as department_id
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE sr.id = ?
|
||||||
|
`;
|
||||||
|
const params: unknown[] = [rateId];
|
||||||
|
|
||||||
|
const existing = await db.query<any[]>(query, params);
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { error: "Standard rate not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supervisors can only update rates in their department
|
||||||
|
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Access denied - rate not in your department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: string[] = [];
|
||||||
|
const updateParams: unknown[] = [];
|
||||||
|
|
||||||
|
if (rate !== undefined) {
|
||||||
|
updates.push("rate = ?");
|
||||||
|
updateParams.push(rate);
|
||||||
|
}
|
||||||
|
if (activity !== undefined) {
|
||||||
|
updates.push("activity = ?");
|
||||||
|
updateParams.push(sanitizeInput(activity));
|
||||||
|
}
|
||||||
|
if (effectiveDate !== undefined) {
|
||||||
|
updates.push("effective_date = ?");
|
||||||
|
updateParams.push(effectiveDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "No fields to update" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParams.push(rateId);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
`UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`,
|
||||||
|
updateParams
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedRate = await db.query<StandardRate[]>(
|
||||||
|
`SELECT sr.*,
|
||||||
|
sd.name as sub_department_name,
|
||||||
|
d.name as department_name,
|
||||||
|
u.name as created_by_name
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
LEFT JOIN users u ON sr.created_by = u.id
|
||||||
|
WHERE sr.id = ?`,
|
||||||
|
[rateId]
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.response.body = updatedRate[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update standard rate error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete standard rate
|
||||||
|
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUser(ctx);
|
||||||
|
const rateId = ctx.params.id;
|
||||||
|
|
||||||
|
// Verify rate exists and user has access
|
||||||
|
const existing = await db.query<any[]>(
|
||||||
|
`SELECT sr.*, d.id as department_id
|
||||||
|
FROM standard_rates sr
|
||||||
|
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||||
|
LEFT JOIN departments d ON sd.department_id = d.id
|
||||||
|
WHERE sr.id = ?`,
|
||||||
|
[rateId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { error: "Standard rate not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supervisors can only delete rates in their department
|
||||||
|
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Access denied - rate not in your department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]);
|
||||||
|
ctx.response.body = { message: "Standard rate deleted successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Delete standard rate error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -32,54 +32,187 @@ async function seedDatabase() {
|
|||||||
console.log(" ℹ️ Departments already exist");
|
console.log(" ℹ️ Departments already exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Seed Sub-departments for Groundnut
|
// 2. Seed Sub-departments and Activities for all departments
|
||||||
console.log("📂 Seeding sub-departments...");
|
console.log("📂 Seeding sub-departments and activities...");
|
||||||
const groundnutDept = await db.query<{ id: number }[]>(
|
|
||||||
|
// Get department IDs
|
||||||
|
const tudkiDeptResult = await db.query<{ id: number }[]>(
|
||||||
|
"SELECT id FROM departments WHERE name = ?",
|
||||||
|
["Tudki"]
|
||||||
|
);
|
||||||
|
const danaDeptResult = await db.query<{ id: number }[]>(
|
||||||
|
"SELECT id FROM departments WHERE name = ?",
|
||||||
|
["Dana"]
|
||||||
|
);
|
||||||
|
const groundnutDeptResult = await db.query<{ id: number }[]>(
|
||||||
"SELECT id FROM departments WHERE name = ?",
|
"SELECT id FROM departments WHERE name = ?",
|
||||||
["Groundnut"]
|
["Groundnut"]
|
||||||
);
|
);
|
||||||
|
|
||||||
let groundnutId: number | null = null;
|
const tudkiId = tudkiDeptResult[0]?.id;
|
||||||
|
const danaId = danaDeptResult[0]?.id;
|
||||||
|
const groundnutId = groundnutDeptResult[0]?.id;
|
||||||
|
|
||||||
if (groundnutDept.length > 0) {
|
// Define sub-departments and activities per department based on activities.md
|
||||||
groundnutId = groundnutDept[0].id;
|
const departmentData: { [key: number]: { subDept: string; activities: { name: string; unit: string }[] }[] } = {};
|
||||||
|
|
||||||
|
if (groundnutId) {
|
||||||
|
departmentData[groundnutId] = [
|
||||||
|
{
|
||||||
|
subDept: "Loading/Unloading",
|
||||||
|
activities: [
|
||||||
|
{ name: "Mufali Aavak Katai (Groundnut Arrival Cutting)", unit: "Per Bag" },
|
||||||
|
{ name: "Mufali Aavak Dhaang (Groundnut Arrival Stacking)", unit: "Per Bag" },
|
||||||
|
{ name: "Dhaang Se Katai (Cutting from Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Guthali dhada Pala Tulai Silai Dhaang / Loading", unit: "Per Bag" },
|
||||||
|
{ name: "Mufali Patthar Bori silai Dhaang", unit: "Per Bag" },
|
||||||
|
{ name: "Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading)", unit: "Per Bag" },
|
||||||
|
{ name: "Bardana Bandal Loading (Gunny Bundle Loading)", unit: "Per Bag" },
|
||||||
|
{ name: "Bardana Gatthi Loading/Unloading", unit: "Per Bag" },
|
||||||
|
{ name: "Black Dana Loading/Unloading", unit: "Per Bag" },
|
||||||
|
{ name: "Dala - Chomu & Jaipur (Branch)", unit: "Per Bag" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ subDept: "Pre Cleaning", activities: [{ name: "Pre Cleaner", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Water", activities: [{ name: "Water", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{
|
||||||
|
subDept: "Decordicater & Cleaning and Round Chalna",
|
||||||
|
activities: [
|
||||||
|
{ name: "Decordicater", unit: "Fixed Rate-Per Person" },
|
||||||
|
{ name: "Round Chalna (Round Sieving)", unit: "Fixed Rate-Per Person" },
|
||||||
|
{ name: "Cleaning", unit: "Fixed Rate-Per Person" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ subDept: "Round Chalna No.1", activities: [{ name: "Round Chalna No.1 (Round Sieving No.1)", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (danaId) {
|
||||||
|
departmentData[danaId] = [
|
||||||
|
{
|
||||||
|
subDept: "Loading/Unloading",
|
||||||
|
activities: [
|
||||||
|
{ name: "Tulai Silai Loading (Weighing Stitching Loading)", unit: "Per Bag" },
|
||||||
|
{ name: "Dhaang se Loading (Loading from Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Silai Dhaang (Stitching Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Tulai Silai Dhaang Ikai No. 2 Machine ke Pass", unit: "Per Bag" },
|
||||||
|
{ name: "Dana Unloading/Dhaang (Grain Unloading/Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Dana Aavak Keep Katai (Grain Arrival Hopper Cutting)", unit: "Per Bag" },
|
||||||
|
{ name: "Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg", unit: "Per Bag" },
|
||||||
|
{ name: "Kachri Dhaang se loading (Waste Loading from Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Keep Katai Khulla Katta (Hopper Cutting Open Bag)", unit: "Per Bag" },
|
||||||
|
{ name: "Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched)", unit: "Per Bag" },
|
||||||
|
{ name: "Bardana Paltai (Gunny Turning)", unit: "Per Bag" },
|
||||||
|
{ name: "Ekai No. 2 me Keep Katai Khula Bag", unit: "Per Bag" },
|
||||||
|
{ name: "Ekai No. 2 me Keep Katai Silai Kholkar", unit: "Per Bag" },
|
||||||
|
{ name: "Silai Loading Company Gadi Dala Sahit", unit: "Per Bag" },
|
||||||
|
{ name: "Kachri Bharai Silai Dhaang Chatt Par", unit: "Per Bag" },
|
||||||
|
{ name: "Bardana Unloading (Gunny Unloading)", unit: "Per Bag" },
|
||||||
|
{ name: "Grading", unit: "Per Bag" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Gravity", activities: [{ name: "Gravity", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Kachri", activities: [{ name: "Kachri (Waste)", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tudkiId) {
|
||||||
|
departmentData[tudkiId] = [
|
||||||
|
{
|
||||||
|
subDept: "Loading/Unloading",
|
||||||
|
activities: [
|
||||||
|
{ name: "Dana Loading/Unloading (Grain Loading/Unloading)", unit: "Per Bag" },
|
||||||
|
{ name: "Loading/Unloading 40 Kg", unit: "Per Bag" },
|
||||||
|
{ name: "Grading Chalne se Maal Bharai Tulai Silai Dhaang", unit: "Per Bag" },
|
||||||
|
{ name: "Grading Chalne se Maal Bharai Tulai Silai Loading", unit: "Per Bag" },
|
||||||
|
{ name: "Chilka Bharai silai Dhaang (Husk Filling Stitching Stack)", unit: "Per Bag" },
|
||||||
|
{ name: "Keep katai Bahar Se (Hopper Cutting from Outside)", unit: "Per Bag" },
|
||||||
|
{ name: "Keep katai Andar Se (Hopper Cutting from Inside)", unit: "Per Bag" },
|
||||||
|
{ name: "Cartoon Banai Vacume Bharai Tulai Packing and Dhaang", unit: "Per Bag" },
|
||||||
|
{ name: "Cartoon Banai Vacume Bharai Tulai Packing and Loading", unit: "Per Bag" },
|
||||||
|
{ name: "Katta Paltai (Bag Turning)", unit: "Per Bag" },
|
||||||
|
{ name: "Dhada Pala Bharai Tulai Silai Dhaang", unit: "Per Bag" },
|
||||||
|
{ name: "Dhada Pala Bharai Tulai Silai Loading", unit: "Per Bag" },
|
||||||
|
{ name: "Sike Maal Ki Silai Dhaang Andar", unit: "Per Bag" },
|
||||||
|
{ name: "Sike Maal Ki Silai Dhaang Bahar", unit: "Per Bag" },
|
||||||
|
{ name: "Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside)", unit: "Per Bag" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Grader (Machine)", activities: [{ name: "Grader (Machine)", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Rejection", activities: [{ name: "Rejection", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Store", activities: [{ name: "Store", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Roster", activities: [{ name: "Roster", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Blancher", activities: [{ name: "Blancher", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
{ subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if activities table exists, if not create it
|
||||||
|
try {
|
||||||
|
await db.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS activities (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sub_department_id INT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
unit_of_measurement ENUM('Per Bag', 'Fixed Rate-Per Person') NOT NULL DEFAULT 'Per Bag',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_activity (sub_department_id, name)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
} catch (_e) {
|
||||||
|
// Table might already exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed sub-departments and activities for each department
|
||||||
|
for (const [deptId, subDepts] of Object.entries(departmentData)) {
|
||||||
const existingSubDepts = await db.query<{ count: number }[]>(
|
const existingSubDepts = await db.query<{ count: number }[]>(
|
||||||
"SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?",
|
"SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?",
|
||||||
[groundnutId]
|
[deptId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingSubDepts[0].count === 0) {
|
if (existingSubDepts[0].count === 0) {
|
||||||
const subDepts = [
|
for (const { subDept, activities } of subDepts) {
|
||||||
"Mufali Aavak Katai",
|
// Insert sub-department
|
||||||
"Mufali Aavak Dhang",
|
|
||||||
"Dhang Se Katai",
|
|
||||||
"Guthli Bori Silai Dhang",
|
|
||||||
"Guthali dada Pala Tulai Silai Dhang",
|
|
||||||
"Mufali Patthar Bori silai dhang",
|
|
||||||
"Mufali Patthar Bori Utrai",
|
|
||||||
"Bardana Bandal Loading Unloading",
|
|
||||||
"Bardana Gatthi Loading",
|
|
||||||
"Black Dana Loading/Unloading",
|
|
||||||
"Pre Cleaning",
|
|
||||||
"Destoner",
|
|
||||||
"Water",
|
|
||||||
"Decordicater",
|
|
||||||
"Round Chalna",
|
|
||||||
"Cleaning",
|
|
||||||
"Round Chalna No.1"
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const name of subDepts) {
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)",
|
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
|
||||||
[groundnutId, name, "Loading/Unloading"]
|
[deptId, subDept]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
console.log(" ✅ Sub-departments created");
|
// Get the sub-department ID
|
||||||
} else {
|
const subDeptResult = await db.query<{ id: number }[]>(
|
||||||
console.log(" ℹ️ Sub-departments already exist");
|
"SELECT id FROM sub_departments WHERE department_id = ? AND name = ?",
|
||||||
|
[deptId, subDept]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subDeptResult.length > 0) {
|
||||||
|
const subDeptId = subDeptResult[0].id;
|
||||||
|
// Insert activities for this sub-department
|
||||||
|
for (const activity of activities) {
|
||||||
|
try {
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
|
||||||
|
[subDeptId, activity.name, activity.unit]
|
||||||
|
);
|
||||||
|
} catch (_e) {
|
||||||
|
// Activity might already exist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(" ✅ Sub-departments and activities created");
|
||||||
|
|
||||||
// 3. Seed SuperAdmin
|
// 3. Seed SuperAdmin
|
||||||
console.log("👤 Seeding SuperAdmin user...");
|
console.log("👤 Seeding SuperAdmin user...");
|
||||||
@@ -104,23 +237,32 @@ async function seedDatabase() {
|
|||||||
console.log(" ✅ SuperAdmin created");
|
console.log(" ✅ SuperAdmin created");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Seed Sample Supervisors
|
// 4. Seed Supervisors for all departments
|
||||||
console.log("👥 Seeding sample supervisors...");
|
console.log("👥 Seeding supervisors...");
|
||||||
const tudkiDept = await db.query<{ id: number }[]>(
|
|
||||||
"SELECT id FROM departments WHERE name = ?",
|
|
||||||
["Tudki"]
|
|
||||||
);
|
|
||||||
const danaDept = await db.query<{ id: number }[]>(
|
|
||||||
"SELECT id FROM departments WHERE name = ?",
|
|
||||||
["Dana"]
|
|
||||||
);
|
|
||||||
|
|
||||||
const supervisorPassword = await hashPassword("supervisor123");
|
const supervisorPassword = await hashPassword("supervisor123");
|
||||||
|
|
||||||
const supervisors = [
|
const supervisors = [
|
||||||
{ username: "supervisor_tudki", name: "Tudki Supervisor", email: "supervisor.tudki@workallocate.com", deptId: tudkiDept[0]?.id },
|
{
|
||||||
{ username: "supervisor_dana", name: "Dana Supervisor", email: "supervisor.dana@workallocate.com", deptId: danaDept[0]?.id },
|
username: "rajesh.sharma.tudki",
|
||||||
{ username: "supervisor_groundnut", name: "Groundnut Supervisor", email: "supervisor.groundnut@workallocate.com", deptId: groundnutId }
|
name: "Rajesh Sharma",
|
||||||
|
email: "rajesh.sharma@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
phone: "9414567890"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "sunil.verma.dana",
|
||||||
|
name: "Sunil Verma",
|
||||||
|
email: "sunil.verma@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
phone: "9414567891"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "mahesh.agarwal.groundnut",
|
||||||
|
name: "Mahesh Agarwal",
|
||||||
|
email: "mahesh.agarwal@workallocate.com",
|
||||||
|
deptId: groundnutId,
|
||||||
|
phone: "9414567892"
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const sup of supervisors) {
|
for (const sup of supervisors) {
|
||||||
@@ -131,8 +273,8 @@ async function seedDatabase() {
|
|||||||
);
|
);
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO users (username, name, email, password, role, department_id, is_active, phone_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
[sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true]
|
[sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true, sup.phone]
|
||||||
);
|
);
|
||||||
console.log(` ✅ ${sup.name} created`);
|
console.log(` ✅ ${sup.name} created`);
|
||||||
} else {
|
} else {
|
||||||
@@ -141,38 +283,97 @@ async function seedDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Seed Sample Contractors
|
// 5. Seed Contractors for all departments
|
||||||
console.log("🏗️ Seeding sample contractors...");
|
console.log("🏗️ Seeding contractors...");
|
||||||
const contractorPassword = await hashPassword("contractor123");
|
const contractorPassword = await hashPassword("contractor123");
|
||||||
|
|
||||||
const contractors = [
|
const contractors = [
|
||||||
|
// Groundnut Department Contractors
|
||||||
{
|
{
|
||||||
username: "contractor1",
|
username: "ramesh.patel.gn",
|
||||||
name: "Contractor One",
|
name: "Ramesh Patel",
|
||||||
email: "contractor1@workallocate.com",
|
email: "ramesh.patel@workallocate.com",
|
||||||
deptId: groundnutId,
|
deptId: groundnutId,
|
||||||
phone: "9876543210",
|
phone: "9829012345",
|
||||||
aadhar: "123456789012",
|
aadhar: "234567891234",
|
||||||
bankAccount: "1234567890123456",
|
bankAccount: "50100123456789",
|
||||||
bankName: "State Bank of India",
|
|
||||||
bankIfsc: "SBIN0001234",
|
|
||||||
agreementNo: "AGR-2024-001",
|
|
||||||
pfNo: "PF/GJ/12345/67890",
|
|
||||||
esicNo: "12-34-567890-123-0001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: "contractor2",
|
|
||||||
name: "Contractor Two",
|
|
||||||
email: "contractor2@workallocate.com",
|
|
||||||
deptId: groundnutId,
|
|
||||||
phone: "9876543211",
|
|
||||||
aadhar: "234567890123",
|
|
||||||
bankAccount: "2345678901234567",
|
|
||||||
bankName: "HDFC Bank",
|
bankName: "HDFC Bank",
|
||||||
bankIfsc: "HDFC0001234",
|
bankIfsc: "HDFC0001234",
|
||||||
agreementNo: "AGR-2024-002",
|
agreementNo: "AGR-GN-2024-001",
|
||||||
pfNo: "PF/GJ/12345/67891",
|
pfNo: "RJ/JPR/12345/001",
|
||||||
esicNo: "12-34-567890-123-0002"
|
esicNo: "12-34-567890-001-0001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "kishan.meena.gn",
|
||||||
|
name: "Kishan Meena",
|
||||||
|
email: "kishan.meena@workallocate.com",
|
||||||
|
deptId: groundnutId,
|
||||||
|
phone: "9829012346",
|
||||||
|
aadhar: "345678912345",
|
||||||
|
bankAccount: "50100123456790",
|
||||||
|
bankName: "State Bank of India",
|
||||||
|
bankIfsc: "SBIN0005678",
|
||||||
|
agreementNo: "AGR-GN-2024-002",
|
||||||
|
pfNo: "RJ/JPR/12345/002",
|
||||||
|
esicNo: "12-34-567890-001-0002"
|
||||||
|
},
|
||||||
|
// Dana Department Contractors
|
||||||
|
{
|
||||||
|
username: "gopal.sharma.dana",
|
||||||
|
name: "Gopal Sharma",
|
||||||
|
email: "gopal.sharma@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
phone: "9829012347",
|
||||||
|
aadhar: "456789123456",
|
||||||
|
bankAccount: "50100123456791",
|
||||||
|
bankName: "Punjab National Bank",
|
||||||
|
bankIfsc: "PUNB0009876",
|
||||||
|
agreementNo: "AGR-DN-2024-001",
|
||||||
|
pfNo: "RJ/JPR/12345/003",
|
||||||
|
esicNo: "12-34-567890-002-0001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "mohan.yadav.dana",
|
||||||
|
name: "Mohan Yadav",
|
||||||
|
email: "mohan.yadav@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
phone: "9829012348",
|
||||||
|
aadhar: "567891234567",
|
||||||
|
bankAccount: "50100123456792",
|
||||||
|
bankName: "Bank of Baroda",
|
||||||
|
bankIfsc: "BARB0004567",
|
||||||
|
agreementNo: "AGR-DN-2024-002",
|
||||||
|
pfNo: "RJ/JPR/12345/004",
|
||||||
|
esicNo: "12-34-567890-002-0002"
|
||||||
|
},
|
||||||
|
// Tudki Department Contractors
|
||||||
|
{
|
||||||
|
username: "suresh.kumar.tudki",
|
||||||
|
name: "Suresh Kumar",
|
||||||
|
email: "suresh.kumar@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
phone: "9829012349",
|
||||||
|
aadhar: "678912345678",
|
||||||
|
bankAccount: "50100123456793",
|
||||||
|
bankName: "ICICI Bank",
|
||||||
|
bankIfsc: "ICIC0003456",
|
||||||
|
agreementNo: "AGR-TK-2024-001",
|
||||||
|
pfNo: "RJ/JPR/12345/005",
|
||||||
|
esicNo: "12-34-567890-003-0001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "dinesh.gupta.tudki",
|
||||||
|
name: "Dinesh Gupta",
|
||||||
|
email: "dinesh.gupta@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
phone: "9829012350",
|
||||||
|
aadhar: "789123456789",
|
||||||
|
bankAccount: "50100123456794",
|
||||||
|
bankName: "Axis Bank",
|
||||||
|
bankIfsc: "UTIB0002345",
|
||||||
|
agreementNo: "AGR-TK-2024-002",
|
||||||
|
pfNo: "RJ/JPR/12345/006",
|
||||||
|
esicNo: "12-34-567890-003-0002"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -197,45 +398,220 @@ async function seedDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Seed Sample Employees
|
// 6. Seed Employees for all departments
|
||||||
console.log("👷 Seeding sample employees...");
|
console.log("👷 Seeding employees...");
|
||||||
const contractor1 = await db.query<{ id: number }[]>(
|
|
||||||
"SELECT id FROM users WHERE username = ?",
|
|
||||||
["contractor1"]
|
|
||||||
);
|
|
||||||
const employeePassword = await hashPassword("employee123");
|
const employeePassword = await hashPassword("employee123");
|
||||||
|
|
||||||
if (contractor1.length > 0) {
|
// Get contractor IDs for employee assignment
|
||||||
|
const contractorIds: { [key: string]: number } = {};
|
||||||
|
for (const con of contractors) {
|
||||||
|
const result = await db.query<{ id: number }[]>(
|
||||||
|
"SELECT id FROM users WHERE username = ?",
|
||||||
|
[con.username]
|
||||||
|
);
|
||||||
|
if (result.length > 0) {
|
||||||
|
contractorIds[con.username] = result[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const employees = [
|
const employees = [
|
||||||
|
// Groundnut Department Employees - Under Ramesh Patel
|
||||||
{
|
{
|
||||||
username: "employee1",
|
username: "ravi.singh.gn1",
|
||||||
name: "Employee One",
|
name: "Ravi Singh",
|
||||||
email: "employee1@workallocate.com",
|
email: "ravi.singh@workallocate.com",
|
||||||
phone: "9876543220",
|
deptId: groundnutId,
|
||||||
aadhar: "345678901234",
|
contractorUsername: "ramesh.patel.gn",
|
||||||
bankAccount: "3456789012345678",
|
phone: "9876501001",
|
||||||
|
aadhar: "111122223333",
|
||||||
|
bankAccount: "30100111122233",
|
||||||
|
bankName: "State Bank of India",
|
||||||
|
bankIfsc: "SBIN0001111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "amit.kumar.gn2",
|
||||||
|
name: "Amit Kumar",
|
||||||
|
email: "amit.kumar@workallocate.com",
|
||||||
|
deptId: groundnutId,
|
||||||
|
contractorUsername: "ramesh.patel.gn",
|
||||||
|
phone: "9876501002",
|
||||||
|
aadhar: "222233334444",
|
||||||
|
bankAccount: "30100222233344",
|
||||||
bankName: "Punjab National Bank",
|
bankName: "Punjab National Bank",
|
||||||
bankIfsc: "PUNB0001234"
|
bankIfsc: "PUNB0002222"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
username: "employee2",
|
username: "vijay.meena.gn3",
|
||||||
name: "Employee Two",
|
name: "Vijay Meena",
|
||||||
email: "employee2@workallocate.com",
|
email: "vijay.meena@workallocate.com",
|
||||||
phone: "9876543221",
|
deptId: groundnutId,
|
||||||
aadhar: "456789012345",
|
contractorUsername: "ramesh.patel.gn",
|
||||||
bankAccount: "4567890123456789",
|
phone: "9876501003",
|
||||||
|
aadhar: "333344445555",
|
||||||
|
bankAccount: "30100333344455",
|
||||||
|
bankName: "HDFC Bank",
|
||||||
|
bankIfsc: "HDFC0003333"
|
||||||
|
},
|
||||||
|
// Groundnut Department Employees - Under Kishan Meena
|
||||||
|
{
|
||||||
|
username: "sanjay.yadav.gn4",
|
||||||
|
name: "Sanjay Yadav",
|
||||||
|
email: "sanjay.yadav@workallocate.com",
|
||||||
|
deptId: groundnutId,
|
||||||
|
contractorUsername: "kishan.meena.gn",
|
||||||
|
phone: "9876501004",
|
||||||
|
aadhar: "444455556666",
|
||||||
|
bankAccount: "30100444455566",
|
||||||
bankName: "Bank of Baroda",
|
bankName: "Bank of Baroda",
|
||||||
bankIfsc: "BARB0001234"
|
bankIfsc: "BARB0004444"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
username: "employee3",
|
username: "prakash.sharma.gn5",
|
||||||
name: "Employee Three",
|
name: "Prakash Sharma",
|
||||||
email: "employee3@workallocate.com",
|
email: "prakash.sharma@workallocate.com",
|
||||||
phone: "9876543222",
|
deptId: groundnutId,
|
||||||
aadhar: "567890123456",
|
contractorUsername: "kishan.meena.gn",
|
||||||
bankAccount: "5678901234567890",
|
phone: "9876501005",
|
||||||
|
aadhar: "555566667777",
|
||||||
|
bankAccount: "30100555566677",
|
||||||
bankName: "ICICI Bank",
|
bankName: "ICICI Bank",
|
||||||
bankIfsc: "ICIC0001234"
|
bankIfsc: "ICIC0005555"
|
||||||
|
},
|
||||||
|
// Dana Department Employees - Under Gopal Sharma
|
||||||
|
{
|
||||||
|
username: "rampal.verma.dn1",
|
||||||
|
name: "Rampal Verma",
|
||||||
|
email: "rampal.verma@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
contractorUsername: "gopal.sharma.dana",
|
||||||
|
phone: "9876502001",
|
||||||
|
aadhar: "666677778888",
|
||||||
|
bankAccount: "30100666677788",
|
||||||
|
bankName: "State Bank of India",
|
||||||
|
bankIfsc: "SBIN0006666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "lakhan.singh.dn2",
|
||||||
|
name: "Lakhan Singh",
|
||||||
|
email: "lakhan.singh@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
contractorUsername: "gopal.sharma.dana",
|
||||||
|
phone: "9876502002",
|
||||||
|
aadhar: "777788889999",
|
||||||
|
bankAccount: "30100777788899",
|
||||||
|
bankName: "Punjab National Bank",
|
||||||
|
bankIfsc: "PUNB0007777"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "bharat.meena.dn3",
|
||||||
|
name: "Bharat Meena",
|
||||||
|
email: "bharat.meena@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
contractorUsername: "gopal.sharma.dana",
|
||||||
|
phone: "9876502003",
|
||||||
|
aadhar: "888899990000",
|
||||||
|
bankAccount: "30100888899900",
|
||||||
|
bankName: "HDFC Bank",
|
||||||
|
bankIfsc: "HDFC0008888"
|
||||||
|
},
|
||||||
|
// Dana Department Employees - Under Mohan Yadav
|
||||||
|
{
|
||||||
|
username: "kailash.patel.dn4",
|
||||||
|
name: "Kailash Patel",
|
||||||
|
email: "kailash.patel@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
contractorUsername: "mohan.yadav.dana",
|
||||||
|
phone: "9876502004",
|
||||||
|
aadhar: "999900001111",
|
||||||
|
bankAccount: "30100999900011",
|
||||||
|
bankName: "Bank of Baroda",
|
||||||
|
bankIfsc: "BARB0009999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "shyam.gupta.dn5",
|
||||||
|
name: "Shyam Gupta",
|
||||||
|
email: "shyam.gupta@workallocate.com",
|
||||||
|
deptId: danaId,
|
||||||
|
contractorUsername: "mohan.yadav.dana",
|
||||||
|
phone: "9876502005",
|
||||||
|
aadhar: "000011112222",
|
||||||
|
bankAccount: "30100000011122",
|
||||||
|
bankName: "ICICI Bank",
|
||||||
|
bankIfsc: "ICIC0000000"
|
||||||
|
},
|
||||||
|
// Tudki Department Employees - Under Suresh Kumar
|
||||||
|
{
|
||||||
|
username: "ganesh.kumar.tk1",
|
||||||
|
name: "Ganesh Kumar",
|
||||||
|
email: "ganesh.kumar@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
contractorUsername: "suresh.kumar.tudki",
|
||||||
|
phone: "9876503001",
|
||||||
|
aadhar: "112233445566",
|
||||||
|
bankAccount: "30100112233445",
|
||||||
|
bankName: "State Bank of India",
|
||||||
|
bankIfsc: "SBIN0001122"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "naresh.yadav.tk2",
|
||||||
|
name: "Naresh Yadav",
|
||||||
|
email: "naresh.yadav@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
contractorUsername: "suresh.kumar.tudki",
|
||||||
|
phone: "9876503002",
|
||||||
|
aadhar: "223344556677",
|
||||||
|
bankAccount: "30100223344556",
|
||||||
|
bankName: "Punjab National Bank",
|
||||||
|
bankIfsc: "PUNB0002233"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "mukesh.sharma.tk3",
|
||||||
|
name: "Mukesh Sharma",
|
||||||
|
email: "mukesh.sharma@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
contractorUsername: "suresh.kumar.tudki",
|
||||||
|
phone: "9876503003",
|
||||||
|
aadhar: "334455667788",
|
||||||
|
bankAccount: "30100334455667",
|
||||||
|
bankName: "HDFC Bank",
|
||||||
|
bankIfsc: "HDFC0003344"
|
||||||
|
},
|
||||||
|
// Tudki Department Employees - Under Dinesh Gupta
|
||||||
|
{
|
||||||
|
username: "pappu.singh.tk4",
|
||||||
|
name: "Pappu Singh",
|
||||||
|
email: "pappu.singh@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
contractorUsername: "dinesh.gupta.tudki",
|
||||||
|
phone: "9876503004",
|
||||||
|
aadhar: "445566778899",
|
||||||
|
bankAccount: "30100445566778",
|
||||||
|
bankName: "Bank of Baroda",
|
||||||
|
bankIfsc: "BARB0004455"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "deepak.verma.tk5",
|
||||||
|
name: "Deepak Verma",
|
||||||
|
email: "deepak.verma@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
contractorUsername: "dinesh.gupta.tudki",
|
||||||
|
phone: "9876503005",
|
||||||
|
aadhar: "556677889900",
|
||||||
|
bankAccount: "30100556677889",
|
||||||
|
bankName: "ICICI Bank",
|
||||||
|
bankIfsc: "ICIC0005566"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "rahul.meena.tk6",
|
||||||
|
name: "Rahul Meena",
|
||||||
|
email: "rahul.meena@workallocate.com",
|
||||||
|
deptId: tudkiId,
|
||||||
|
contractorUsername: "dinesh.gupta.tudki",
|
||||||
|
phone: "9876503006",
|
||||||
|
aadhar: "667788990011",
|
||||||
|
bankAccount: "30100667788990",
|
||||||
|
bankName: "Axis Bank",
|
||||||
|
bankIfsc: "UTIB0006677"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -245,39 +621,76 @@ async function seedDatabase() {
|
|||||||
[emp.username]
|
[emp.username]
|
||||||
);
|
);
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
|
const contractorId = contractorIds[emp.contractorUsername];
|
||||||
|
if (contractorId) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active,
|
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active,
|
||||||
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc)
|
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[emp.username, emp.name, emp.email, employeePassword, "Employee", groundnutId, contractor1[0].id, true,
|
[emp.username, emp.name, emp.email, employeePassword, "Employee", emp.deptId, contractorId, true,
|
||||||
emp.phone, emp.aadhar, emp.bankAccount, emp.bankName, emp.bankIfsc]
|
emp.phone, emp.aadhar, emp.bankAccount, emp.bankName, emp.bankIfsc]
|
||||||
);
|
);
|
||||||
console.log(` ✅ ${emp.name} created`);
|
console.log(` ✅ ${emp.name} created`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(` ℹ️ ${emp.name} already exists`);
|
console.log(` ℹ️ ${emp.name} already exists`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Seed Contractor Rates
|
// 7. Seed Contractor Rates for all contractors
|
||||||
console.log("💰 Seeding contractor rates...");
|
console.log("💰 Seeding contractor rates...");
|
||||||
if (contractor1.length > 0) {
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
// Get all sub-departments for rate assignment
|
||||||
|
const allSubDepts = await db.query<{ id: number; name: string; department_id: number }[]>(
|
||||||
|
"SELECT id, name, department_id FROM sub_departments"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create rates for each contractor based on their department
|
||||||
|
for (const [username, contractorId] of Object.entries(contractorIds)) {
|
||||||
const existingRate = await db.query<{ id: number }[]>(
|
const existingRate = await db.query<{ id: number }[]>(
|
||||||
"SELECT id FROM contractor_rates WHERE contractor_id = ?",
|
"SELECT id FROM contractor_rates WHERE contractor_id = ?",
|
||||||
[contractor1[0].id]
|
[contractorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingRate.length === 0) {
|
if (existingRate.length === 0) {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
// Find the contractor's department
|
||||||
await db.execute(
|
const contractor = contractors.find(c => c.username === username);
|
||||||
"INSERT INTO contractor_rates (contractor_id, rate, effective_date) VALUES (?, ?, ?)",
|
if (contractor) {
|
||||||
[contractor1[0].id, 500.00, today]
|
// Get sub-departments for this contractor's department
|
||||||
|
const deptSubDepts = allSubDepts.filter(sd => sd.department_id === contractor.deptId);
|
||||||
|
|
||||||
|
// Create rates for Loading/Unloading sub-department (Per Bag rates)
|
||||||
|
const loadingSubDept = deptSubDepts.find(sd => sd.name === "Loading/Unloading");
|
||||||
|
if (loadingSubDept) {
|
||||||
|
// Get activities for this sub-department
|
||||||
|
const activities = await db.query<{ id: number; name: string }[]>(
|
||||||
|
"SELECT id, name FROM activities WHERE sub_department_id = ? LIMIT 3",
|
||||||
|
[loadingSubDept.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const activity of activities) {
|
||||||
|
const rate = 5 + Math.random() * 3; // Random rate between 5-8 per bag
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
[contractorId, loadingSubDept.id, activity.name, rate.toFixed(2), today]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fixed rates for other sub-departments
|
||||||
|
const fixedSubDepts = deptSubDepts.filter(sd => sd.name !== "Loading/Unloading");
|
||||||
|
for (const subDept of fixedSubDepts.slice(0, 2)) { // Limit to 2 fixed rate sub-depts per contractor
|
||||||
|
const rate = 300 + Math.random() * 200; // Random rate between 300-500 per person
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO contractor_rates (contractor_id, sub_department_id, rate, effective_date) VALUES (?, ?, ?, ?)",
|
||||||
|
[contractorId, subDept.id, rate.toFixed(2), today]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log(" ✅ Contractor rates created");
|
console.log(" ✅ Contractor rates created");
|
||||||
} else {
|
|
||||||
console.log(" ℹ️ Contractor rates already exist");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@@ -289,17 +702,18 @@ async function seedDatabase() {
|
|||||||
Username: admin
|
Username: admin
|
||||||
Password: admin123
|
Password: admin123
|
||||||
|
|
||||||
Supervisor (Groundnut):
|
Supervisors (password: supervisor123):
|
||||||
Username: supervisor_groundnut
|
- Tudki: rajesh.sharma.tudki
|
||||||
Password: supervisor123
|
- Dana: sunil.verma.dana
|
||||||
|
- Groundnut: mahesh.agarwal.groundnut
|
||||||
|
|
||||||
Contractor:
|
Contractors (password: contractor123):
|
||||||
Username: contractor1
|
- Groundnut: ramesh.patel.gn, kishan.meena.gn
|
||||||
Password: contractor123
|
- Dana: gopal.sharma.dana, mohan.yadav.dana
|
||||||
|
- Tudki: suresh.kumar.tudki, dinesh.gupta.tudki
|
||||||
|
|
||||||
Employee:
|
Employees (password: employee123):
|
||||||
Username: employee1
|
- Use any employee username like ravi.singh.gn1, rampal.verma.dn1, ganesh.kumar.tk1
|
||||||
Password: employee123
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
@@ -242,3 +242,25 @@ export interface CreateContractorRateRequest {
|
|||||||
rate: number;
|
rate: number;
|
||||||
effectiveDate: string;
|
effectiveDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standard rate types
|
||||||
|
export interface StandardRate {
|
||||||
|
id: number;
|
||||||
|
sub_department_id: number | null;
|
||||||
|
activity: string | null;
|
||||||
|
rate: number;
|
||||||
|
effective_date: Date;
|
||||||
|
created_by: number;
|
||||||
|
created_at: Date;
|
||||||
|
sub_department_name?: string;
|
||||||
|
department_name?: string;
|
||||||
|
department_id?: number;
|
||||||
|
created_by_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateStandardRateRequest {
|
||||||
|
subDepartmentId?: number | null;
|
||||||
|
activity?: string | null;
|
||||||
|
rate: number;
|
||||||
|
effectiveDate: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,12 +13,22 @@ CREATE TABLE IF NOT EXISTS sub_departments (
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
department_id INT NOT NULL,
|
department_id INT NOT NULL,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
primary_activity VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE,
|
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE,
|
||||||
UNIQUE KEY unique_subdept (department_id, name)
|
UNIQUE KEY unique_subdept (department_id, name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Create activities table (activities belong to sub-departments)
|
||||||
|
CREATE TABLE IF NOT EXISTS activities (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sub_department_id INT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
unit_of_measurement ENUM('Per Bag', 'Fixed Rate-Per Person') NOT NULL DEFAULT 'Per Bag',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_activity (sub_department_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
-- Create users table
|
-- Create users table
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -120,6 +130,19 @@ CREATE TABLE IF NOT EXISTS contractor_rates (
|
|||||||
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL
|
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Create standard_rates table (default rates for comparison with contractor rates)
|
||||||
|
CREATE TABLE IF NOT EXISTS standard_rates (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sub_department_id INT,
|
||||||
|
activity VARCHAR(255),
|
||||||
|
rate DECIMAL(10, 2) NOT NULL,
|
||||||
|
effective_date DATE NOT NULL,
|
||||||
|
created_by INT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
-- Create indexes for better query performance
|
-- Create indexes for better query performance
|
||||||
CREATE INDEX idx_users_role ON users(role);
|
CREATE INDEX idx_users_role ON users(role);
|
||||||
CREATE INDEX idx_users_department ON users(department_id);
|
CREATE INDEX idx_users_department ON users(department_id);
|
||||||
@@ -136,3 +159,6 @@ CREATE INDEX idx_attendance_date ON attendance(work_date);
|
|||||||
CREATE INDEX idx_attendance_status ON attendance(status);
|
CREATE INDEX idx_attendance_status ON attendance(status);
|
||||||
CREATE INDEX idx_contractor_rates_contractor ON contractor_rates(contractor_id);
|
CREATE INDEX idx_contractor_rates_contractor ON contractor_rates(contractor_id);
|
||||||
CREATE INDEX idx_contractor_rates_date ON contractor_rates(effective_date);
|
CREATE INDEX idx_contractor_rates_date ON contractor_rates(effective_date);
|
||||||
|
CREATE INDEX idx_standard_rates_subdept ON standard_rates(sub_department_id);
|
||||||
|
CREATE INDEX idx_standard_rates_date ON standard_rates(effective_date);
|
||||||
|
CREATE INDEX idx_standard_rates_created_by ON standard_rates(created_by);
|
||||||
|
|||||||
23
backend/database/migrations/add_standard_rates.sql
Normal file
23
backend/database/migrations/add_standard_rates.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
-- Migration: Add standard_rates table
|
||||||
|
-- Run this migration to add the standard_rates table for supervisor-managed default rates
|
||||||
|
|
||||||
|
-- Create standard_rates table (default rates for comparison with contractor rates)
|
||||||
|
CREATE TABLE IF NOT EXISTS standard_rates (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sub_department_id INT,
|
||||||
|
activity VARCHAR(255),
|
||||||
|
rate DECIMAL(10, 2) NOT NULL,
|
||||||
|
effective_date DATE NOT NULL,
|
||||||
|
created_by INT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for standard_rates
|
||||||
|
CREATE INDEX idx_standard_rates_subdept ON standard_rates(sub_department_id);
|
||||||
|
CREATE INDEX idx_standard_rates_date ON standard_rates(effective_date);
|
||||||
|
CREATE INDEX idx_standard_rates_created_by ON standard_rates(created_by);
|
||||||
|
|
||||||
|
-- Verify table was created
|
||||||
|
SELECT 'standard_rates table created successfully' AS status;
|
||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
MYSQL_ROOT_PASSWORD: admin123
|
MYSQL_ROOT_PASSWORD: admin123
|
||||||
MYSQL_DATABASE: work_allocation
|
MYSQL_DATABASE: work_allocation
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3307:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
- ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro
|
- ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro
|
||||||
|
|||||||
11
src/App.tsx
11
src/App.tsx
@@ -9,8 +9,11 @@ import { AttendancePage } from './pages/AttendancePage';
|
|||||||
import { RatesPage } from './pages/RatesPage';
|
import { RatesPage } from './pages/RatesPage';
|
||||||
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
|
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
|
||||||
import { LoginPage } from './pages/LoginPage';
|
import { LoginPage } from './pages/LoginPage';
|
||||||
|
import { ReportingPage } from './pages/ReportingPage';
|
||||||
|
import { StandardRatesPage } from './pages/StandardRatesPage';
|
||||||
|
import { AllRatesPage } from './pages/AllRatesPage';
|
||||||
|
|
||||||
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps';
|
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates';
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
const [activePage, setActivePage] = useState<PageType>('dashboard');
|
const [activePage, setActivePage] = useState<PageType>('dashboard');
|
||||||
@@ -30,6 +33,12 @@ const AppContent: React.FC = () => {
|
|||||||
return <RatesPage />;
|
return <RatesPage />;
|
||||||
case 'swaps':
|
case 'swaps':
|
||||||
return <EmployeeSwapPage />;
|
return <EmployeeSwapPage />;
|
||||||
|
case 'reports':
|
||||||
|
return <ReportingPage />;
|
||||||
|
case 'standard-rates':
|
||||||
|
return <StandardRatesPage />;
|
||||||
|
case 'all-rates':
|
||||||
|
return <AllRatesPage />;
|
||||||
default:
|
default:
|
||||||
return <DashboardPage />;
|
return <DashboardPage />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft } from 'lucide-react';
|
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye } from 'lucide-react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
||||||
interface SidebarItemProps {
|
interface SidebarItemProps {
|
||||||
@@ -112,6 +112,36 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
|
|||||||
onClick={() => onNavigate('swaps')}
|
onClick={() => onNavigate('swaps')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Reports - SuperAdmin and Supervisor */}
|
||||||
|
{canManageRates && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={FileSpreadsheet}
|
||||||
|
label="Reports"
|
||||||
|
active={activePage === 'reports'}
|
||||||
|
onClick={() => onNavigate('reports')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Standard Rates - SuperAdmin and Supervisor */}
|
||||||
|
{canManageRates && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={Scale}
|
||||||
|
label="Standard Rates"
|
||||||
|
active={activePage === 'standard-rates'}
|
||||||
|
onClick={() => onNavigate('standard-rates')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* All Rates View - SuperAdmin only */}
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={Eye}
|
||||||
|
label="All Rates"
|
||||||
|
active={activePage === 'all-rates'}
|
||||||
|
onClick={() => onNavigate('all-rates')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Role indicator at bottom */}
|
{/* Role indicator at bottom */}
|
||||||
|
|||||||
302
src/pages/AllRatesPage.tsx
Normal file
302
src/pages/AllRatesPage.tsx
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { RefreshCw, Search, Filter, Eye, Calendar } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '../components/ui/Card';
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input, Select } from '../components/ui/Input';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
import { useDepartments } from '../hooks/useDepartments';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
|
export const AllRatesPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { departments } = useDepartments();
|
||||||
|
const [allRates, setAllRates] = useState<any[]>([]);
|
||||||
|
const [summary, setSummary] = useState<{ totalContractorRates: number; totalStandardRates: number; totalRates: number } | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
departmentId: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
rateType: '', // 'contractor' | 'standard' | ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||||
|
|
||||||
|
// Fetch all rates
|
||||||
|
const fetchAllRates = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const params: any = {};
|
||||||
|
if (filters.departmentId) params.departmentId = parseInt(filters.departmentId);
|
||||||
|
if (filters.startDate) params.startDate = filters.startDate;
|
||||||
|
if (filters.endDate) params.endDate = filters.endDate;
|
||||||
|
|
||||||
|
const data = await api.getAllRates(params);
|
||||||
|
setAllRates(data.allRates);
|
||||||
|
setSummary(data.summary);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to fetch rates');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
fetchAllRates();
|
||||||
|
}
|
||||||
|
}, [isSuperAdmin]);
|
||||||
|
|
||||||
|
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFilters(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
fetchAllRates();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
departmentId: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
rateType: '',
|
||||||
|
});
|
||||||
|
setTimeout(fetchAllRates, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter rates based on search and rate type
|
||||||
|
const filteredRates = useMemo(() => {
|
||||||
|
let rates = allRates;
|
||||||
|
|
||||||
|
// Filter by rate type
|
||||||
|
if (filters.rateType) {
|
||||||
|
rates = rates.filter(r => r.rate_type === filters.rateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search query
|
||||||
|
if (searchQuery) {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
rates = rates.filter(r =>
|
||||||
|
r.contractor_name?.toLowerCase().includes(query) ||
|
||||||
|
r.sub_department_name?.toLowerCase().includes(query) ||
|
||||||
|
r.department_name?.toLowerCase().includes(query) ||
|
||||||
|
r.activity?.toLowerCase().includes(query) ||
|
||||||
|
r.created_by_name?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rates;
|
||||||
|
}, [allRates, searchQuery, filters.rateType]);
|
||||||
|
|
||||||
|
// Access check
|
||||||
|
if (!isSuperAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Eye size={48} className="mx-auto text-gray-400 mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold text-gray-700 mb-2">Access Restricted</h2>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
This page is only accessible to Super Admin accounts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<div className="border-b border-gray-200 px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Eye className="text-purple-600" size={24} />
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">All Rates Overview</h2>
|
||||||
|
<p className="text-sm text-gray-500">View all contractor and standard rates across all departments</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Filter size={18} className="text-gray-500" />
|
||||||
|
<h3 className="font-medium text-gray-700">Filters</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<Select
|
||||||
|
label="Department"
|
||||||
|
name="departmentId"
|
||||||
|
value={filters.departmentId}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'All Departments' },
|
||||||
|
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Rate Type"
|
||||||
|
name="rateType"
|
||||||
|
value={filters.rateType}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'All Types' },
|
||||||
|
{ value: 'contractor', label: 'Contractor Rates' },
|
||||||
|
{ value: 'standard', label: 'Standard Rates' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
name="startDate"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
name="endDate"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<Button onClick={applyFilters} size="sm">
|
||||||
|
Apply Filters
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={clearFilters} size="sm">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
{summary && (
|
||||||
|
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-blue-600 font-medium">Total Rates</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-800">{summary.totalRates}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-orange-600 font-medium">Contractor Rates</div>
|
||||||
|
<div className="text-2xl font-bold text-orange-800">{summary.totalContractorRates}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-green-600 font-medium">Standard Rates</div>
|
||||||
|
<div className="text-2xl font-bold text-green-800">{summary.totalStandardRates}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search and Refresh */}
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by contractor, department, activity..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" onClick={fetchAllRates}>
|
||||||
|
<RefreshCw size={16} className="mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8">Loading all rates...</div>
|
||||||
|
) : filteredRates.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Contractor</TableHead>
|
||||||
|
<TableHead>Department</TableHead>
|
||||||
|
<TableHead>Sub-Department</TableHead>
|
||||||
|
<TableHead>Activity</TableHead>
|
||||||
|
<TableHead>Rate (₹)</TableHead>
|
||||||
|
<TableHead>Effective Date</TableHead>
|
||||||
|
<TableHead>Created By</TableHead>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredRates.map((rate, idx) => (
|
||||||
|
<TableRow key={`${rate.rate_type}-${rate.id}-${idx}`}>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
rate.rate_type === 'contractor'
|
||||||
|
? 'bg-orange-100 text-orange-700'
|
||||||
|
: 'bg-green-100 text-green-700'
|
||||||
|
}`}>
|
||||||
|
{rate.rate_type === 'contractor' ? 'Contractor' : 'Standard'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{rate.contractor_name || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{rate.department_name || '-'}</TableCell>
|
||||||
|
<TableCell>{rate.sub_department_name || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
rate.activity === 'Loading' || rate.activity === 'Unloading'
|
||||||
|
? 'bg-purple-100 text-purple-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{rate.activity || 'Standard'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-green-600 font-semibold">₹{rate.rate}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar size={14} className="text-gray-400" />
|
||||||
|
{new Date(rate.effective_date).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-gray-500">
|
||||||
|
{rate.created_by_name || '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No rates found. Adjust your filters or check back later.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
|
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink, RefreshCw } from 'lucide-react';
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts';
|
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts';
|
||||||
import { Card, CardHeader, CardContent } from '../components/ui/Card';
|
import { Card, CardHeader, CardContent } from '../components/ui/Card';
|
||||||
import { useEmployees } from '../hooks/useEmployees';
|
import { useEmployees } from '../hooks/useEmployees';
|
||||||
@@ -43,15 +43,25 @@ interface HierarchyNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardPage: React.FC = () => {
|
export const DashboardPage: React.FC = () => {
|
||||||
const { employees, loading: employeesLoading } = useEmployees();
|
const { employees, loading: employeesLoading, refresh: refreshEmployees } = useEmployees();
|
||||||
const { departments, loading: deptLoading } = useDepartments();
|
const { departments, loading: deptLoading } = useDepartments();
|
||||||
const { allocations, loading: allocLoading } = useWorkAllocations();
|
const { allocations, loading: allocLoading, refresh: refreshAllocations } = useWorkAllocations();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [attendance, setAttendance] = useState<AttendanceRecord[]>([]);
|
const [attendance, setAttendance] = useState<AttendanceRecord[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||||
const [contractorRates, setContractorRates] = useState<Record<number, number>>({});
|
const [contractorRates, setContractorRates] = useState<Record<number, number>>({});
|
||||||
|
|
||||||
|
// Refresh all data function
|
||||||
|
const refreshAllData = () => {
|
||||||
|
refreshEmployees();
|
||||||
|
refreshAllocations();
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
api.getAttendance({ startDate: today, endDate: today })
|
||||||
|
.then(setAttendance)
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
const isSuperAdmin = user?.role === 'SuperAdmin';
|
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||||
const isSupervisor = user?.role === 'Supervisor';
|
const isSupervisor = user?.role === 'Supervisor';
|
||||||
const isContractor = user?.role === 'Contractor';
|
const isContractor = user?.role === 'Contractor';
|
||||||
@@ -161,12 +171,14 @@ export const DashboardPage: React.FC = () => {
|
|||||||
e => e.role === 'Contractor' && e.department_id === supervisor.department_id
|
e => e.role === 'Contractor' && e.department_id === supervisor.department_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const supervisorNode: HierarchyNode = {
|
// Get employees without a contractor but in this department (e.g., swapped employees)
|
||||||
id: supervisor.id,
|
const unassignedEmployees = employees.filter(
|
||||||
name: supervisor.name,
|
e => e.role === 'Employee' &&
|
||||||
role: 'Supervisor',
|
e.department_id === supervisor.department_id &&
|
||||||
department: supervisor.department_name || '',
|
!e.contractor_id
|
||||||
children: deptContractors.map(contractor => {
|
);
|
||||||
|
|
||||||
|
const contractorNodes = deptContractors.map(contractor => {
|
||||||
const contractorEmployees = employees.filter(
|
const contractorEmployees = employees.filter(
|
||||||
e => e.role === 'Employee' && e.contractor_id === contractor.id
|
e => e.role === 'Employee' && e.contractor_id === contractor.id
|
||||||
);
|
);
|
||||||
@@ -178,15 +190,15 @@ export const DashboardPage: React.FC = () => {
|
|||||||
department: contractor.department_name || '',
|
department: contractor.department_name || '',
|
||||||
children: contractorEmployees.map(emp => {
|
children: contractorEmployees.map(emp => {
|
||||||
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
||||||
const empAllocation = allocations.find(a => a.employee_id === emp.id);
|
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: emp.id,
|
id: emp.id,
|
||||||
name: emp.name,
|
name: emp.name,
|
||||||
role: 'Employee',
|
role: 'Employee',
|
||||||
department: emp.department_name || '',
|
department: emp.department_name || '',
|
||||||
subDepartment: emp.sub_department_name,
|
subDepartment: empAllocation?.sub_department_name || '-',
|
||||||
activity: empAllocation?.description || 'Loading',
|
activity: empAllocation?.description || empAllocation?.activity || '-',
|
||||||
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
||||||
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
||||||
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
||||||
@@ -195,7 +207,42 @@ export const DashboardPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add unassigned employees node if there are any
|
||||||
|
if (unassignedEmployees.length > 0) {
|
||||||
|
contractorNodes.push({
|
||||||
|
id: -supervisor.department_id!, // Negative ID to avoid conflicts
|
||||||
|
name: 'Unassigned (Swapped)',
|
||||||
|
role: 'Contractor',
|
||||||
|
department: supervisor.department_name || '',
|
||||||
|
children: unassignedEmployees.map(emp => {
|
||||||
|
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
||||||
|
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: emp.id,
|
||||||
|
name: emp.name,
|
||||||
|
role: 'Employee',
|
||||||
|
department: emp.department_name || '',
|
||||||
|
subDepartment: empAllocation?.sub_department_name || '-',
|
||||||
|
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
|
||||||
|
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
||||||
|
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
||||||
|
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
||||||
|
remark: empAttendance?.remark,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const supervisorNode: HierarchyNode = {
|
||||||
|
id: supervisor.id,
|
||||||
|
name: supervisor.name,
|
||||||
|
role: 'Supervisor',
|
||||||
|
department: supervisor.department_name || '',
|
||||||
|
children: contractorNodes,
|
||||||
};
|
};
|
||||||
|
|
||||||
return supervisorNode;
|
return supervisorNode;
|
||||||
@@ -211,27 +258,34 @@ export const DashboardPage: React.FC = () => {
|
|||||||
e => e.role === 'Contractor' && e.department_id === user.department_id
|
e => e.role === 'Contractor' && e.department_id === user.department_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return deptContractors.map(contractor => {
|
// Get employees without a contractor but in this department (e.g., swapped employees)
|
||||||
|
const unassignedEmployees = employees.filter(
|
||||||
|
e => e.role === 'Employee' &&
|
||||||
|
e.department_id === user.department_id &&
|
||||||
|
!e.contractor_id
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractorNodes: HierarchyNode[] = deptContractors.map(contractor => {
|
||||||
const contractorEmployees = employees.filter(
|
const contractorEmployees = employees.filter(
|
||||||
e => e.role === 'Employee' && e.contractor_id === contractor.id
|
e => e.role === 'Employee' && e.contractor_id === contractor.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const contractorNode: HierarchyNode = {
|
return {
|
||||||
id: contractor.id,
|
id: contractor.id,
|
||||||
name: contractor.name,
|
name: contractor.name,
|
||||||
role: 'Contractor',
|
role: 'Contractor',
|
||||||
department: contractor.department_name || '',
|
department: contractor.department_name || '',
|
||||||
children: contractorEmployees.map(emp => {
|
children: contractorEmployees.map(emp => {
|
||||||
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
||||||
const empAllocation = allocations.find(a => a.employee_id === emp.id);
|
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: emp.id,
|
id: emp.id,
|
||||||
name: emp.name,
|
name: emp.name,
|
||||||
role: 'Employee',
|
role: 'Employee',
|
||||||
department: emp.department_name || '',
|
department: emp.department_name || '',
|
||||||
subDepartment: emp.sub_department_name,
|
subDepartment: empAllocation?.sub_department_name || '-',
|
||||||
activity: empAllocation?.description || '-',
|
activity: empAllocation?.description || empAllocation?.activity || '-',
|
||||||
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
||||||
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
||||||
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
||||||
@@ -240,10 +294,38 @@ export const DashboardPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return contractorNode;
|
|
||||||
});
|
});
|
||||||
}, [isSupervisor, user, employees, attendance, allocations]);
|
|
||||||
|
// Add unassigned employees node if there are any
|
||||||
|
if (unassignedEmployees.length > 0) {
|
||||||
|
contractorNodes.push({
|
||||||
|
id: -user.department_id, // Negative ID to avoid conflicts
|
||||||
|
name: 'Unassigned (Swapped)',
|
||||||
|
role: 'Contractor',
|
||||||
|
department: filteredDepartments[0]?.name || '',
|
||||||
|
children: unassignedEmployees.map(emp => {
|
||||||
|
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
||||||
|
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: emp.id,
|
||||||
|
name: emp.name,
|
||||||
|
role: 'Employee',
|
||||||
|
department: emp.department_name || '',
|
||||||
|
subDepartment: empAllocation?.sub_department_name || '-',
|
||||||
|
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
|
||||||
|
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
||||||
|
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
||||||
|
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
||||||
|
remark: empAttendance?.remark,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return contractorNodes;
|
||||||
|
}, [isSupervisor, user, employees, attendance, allocations, filteredDepartments]);
|
||||||
|
|
||||||
// Department presence data for bar chart
|
// Department presence data for bar chart
|
||||||
const departmentPresenceData = useMemo(() => {
|
const departmentPresenceData = useMemo(() => {
|
||||||
@@ -405,11 +487,20 @@ export const DashboardPage: React.FC = () => {
|
|||||||
{/* Daily Attendance Report Header */}
|
{/* Daily Attendance Report Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold text-gray-800">Daily Attendance Report</h1>
|
<h1 className="text-2xl font-bold text-gray-800">Daily Attendance Report</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={refreshAllData}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={18} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
<Calendar size={18} />
|
<Calendar size={18} />
|
||||||
Date Range
|
Date Range
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="relative max-w-md">
|
<div className="relative max-w-md">
|
||||||
@@ -647,11 +738,20 @@ export const DashboardPage: React.FC = () => {
|
|||||||
<h1 className="text-2xl font-bold text-gray-800">{departmentName} Dashboard</h1>
|
<h1 className="text-2xl font-bold text-gray-800">{departmentName} Dashboard</h1>
|
||||||
<p className="text-gray-500 mt-1">Daily Attendance & Work Overview</p>
|
<p className="text-gray-500 mt-1">Daily Attendance & Work Overview</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={refreshAllData}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={18} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
<Calendar size={18} />
|
<Calendar size={18} />
|
||||||
Date Range
|
Date Range
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="relative max-w-md">
|
<div className="relative max-w-md">
|
||||||
|
|||||||
@@ -202,16 +202,11 @@ export const LoginPage: React.FC = () => {
|
|||||||
<div className="w-full border-t border-white/10" />
|
<div className="w-full border-t border-white/10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center text-xs">
|
<div className="relative flex justify-center text-xs">
|
||||||
<span className="px-4 bg-transparent text-blue-200/50">Work Allocation System</span>
|
<span className="px-4 bg-transparent text-blue-200/50"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Info */}
|
{/* Footer Info */}
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-blue-200/40 text-xs">
|
|
||||||
Secure login powered by JWT authentication
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Version badge */}
|
{/* Version badge */}
|
||||||
|
|||||||
346
src/pages/ReportingPage.tsx
Normal file
346
src/pages/ReportingPage.tsx
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { Download, RefreshCw, Search, FileSpreadsheet, Filter } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '../components/ui/Card';
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input, Select } from '../components/ui/Input';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
import { useDepartments } from '../hooks/useDepartments';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
|
export const ReportingPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { departments } = useDepartments();
|
||||||
|
const [allocations, setAllocations] = useState<any[]>([]);
|
||||||
|
const [summary, setSummary] = useState<{ totalAllocations: number; totalAmount: string; totalUnits: string } | null>(null);
|
||||||
|
const [contractors, setContractors] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
departmentId: '',
|
||||||
|
contractorId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||||
|
|
||||||
|
// Fetch contractors
|
||||||
|
useEffect(() => {
|
||||||
|
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch report data
|
||||||
|
const fetchReport = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const params: any = {};
|
||||||
|
if (filters.startDate) params.startDate = filters.startDate;
|
||||||
|
if (filters.endDate) params.endDate = filters.endDate;
|
||||||
|
if (filters.departmentId && isSuperAdmin) params.departmentId = parseInt(filters.departmentId);
|
||||||
|
if (filters.contractorId) params.contractorId = parseInt(filters.contractorId);
|
||||||
|
|
||||||
|
const data = await api.getCompletedAllocationsReport(params);
|
||||||
|
setAllocations(data.allocations);
|
||||||
|
setSummary(data.summary);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to fetch report');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchReport();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filter allocations based on search
|
||||||
|
const filteredAllocations = useMemo(() => {
|
||||||
|
if (!searchQuery) return allocations;
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return allocations.filter(a =>
|
||||||
|
a.employee_name?.toLowerCase().includes(query) ||
|
||||||
|
a.contractor_name?.toLowerCase().includes(query) ||
|
||||||
|
a.sub_department_name?.toLowerCase().includes(query) ||
|
||||||
|
a.activity?.toLowerCase().includes(query) ||
|
||||||
|
a.department_name?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [allocations, searchQuery]);
|
||||||
|
|
||||||
|
// Export to Excel (CSV format)
|
||||||
|
const exportToExcel = () => {
|
||||||
|
if (filteredAllocations.length === 0) {
|
||||||
|
alert('No data to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define headers
|
||||||
|
const headers = [
|
||||||
|
'ID',
|
||||||
|
'Employee Name',
|
||||||
|
'Employee Phone',
|
||||||
|
'Contractor',
|
||||||
|
'Department',
|
||||||
|
'Sub-Department',
|
||||||
|
'Activity',
|
||||||
|
'Assigned Date',
|
||||||
|
'Completion Date',
|
||||||
|
'Rate (₹)',
|
||||||
|
'Units',
|
||||||
|
'Total Amount (₹)',
|
||||||
|
'Status',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map data to rows
|
||||||
|
const rows = filteredAllocations.map(a => [
|
||||||
|
a.id,
|
||||||
|
a.employee_name || '',
|
||||||
|
a.employee_phone || '',
|
||||||
|
a.contractor_name || '',
|
||||||
|
a.department_name || '',
|
||||||
|
a.sub_department_name || '',
|
||||||
|
a.activity || 'Standard',
|
||||||
|
a.assigned_date ? new Date(a.assigned_date).toLocaleDateString() : '',
|
||||||
|
a.completion_date ? new Date(a.completion_date).toLocaleDateString() : '',
|
||||||
|
a.rate || 0,
|
||||||
|
a.units || '',
|
||||||
|
a.total_amount || a.rate || 0,
|
||||||
|
a.status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create CSV content
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Add summary at the end
|
||||||
|
const summaryRows = [
|
||||||
|
'',
|
||||||
|
'SUMMARY',
|
||||||
|
`Total Allocations,${summary?.totalAllocations || 0}`,
|
||||||
|
`Total Amount (₹),${summary?.totalAmount || 0}`,
|
||||||
|
`Total Units,${summary?.totalUnits || 0}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const fullContent = csvContent + '\n' + summaryRows.join('\n');
|
||||||
|
|
||||||
|
// Create and download file
|
||||||
|
const blob = new Blob([fullContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `completed_work_allocations_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFilters(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
fetchReport();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
departmentId: '',
|
||||||
|
contractorId: '',
|
||||||
|
});
|
||||||
|
setTimeout(fetchReport, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<div className="border-b border-gray-200 px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<FileSpreadsheet className="text-green-600" size={24} />
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2>
|
||||||
|
</div>
|
||||||
|
<Button onClick={exportToExcel} disabled={filteredAllocations.length === 0}>
|
||||||
|
<Download size={16} className="mr-2" />
|
||||||
|
Export to Excel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Filter size={18} className="text-gray-500" />
|
||||||
|
<h3 className="font-medium text-gray-700">Filters</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
name="startDate"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
name="endDate"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<Select
|
||||||
|
label="Department"
|
||||||
|
name="departmentId"
|
||||||
|
value={filters.departmentId}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'All Departments' },
|
||||||
|
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Select
|
||||||
|
label="Contractor"
|
||||||
|
name="contractorId"
|
||||||
|
value={filters.contractorId}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'All Contractors' },
|
||||||
|
...contractors.map(c => ({ value: String(c.id), label: c.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<Button onClick={applyFilters} size="sm">
|
||||||
|
Apply Filters
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={clearFilters} size="sm">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
{summary && (
|
||||||
|
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-blue-600 font-medium">Total Completed</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-800">{summary.totalAllocations}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-green-600 font-medium">Total Amount</div>
|
||||||
|
<div className="text-2xl font-bold text-green-800">₹{parseFloat(summary.totalAmount).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-purple-600 font-medium">Total Units</div>
|
||||||
|
<div className="text-2xl font-bold text-purple-800">{parseFloat(summary.totalUnits).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search and Refresh */}
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by employee, contractor, department..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" onClick={fetchReport}>
|
||||||
|
<RefreshCw size={16} className="mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8">Loading report data...</div>
|
||||||
|
) : filteredAllocations.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Employee</TableHead>
|
||||||
|
<TableHead>Contractor</TableHead>
|
||||||
|
<TableHead>Department</TableHead>
|
||||||
|
<TableHead>Sub-Department</TableHead>
|
||||||
|
<TableHead>Activity</TableHead>
|
||||||
|
<TableHead>Assigned</TableHead>
|
||||||
|
<TableHead>Completed</TableHead>
|
||||||
|
<TableHead>Rate (₹)</TableHead>
|
||||||
|
<TableHead>Units</TableHead>
|
||||||
|
<TableHead>Total (₹)</TableHead>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredAllocations.map((allocation) => {
|
||||||
|
const rate = parseFloat(allocation.rate) || 0;
|
||||||
|
const units = parseFloat(allocation.units) || 0;
|
||||||
|
const total = parseFloat(allocation.total_amount) || rate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={allocation.id}>
|
||||||
|
<TableCell>{allocation.id}</TableCell>
|
||||||
|
<TableCell className="font-medium">{allocation.employee_name || '-'}</TableCell>
|
||||||
|
<TableCell>{allocation.contractor_name || '-'}</TableCell>
|
||||||
|
<TableCell>{allocation.department_name || '-'}</TableCell>
|
||||||
|
<TableCell>{allocation.sub_department_name || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
allocation.activity === 'Loading' || allocation.activity === 'Unloading'
|
||||||
|
? 'bg-purple-100 text-purple-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{allocation.activity || 'Standard'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{allocation.completion_date
|
||||||
|
? new Date(allocation.completion_date).toLocaleDateString()
|
||||||
|
: '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>₹{rate.toFixed(2)}</TableCell>
|
||||||
|
<TableCell>{units > 0 ? units : '-'}</TableCell>
|
||||||
|
<TableCell className="font-semibold text-green-600">₹{total.toFixed(2)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No completed work allocations found. Adjust your filters or check back later.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
523
src/pages/StandardRatesPage.tsx
Normal file
523
src/pages/StandardRatesPage.tsx
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { RefreshCw, Trash2, Edit, DollarSign, Search, Scale, ArrowUpDown } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '../components/ui/Card';
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||||
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Input, Select } from '../components/ui/Input';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
|
export const StandardRatesPage: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'list' | 'add' | 'compare'>('list');
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { departments } = useDepartments();
|
||||||
|
const [standardRates, setStandardRates] = useState<any[]>([]);
|
||||||
|
const [contractors, setContractors] = useState<any[]>([]);
|
||||||
|
const [comparisons, setComparisons] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
subDepartmentId: '',
|
||||||
|
activity: '',
|
||||||
|
rate: '',
|
||||||
|
effectiveDate: new Date().toISOString().split('T')[0],
|
||||||
|
});
|
||||||
|
const [selectedDept, setSelectedDept] = useState('');
|
||||||
|
const { subDepartments } = useSubDepartments(selectedDept);
|
||||||
|
const [formError, setFormError] = useState('');
|
||||||
|
const [formLoading, setFormLoading] = useState(false);
|
||||||
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Compare filters
|
||||||
|
const [compareContractorId, setCompareContractorId] = useState('');
|
||||||
|
|
||||||
|
const isSupervisor = user?.role === 'Supervisor';
|
||||||
|
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
|
||||||
|
|
||||||
|
// Fetch standard rates
|
||||||
|
const fetchStandardRates = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const data = await api.getStandardRates();
|
||||||
|
setStandardRates(data);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to fetch standard rates');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch contractors
|
||||||
|
const fetchContractors = async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.getUsers({ role: 'Contractor' });
|
||||||
|
setContractors(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch contractors:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch comparison data
|
||||||
|
const fetchComparison = async () => {
|
||||||
|
if (!compareContractorId) {
|
||||||
|
setComparisons([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await api.compareRates({ contractorId: parseInt(compareContractorId) });
|
||||||
|
setComparisons(data.comparisons);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to fetch comparison');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStandardRates();
|
||||||
|
fetchContractors();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSupervisor && user?.department_id) {
|
||||||
|
setSelectedDept(String(user.department_id));
|
||||||
|
}
|
||||||
|
}, [isSupervisor, user?.department_id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'compare' && compareContractorId) {
|
||||||
|
fetchComparison();
|
||||||
|
}
|
||||||
|
}, [activeTab, compareContractorId]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
setFormError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
subDepartmentId: '',
|
||||||
|
activity: '',
|
||||||
|
rate: '',
|
||||||
|
effectiveDate: new Date().toISOString().split('T')[0],
|
||||||
|
});
|
||||||
|
setEditingId(null);
|
||||||
|
setFormError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.rate || !formData.effectiveDate) {
|
||||||
|
setFormError('Rate and effective date are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormLoading(true);
|
||||||
|
setFormError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingId) {
|
||||||
|
await api.updateStandardRate(editingId, {
|
||||||
|
rate: parseFloat(formData.rate),
|
||||||
|
activity: formData.activity || undefined,
|
||||||
|
effectiveDate: formData.effectiveDate,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await api.createStandardRate({
|
||||||
|
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined,
|
||||||
|
activity: formData.activity || undefined,
|
||||||
|
rate: parseFloat(formData.rate),
|
||||||
|
effectiveDate: formData.effectiveDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
setActiveTab('list');
|
||||||
|
fetchStandardRates();
|
||||||
|
} catch (err: any) {
|
||||||
|
setFormError(err.message || 'Failed to save rate');
|
||||||
|
} finally {
|
||||||
|
setFormLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (rate: any) => {
|
||||||
|
setFormData({
|
||||||
|
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '',
|
||||||
|
activity: rate.activity || '',
|
||||||
|
rate: String(rate.rate),
|
||||||
|
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0],
|
||||||
|
});
|
||||||
|
if (rate.department_id) {
|
||||||
|
setSelectedDept(String(rate.department_id));
|
||||||
|
}
|
||||||
|
setEditingId(rate.id);
|
||||||
|
setActiveTab('add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this standard rate?')) return;
|
||||||
|
try {
|
||||||
|
await api.deleteStandardRate(id);
|
||||||
|
fetchStandardRates();
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.message || 'Failed to delete rate');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter rates based on search
|
||||||
|
const filteredRates = useMemo(() => {
|
||||||
|
if (!searchQuery) return standardRates;
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return standardRates.filter(rate =>
|
||||||
|
rate.sub_department_name?.toLowerCase().includes(query) ||
|
||||||
|
rate.department_name?.toLowerCase().includes(query) ||
|
||||||
|
rate.activity?.toLowerCase().includes(query) ||
|
||||||
|
rate.created_by_name?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [standardRates, searchQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<div className="border-b border-gray-200">
|
||||||
|
<div className="flex space-x-8 px-6">
|
||||||
|
<button
|
||||||
|
onClick={() => { setActiveTab('list'); resetForm(); }}
|
||||||
|
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||||
|
activeTab === 'list'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Standard Rates
|
||||||
|
</button>
|
||||||
|
{canManageRates && (
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('add')}
|
||||||
|
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||||
|
activeTab === 'add'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{editingId ? 'Edit Rate' : 'Add Standard Rate'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('compare')}
|
||||||
|
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||||
|
activeTab === 'compare'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Scale size={16} className="inline mr-1" />
|
||||||
|
Compare Rates
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{activeTab === 'list' && (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
<div className="relative min-w-[300px] flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by sub-department, activity..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" onClick={fetchStandardRates}>
|
||||||
|
<RefreshCw size={16} className="mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
<strong>Standard Rates</strong> are default rates set by supervisors for sub-departments and activities.
|
||||||
|
These are used as benchmarks to compare against contractor-specific rates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8">Loading standard rates...</div>
|
||||||
|
) : filteredRates.length > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableHead>Department</TableHead>
|
||||||
|
<TableHead>Sub-Department</TableHead>
|
||||||
|
<TableHead>Activity</TableHead>
|
||||||
|
<TableHead>Rate (₹)</TableHead>
|
||||||
|
<TableHead>Effective Date</TableHead>
|
||||||
|
<TableHead>Created By</TableHead>
|
||||||
|
{canManageRates && <TableHead>Actions</TableHead>}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredRates.map((rate) => (
|
||||||
|
<TableRow key={rate.id}>
|
||||||
|
<TableCell>{rate.department_name || '-'}</TableCell>
|
||||||
|
<TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
rate.activity === 'Loading' || rate.activity === 'Unloading'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{rate.activity || 'Standard'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-green-600 font-semibold">₹{rate.rate}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell className="text-gray-500">{rate.created_by_name || '-'}</TableCell>
|
||||||
|
{canManageRates && (
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(rate)}
|
||||||
|
className="text-blue-600"
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<Edit size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(rate.id)}
|
||||||
|
className="text-red-600"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No standard rates configured yet. Add one to get started!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'add' && canManageRates && (
|
||||||
|
<div className="max-w-2xl space-y-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
{editingId ? 'Edit Standard Rate' : 'Add New Standard Rate'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||||
|
<h4 className="font-medium text-yellow-800 mb-2">About Standard Rates</h4>
|
||||||
|
<p className="text-sm text-yellow-700">
|
||||||
|
Standard rates serve as default benchmarks for sub-departments and activities.
|
||||||
|
Contractor rates can be compared against these to identify deviations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formError && (
|
||||||
|
<div className="p-3 bg-red-100 text-red-700 rounded-md">
|
||||||
|
{formError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{isSupervisor ? (
|
||||||
|
<Input
|
||||||
|
label="Department"
|
||||||
|
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
label="Department"
|
||||||
|
value={selectedDept}
|
||||||
|
onChange={(e) => setSelectedDept(e.target.value)}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select Department' },
|
||||||
|
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Select
|
||||||
|
label="Sub-Department"
|
||||||
|
name="subDepartmentId"
|
||||||
|
value={formData.subDepartmentId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={!!editingId}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'All Sub-Departments' },
|
||||||
|
...subDepartments.map(s => ({ value: String(s.id), label: s.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Activity Type"
|
||||||
|
name="activity"
|
||||||
|
value={formData.activity}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Standard (Default)' },
|
||||||
|
{ value: 'Loading', label: 'Loading (per unit)' },
|
||||||
|
{ value: 'Unloading', label: 'Unloading (per unit)' },
|
||||||
|
{ value: 'Other', label: 'Other' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
|
||||||
|
? "Rate per Unit (₹)"
|
||||||
|
: "Standard Rate (₹)"}
|
||||||
|
name="rate"
|
||||||
|
type="number"
|
||||||
|
value={formData.rate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter rate amount"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Effective Date"
|
||||||
|
name="effectiveDate"
|
||||||
|
type="date"
|
||||||
|
value={formData.effectiveDate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={formLoading}>
|
||||||
|
{formLoading ? 'Saving...' : (
|
||||||
|
<>
|
||||||
|
<DollarSign size={16} className="mr-2" />
|
||||||
|
{editingId ? 'Update Rate' : 'Add Standard Rate'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'compare' && (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||||
|
<ArrowUpDown size={20} className="inline mr-2" />
|
||||||
|
Compare Contractor Rates vs Standard Rates
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-4 items-end">
|
||||||
|
<div className="w-64">
|
||||||
|
<Select
|
||||||
|
label="Select Contractor"
|
||||||
|
value={compareContractorId}
|
||||||
|
onChange={(e) => setCompareContractorId(e.target.value)}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select Contractor' },
|
||||||
|
...contractors.map(c => ({ value: String(c.id), label: c.name }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={fetchComparison} disabled={!compareContractorId}>
|
||||||
|
Compare
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8">Loading comparison...</div>
|
||||||
|
) : comparisons.length > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableHead>Sub-Department</TableHead>
|
||||||
|
<TableHead>Activity</TableHead>
|
||||||
|
<TableHead>Contractor Rate (₹)</TableHead>
|
||||||
|
<TableHead>Standard Rate (₹)</TableHead>
|
||||||
|
<TableHead>Difference (₹)</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{comparisons.map((comp, idx) => (
|
||||||
|
<TableRow key={idx}>
|
||||||
|
<TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
comp.activity === 'Loading' || comp.activity === 'Unloading'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{comp.activity || 'Standard'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-semibold">₹{comp.rate}</TableCell>
|
||||||
|
<TableCell className="text-gray-600">₹{comp.standard_rate}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`font-semibold ${
|
||||||
|
comp.difference > 0 ? 'text-red-600' :
|
||||||
|
comp.difference < 0 ? 'text-green-600' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{comp.difference > 0 ? '+' : ''}₹{comp.difference.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{comp.is_above_standard ? (
|
||||||
|
<span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">
|
||||||
|
Above Standard ({comp.percentage_difference}%)
|
||||||
|
</span>
|
||||||
|
) : comp.is_below_standard ? (
|
||||||
|
<span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">
|
||||||
|
Below Standard ({comp.percentage_difference}%)
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">
|
||||||
|
At Standard
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : compareContractorId ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No rates found for this contractor to compare.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
Select a contractor to compare their rates against standard rates.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -253,6 +253,108 @@ class ApiService {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reports
|
||||||
|
async getCompletedAllocationsReport(params?: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
departmentId?: number;
|
||||||
|
contractorId?: number;
|
||||||
|
employeeId?: number;
|
||||||
|
}) {
|
||||||
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
return this.request<{
|
||||||
|
allocations: any[];
|
||||||
|
summary: { totalAllocations: number; totalAmount: string; totalUnits: string }
|
||||||
|
}>(`/reports/completed-allocations${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReportSummary(params?: { startDate?: string; endDate?: string }) {
|
||||||
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
return this.request<{
|
||||||
|
byContractor: any[];
|
||||||
|
bySubDepartment: any[];
|
||||||
|
byActivity: any[];
|
||||||
|
}>(`/reports/summary${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Rates
|
||||||
|
async getStandardRates(params?: { departmentId?: number; subDepartmentId?: number; activity?: string }) {
|
||||||
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
return this.request<any[]>(`/standard-rates${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllRates(params?: { departmentId?: number; startDate?: string; endDate?: string }) {
|
||||||
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
return this.request<{
|
||||||
|
allRates: any[];
|
||||||
|
summary: { totalContractorRates: number; totalStandardRates: number; totalRates: number };
|
||||||
|
}>(`/standard-rates/all-rates${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async compareRates(params?: { contractorId?: number; subDepartmentId?: number }) {
|
||||||
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
return this.request<{
|
||||||
|
standardRates: any[];
|
||||||
|
contractorRates: any[];
|
||||||
|
comparisons: any[];
|
||||||
|
}>(`/standard-rates/compare${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createStandardRate(data: {
|
||||||
|
subDepartmentId?: number;
|
||||||
|
activity?: string;
|
||||||
|
rate: number;
|
||||||
|
effectiveDate: string
|
||||||
|
}) {
|
||||||
|
return this.request<any>('/standard-rates', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStandardRate(id: number, data: { rate?: number; activity?: string; effectiveDate?: string }) {
|
||||||
|
return this.request<any>(`/standard-rates/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStandardRate(id: number) {
|
||||||
|
return this.request<{ message: string }>(`/standard-rates/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activities
|
||||||
|
async getActivities(params?: { departmentId?: number; subDepartmentId?: number }) {
|
||||||
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
return this.request<any[]>(`/activities${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActivity(id: number) {
|
||||||
|
return this.request<any>(`/activities/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createActivity(data: { sub_department_id: number; name: string; unit_of_measurement?: string }) {
|
||||||
|
return this.request<any>('/activities', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateActivity(id: number, data: { name?: string; unit_of_measurement?: string }) {
|
||||||
|
return this.request<any>(`/activities/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteActivity(id: number) {
|
||||||
|
return this.request<{ message: string }>(`/activities/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new ApiService(API_BASE_URL);
|
export const api = new ApiService(API_BASE_URL);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface User {
|
|||||||
contractor_name?: string;
|
contractor_name?: string;
|
||||||
sub_department_id?: number;
|
sub_department_id?: number;
|
||||||
sub_department_name?: string;
|
sub_department_name?: string;
|
||||||
|
primary_activity?: string;
|
||||||
// Common fields for Employee and Contractor
|
// Common fields for Employee and Contractor
|
||||||
phone_number?: string;
|
phone_number?: string;
|
||||||
aadhar_number?: string;
|
aadhar_number?: string;
|
||||||
@@ -35,9 +36,20 @@ export interface SubDepartment {
|
|||||||
id: number;
|
id: number;
|
||||||
department_id: number;
|
department_id: number;
|
||||||
name: string;
|
name: string;
|
||||||
primary_activity: string;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
department_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
id: number;
|
||||||
|
sub_department_id: number;
|
||||||
|
name: string;
|
||||||
|
unit_of_measurement: 'Per Bag' | 'Fixed Rate-Per Person';
|
||||||
|
created_at: string;
|
||||||
|
sub_department_name?: string;
|
||||||
|
department_id?: number;
|
||||||
|
department_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkAllocation {
|
export interface WorkAllocation {
|
||||||
@@ -111,10 +123,43 @@ export interface EmployeeSwap {
|
|||||||
export interface ContractorRate {
|
export interface ContractorRate {
|
||||||
id: number;
|
id: number;
|
||||||
contractor_id: number;
|
contractor_id: number;
|
||||||
|
sub_department_id?: number;
|
||||||
|
activity?: string;
|
||||||
rate: number;
|
rate: number;
|
||||||
effective_date: string;
|
effective_date: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
contractor_name?: string;
|
contractor_name?: string;
|
||||||
contractor_username?: string;
|
contractor_username?: string;
|
||||||
|
sub_department_name?: string;
|
||||||
|
department_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StandardRate {
|
||||||
|
id: number;
|
||||||
|
sub_department_id?: number;
|
||||||
|
activity?: string;
|
||||||
|
rate: number;
|
||||||
|
effective_date: string;
|
||||||
|
created_by: number;
|
||||||
|
created_at: string;
|
||||||
|
sub_department_name?: string;
|
||||||
|
department_name?: string;
|
||||||
|
department_id?: number;
|
||||||
|
created_by_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateComparison {
|
||||||
|
id: number;
|
||||||
|
contractor_id: number;
|
||||||
|
contractor_name: string;
|
||||||
|
sub_department_id?: number;
|
||||||
|
sub_department_name?: string;
|
||||||
|
activity?: string;
|
||||||
|
rate: number;
|
||||||
|
standard_rate: number;
|
||||||
|
difference: number;
|
||||||
|
percentage_difference: string | null;
|
||||||
|
is_above_standard: boolean;
|
||||||
|
is_below_standard: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ check_db_connection() {
|
|||||||
docker exec work_allocation_db mysql -u root -padmin123 -e "SELECT 1" &> /dev/null
|
docker exec work_allocation_db mysql -u root -padmin123 -e "SELECT 1" &> /dev/null
|
||||||
return $?
|
return $?
|
||||||
elif command -v mysql &> /dev/null; then
|
elif command -v mysql &> /dev/null; then
|
||||||
mysql -h localhost -P 3306 -u root -padmin123 -e "SELECT 1" &> /dev/null
|
mysql -h localhost -P 3307 -u root -padmin123 -e "SELECT 1" &> /dev/null
|
||||||
return $?
|
return $?
|
||||||
else
|
else
|
||||||
# Try using nc to check if port is open
|
# Try using nc to check if port is open
|
||||||
nc -z localhost 3306 &> /dev/null
|
nc -z localhost 3307 &> /dev/null
|
||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
259
test_employee_swap.sh
Executable file
259
test_employee_swap.sh
Executable file
@@ -0,0 +1,259 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Employee Swap Backend Test Script
|
||||||
|
# Tests the employee swap functionality including create, complete, and cancel operations
|
||||||
|
|
||||||
|
BASE_URL="http://localhost:3000/api"
|
||||||
|
ADMIN_USER="admin"
|
||||||
|
ADMIN_PASS="admin123"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Employee Swap Backend Test"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print test results
|
||||||
|
print_result() {
|
||||||
|
if [ $1 -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✓ PASS${NC}: $2"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ FAIL${NC}: $2"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Login as SuperAdmin
|
||||||
|
echo "Step 1: Logging in as SuperAdmin..."
|
||||||
|
LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"$ADMIN_USER\",\"password\":\"$ADMIN_PASS\"}")
|
||||||
|
|
||||||
|
TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.token')
|
||||||
|
|
||||||
|
if [ "$TOKEN" != "null" ] && [ -n "$TOKEN" ]; then
|
||||||
|
print_result 0 "Login successful"
|
||||||
|
echo " Token: ${TOKEN:0:30}..."
|
||||||
|
else
|
||||||
|
print_result 1 "Login failed"
|
||||||
|
echo " Response: $LOGIN_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: Get list of employees
|
||||||
|
echo "Step 2: Fetching employees..."
|
||||||
|
EMPLOYEES_RESPONSE=$(curl -s -X GET "$BASE_URL/users?role=Employee" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
EMPLOYEE_COUNT=$(echo $EMPLOYEES_RESPONSE | jq 'length')
|
||||||
|
if [ "$EMPLOYEE_COUNT" -gt 0 ]; then
|
||||||
|
print_result 0 "Found $EMPLOYEE_COUNT employees"
|
||||||
|
FIRST_EMPLOYEE_ID=$(echo $EMPLOYEES_RESPONSE | jq '.[0].id')
|
||||||
|
FIRST_EMPLOYEE_NAME=$(echo $EMPLOYEES_RESPONSE | jq -r '.[0].name')
|
||||||
|
FIRST_EMPLOYEE_DEPT=$(echo $EMPLOYEES_RESPONSE | jq '.[0].department_id')
|
||||||
|
FIRST_EMPLOYEE_CONTRACTOR=$(echo $EMPLOYEES_RESPONSE | jq '.[0].contractor_id')
|
||||||
|
echo " Test employee: $FIRST_EMPLOYEE_NAME (ID: $FIRST_EMPLOYEE_ID)"
|
||||||
|
echo " Current department_id: $FIRST_EMPLOYEE_DEPT"
|
||||||
|
echo " Current contractor_id: $FIRST_EMPLOYEE_CONTRACTOR"
|
||||||
|
else
|
||||||
|
print_result 1 "No employees found"
|
||||||
|
echo " Response: $EMPLOYEES_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 3: Get list of departments
|
||||||
|
echo "Step 3: Fetching departments..."
|
||||||
|
DEPTS_RESPONSE=$(curl -s -X GET "$BASE_URL/departments" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
DEPT_COUNT=$(echo $DEPTS_RESPONSE | jq 'length')
|
||||||
|
if [ "$DEPT_COUNT" -gt 1 ]; then
|
||||||
|
print_result 0 "Found $DEPT_COUNT departments"
|
||||||
|
# Find a different department than the employee's current one
|
||||||
|
TARGET_DEPT_ID=$(echo $DEPTS_RESPONSE | jq --argjson current "$FIRST_EMPLOYEE_DEPT" '[.[] | select(.id != $current)][0].id')
|
||||||
|
TARGET_DEPT_NAME=$(echo $DEPTS_RESPONSE | jq -r --argjson current "$FIRST_EMPLOYEE_DEPT" '[.[] | select(.id != $current)][0].name')
|
||||||
|
echo " Target department: $TARGET_DEPT_NAME (ID: $TARGET_DEPT_ID)"
|
||||||
|
else
|
||||||
|
print_result 1 "Need at least 2 departments for swap test"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 4: Get contractors in target department
|
||||||
|
echo "Step 4: Fetching contractors in target department..."
|
||||||
|
CONTRACTORS_RESPONSE=$(curl -s -X GET "$BASE_URL/users?role=Contractor&departmentId=$TARGET_DEPT_ID" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
CONTRACTOR_COUNT=$(echo $CONTRACTORS_RESPONSE | jq 'length')
|
||||||
|
if [ "$CONTRACTOR_COUNT" -gt 0 ]; then
|
||||||
|
TARGET_CONTRACTOR_ID=$(echo $CONTRACTORS_RESPONSE | jq '.[0].id')
|
||||||
|
TARGET_CONTRACTOR_NAME=$(echo $CONTRACTORS_RESPONSE | jq -r '.[0].name')
|
||||||
|
print_result 0 "Found contractor: $TARGET_CONTRACTOR_NAME (ID: $TARGET_CONTRACTOR_ID)"
|
||||||
|
else
|
||||||
|
TARGET_CONTRACTOR_ID="null"
|
||||||
|
echo -e "${YELLOW}⚠ WARNING${NC}: No contractors in target department, will swap without contractor"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 5: Check for existing active swaps
|
||||||
|
echo "Step 5: Checking for existing active swaps..."
|
||||||
|
EXISTING_SWAPS=$(curl -s -X GET "$BASE_URL/employee-swaps?status=Active" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
ACTIVE_SWAP_FOR_EMPLOYEE=$(echo $EXISTING_SWAPS | jq --argjson empId "$FIRST_EMPLOYEE_ID" '[.[] | select(.employee_id == $empId)][0]')
|
||||||
|
if [ "$ACTIVE_SWAP_FOR_EMPLOYEE" != "null" ]; then
|
||||||
|
EXISTING_SWAP_ID=$(echo $ACTIVE_SWAP_FOR_EMPLOYEE | jq '.id')
|
||||||
|
echo -e "${YELLOW}⚠ WARNING${NC}: Employee already has active swap (ID: $EXISTING_SWAP_ID), cancelling it first..."
|
||||||
|
|
||||||
|
CANCEL_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$EXISTING_SWAP_ID/cancel" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
echo " Cancelled existing swap"
|
||||||
|
fi
|
||||||
|
print_result 0 "No blocking active swaps"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 6: Create a new employee swap
|
||||||
|
echo "Step 6: Creating employee swap..."
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
SWAP_DATA="{
|
||||||
|
\"employeeId\": $FIRST_EMPLOYEE_ID,
|
||||||
|
\"targetDepartmentId\": $TARGET_DEPT_ID,
|
||||||
|
\"targetContractorId\": $TARGET_CONTRACTOR_ID,
|
||||||
|
\"swapReason\": \"FinishedEarly\",
|
||||||
|
\"reasonDetails\": \"Test swap from backend test script\",
|
||||||
|
\"workCompletionPercentage\": 75,
|
||||||
|
\"swapDate\": \"$TODAY\"
|
||||||
|
}"
|
||||||
|
|
||||||
|
CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/employee-swaps" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$SWAP_DATA")
|
||||||
|
|
||||||
|
SWAP_ID=$(echo $CREATE_RESPONSE | jq '.id')
|
||||||
|
if [ "$SWAP_ID" != "null" ] && [ -n "$SWAP_ID" ]; then
|
||||||
|
print_result 0 "Swap created successfully (ID: $SWAP_ID)"
|
||||||
|
echo " Employee: $(echo $CREATE_RESPONSE | jq -r '.employee_name')"
|
||||||
|
echo " From: $(echo $CREATE_RESPONSE | jq -r '.original_department_name') → To: $(echo $CREATE_RESPONSE | jq -r '.target_department_name')"
|
||||||
|
echo " Status: $(echo $CREATE_RESPONSE | jq -r '.status')"
|
||||||
|
else
|
||||||
|
print_result 1 "Failed to create swap"
|
||||||
|
echo " Response: $CREATE_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 7: Verify employee's department was updated
|
||||||
|
echo "Step 7: Verifying employee's department was updated..."
|
||||||
|
UPDATED_EMPLOYEE=$(curl -s -X GET "$BASE_URL/users/$FIRST_EMPLOYEE_ID" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
NEW_DEPT_ID=$(echo $UPDATED_EMPLOYEE | jq '.department_id')
|
||||||
|
NEW_CONTRACTOR_ID=$(echo $UPDATED_EMPLOYEE | jq '.contractor_id')
|
||||||
|
|
||||||
|
if [ "$NEW_DEPT_ID" == "$TARGET_DEPT_ID" ]; then
|
||||||
|
print_result 0 "Employee department updated correctly"
|
||||||
|
echo " New department_id: $NEW_DEPT_ID (was: $FIRST_EMPLOYEE_DEPT)"
|
||||||
|
echo " New contractor_id: $NEW_CONTRACTOR_ID (was: $FIRST_EMPLOYEE_CONTRACTOR)"
|
||||||
|
else
|
||||||
|
print_result 1 "Employee department NOT updated"
|
||||||
|
echo " Expected: $TARGET_DEPT_ID, Got: $NEW_DEPT_ID"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 8: Get swap by ID
|
||||||
|
echo "Step 8: Fetching swap by ID..."
|
||||||
|
GET_SWAP_RESPONSE=$(curl -s -X GET "$BASE_URL/employee-swaps/$SWAP_ID" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
FETCHED_SWAP_ID=$(echo $GET_SWAP_RESPONSE | jq '.id')
|
||||||
|
if [ "$FETCHED_SWAP_ID" == "$SWAP_ID" ]; then
|
||||||
|
print_result 0 "Swap fetched successfully"
|
||||||
|
else
|
||||||
|
print_result 1 "Failed to fetch swap"
|
||||||
|
echo " Response: $GET_SWAP_RESPONSE"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 9: Complete the swap (return employee to original department)
|
||||||
|
echo "Step 9: Completing swap (returning employee to original department)..."
|
||||||
|
COMPLETE_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$SWAP_ID/complete" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
COMPLETED_STATUS=$(echo $COMPLETE_RESPONSE | jq -r '.status')
|
||||||
|
if [ "$COMPLETED_STATUS" == "Completed" ]; then
|
||||||
|
print_result 0 "Swap completed successfully"
|
||||||
|
echo " Status: $COMPLETED_STATUS"
|
||||||
|
else
|
||||||
|
print_result 1 "Failed to complete swap"
|
||||||
|
echo " Response: $COMPLETE_RESPONSE"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 10: Verify employee returned to original department
|
||||||
|
echo "Step 10: Verifying employee returned to original department..."
|
||||||
|
FINAL_EMPLOYEE=$(curl -s -X GET "$BASE_URL/users/$FIRST_EMPLOYEE_ID" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
FINAL_DEPT_ID=$(echo $FINAL_EMPLOYEE | jq '.department_id')
|
||||||
|
FINAL_CONTRACTOR_ID=$(echo $FINAL_EMPLOYEE | jq '.contractor_id')
|
||||||
|
|
||||||
|
if [ "$FINAL_DEPT_ID" == "$FIRST_EMPLOYEE_DEPT" ]; then
|
||||||
|
print_result 0 "Employee returned to original department"
|
||||||
|
echo " Final department_id: $FINAL_DEPT_ID"
|
||||||
|
echo " Final contractor_id: $FINAL_CONTRACTOR_ID"
|
||||||
|
else
|
||||||
|
print_result 1 "Employee NOT returned to original department"
|
||||||
|
echo " Expected: $FIRST_EMPLOYEE_DEPT, Got: $FINAL_DEPT_ID"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 11: Test swap cancellation
|
||||||
|
echo "Step 11: Testing swap cancellation..."
|
||||||
|
# Create another swap
|
||||||
|
CREATE_RESPONSE2=$(curl -s -X POST "$BASE_URL/employee-swaps" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$SWAP_DATA")
|
||||||
|
|
||||||
|
SWAP_ID2=$(echo $CREATE_RESPONSE2 | jq '.id')
|
||||||
|
if [ "$SWAP_ID2" != "null" ]; then
|
||||||
|
echo " Created test swap (ID: $SWAP_ID2)"
|
||||||
|
|
||||||
|
# Cancel it
|
||||||
|
CANCEL_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$SWAP_ID2/cancel" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
CANCELLED_STATUS=$(echo $CANCEL_RESPONSE | jq -r '.status')
|
||||||
|
if [ "$CANCELLED_STATUS" == "Cancelled" ]; then
|
||||||
|
print_result 0 "Swap cancelled successfully"
|
||||||
|
else
|
||||||
|
print_result 1 "Failed to cancel swap"
|
||||||
|
echo " Response: $CANCEL_RESPONSE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_result 1 "Failed to create swap for cancellation test"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "========================================"
|
||||||
|
echo " Test Summary"
|
||||||
|
echo "========================================"
|
||||||
|
echo -e "${GREEN}All employee swap backend tests completed!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Tested operations:"
|
||||||
|
echo " ✓ Login as SuperAdmin"
|
||||||
|
echo " ✓ Fetch employees and departments"
|
||||||
|
echo " ✓ Create employee swap"
|
||||||
|
echo " ✓ Verify employee department/contractor update"
|
||||||
|
echo " ✓ Fetch swap by ID"
|
||||||
|
echo " ✓ Complete swap (return to original)"
|
||||||
|
echo " ✓ Cancel swap"
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user