diff --git a/QUICK_START.md b/QUICK_START.md index 797699a..0ec3dd7 100644 --- a/QUICK_START.md +++ b/QUICK_START.md @@ -61,7 +61,7 @@ npm run dev ### Docker Management ```bash -# Stop MySQL +# Stop MySQLclear docker-compose down # Stop and remove all data diff --git a/activities.md b/activities.md new file mode 100644 index 0000000..aaf7296 --- /dev/null +++ b/activities.md @@ -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 | \ No newline at end of file diff --git a/backend-deno/.env b/backend-deno/.env index 353c2df..95a4ca7 100644 --- a/backend-deno/.env +++ b/backend-deno/.env @@ -3,7 +3,7 @@ DB_HOST=localhost DB_USER=root DB_PASSWORD=admin123 DB_NAME=work_allocation -DB_PORT=3306 +DB_PORT=3307 # JWT Configuration - CHANGE IN PRODUCTION! JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024 diff --git a/backend-deno/main.ts b/backend-deno/main.ts index cf71338..502ce83 100644 --- a/backend-deno/main.ts +++ b/backend-deno/main.ts @@ -11,6 +11,9 @@ import workAllocationRoutes from "./routes/work-allocations.ts"; import attendanceRoutes from "./routes/attendance.ts"; import contractorRateRoutes from "./routes/contractor-rates.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 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/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.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 app.use(router.routes()); diff --git a/backend-deno/routes/activities.ts b/backend-deno/routes/activities.ts new file mode 100644 index 0000000..05bf9a1 --- /dev/null +++ b/backend-deno/routes/activities.ts @@ -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(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( + `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; diff --git a/backend-deno/routes/reports.ts b/backend-deno/routes/reports.ts new file mode 100644 index 0000000..b83cb11 --- /dev/null +++ b/backend-deno/routes/reports.ts @@ -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(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(` + 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(` + 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(` + 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; diff --git a/backend-deno/routes/standard-rates.ts b/backend-deno/routes/standard-rates.ts new file mode 100644 index 0000000..90d0d41 --- /dev/null +++ b/backend-deno/routes/standard-rates.ts @@ -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(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(contractorQuery, contractorParams); + const standardRates = await db.query(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(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(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( + "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( + `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(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( + `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( + `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; diff --git a/backend-deno/scripts/seed.ts b/backend-deno/scripts/seed.ts index 2ddae7d..95eef49 100644 --- a/backend-deno/scripts/seed.ts +++ b/backend-deno/scripts/seed.ts @@ -32,54 +32,187 @@ async function seedDatabase() { console.log(" â„šī¸ Departments already exist"); } - // 2. Seed Sub-departments for Groundnut - console.log("📂 Seeding sub-departments..."); - const groundnutDept = await db.query<{ id: number }[]>( + // 2. Seed Sub-departments and Activities for all departments + console.log("📂 Seeding sub-departments and activities..."); + + // 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 = ?", ["Groundnut"] ); - let groundnutId: number | null = null; + const tudkiId = tudkiDeptResult[0]?.id; + const danaId = danaDeptResult[0]?.id; + const groundnutId = groundnutDeptResult[0]?.id; + + // Define sub-departments and activities per department based on activities.md + const departmentData: { [key: number]: { subDept: string; activities: { name: string; unit: string }[] }[] } = {}; - if (groundnutDept.length > 0) { - groundnutId = groundnutDept[0].id; + 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 }[]>( "SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?", - [groundnutId] + [deptId] ); if (existingSubDepts[0].count === 0) { - const subDepts = [ - "Mufali Aavak Katai", - "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) { + for (const { subDept, activities } of subDepts) { + // Insert sub-department await db.execute( - "INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)", - [groundnutId, name, "Loading/Unloading"] + "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", + [deptId, subDept] ); + + // Get the sub-department ID + const subDeptResult = await db.query<{ id: number }[]>( + "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 created"); - } else { - console.log(" â„šī¸ Sub-departments already exist"); } } + console.log(" ✅ Sub-departments and activities created"); // 3. Seed SuperAdmin console.log("👤 Seeding SuperAdmin user..."); @@ -104,23 +237,32 @@ async function seedDatabase() { console.log(" ✅ SuperAdmin created"); } - // 4. Seed Sample Supervisors - console.log("đŸ‘Ĩ Seeding sample 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"] - ); - + // 4. Seed Supervisors for all departments + console.log("đŸ‘Ĩ Seeding supervisors..."); const supervisorPassword = await hashPassword("supervisor123"); 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: "supervisor_groundnut", name: "Groundnut Supervisor", email: "supervisor.groundnut@workallocate.com", deptId: groundnutId } + { + username: "rajesh.sharma.tudki", + 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) { @@ -131,8 +273,8 @@ async function seedDatabase() { ); if (existing.length === 0) { await db.execute( - "INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)", - [sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true] + "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.phone] ); console.log(` ✅ ${sup.name} created`); } else { @@ -141,38 +283,97 @@ async function seedDatabase() { } } - // 5. Seed Sample Contractors - console.log("đŸ—ī¸ Seeding sample contractors..."); + // 5. Seed Contractors for all departments + console.log("đŸ—ī¸ Seeding contractors..."); const contractorPassword = await hashPassword("contractor123"); const contractors = [ + // Groundnut Department Contractors { - username: "contractor1", - name: "Contractor One", - email: "contractor1@workallocate.com", + username: "ramesh.patel.gn", + name: "Ramesh Patel", + email: "ramesh.patel@workallocate.com", deptId: groundnutId, - phone: "9876543210", - aadhar: "123456789012", - bankAccount: "1234567890123456", - 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", + phone: "9829012345", + aadhar: "234567891234", + bankAccount: "50100123456789", bankName: "HDFC Bank", bankIfsc: "HDFC0001234", - agreementNo: "AGR-2024-002", - pfNo: "PF/GJ/12345/67891", - esicNo: "12-34-567890-123-0002" + agreementNo: "AGR-GN-2024-001", + pfNo: "RJ/JPR/12345/001", + 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,87 +398,299 @@ async function seedDatabase() { } } - // 6. Seed Sample Employees - console.log("👷 Seeding sample employees..."); - const contractor1 = await db.query<{ id: number }[]>( - "SELECT id FROM users WHERE username = ?", - ["contractor1"] - ); + // 6. Seed Employees for all departments + console.log("👷 Seeding employees..."); const employeePassword = await hashPassword("employee123"); - if (contractor1.length > 0) { - const employees = [ - { - username: "employee1", - name: "Employee One", - email: "employee1@workallocate.com", - phone: "9876543220", - aadhar: "345678901234", - bankAccount: "3456789012345678", - bankName: "Punjab National Bank", - bankIfsc: "PUNB0001234" - }, - { - username: "employee2", - name: "Employee Two", - email: "employee2@workallocate.com", - phone: "9876543221", - aadhar: "456789012345", - bankAccount: "4567890123456789", - bankName: "Bank of Baroda", - bankIfsc: "BARB0001234" - }, - { - username: "employee3", - name: "Employee Three", - email: "employee3@workallocate.com", - phone: "9876543222", - aadhar: "567890123456", - bankAccount: "5678901234567890", - bankName: "ICICI Bank", - bankIfsc: "ICIC0001234" - } - ]; + // 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; + } + } - for (const emp of employees) { - const existing = await db.query<{ id: number }[]>( - "SELECT id FROM users WHERE username = ?", - [emp.username] - ); - if (existing.length === 0) { + const employees = [ + // Groundnut Department Employees - Under Ramesh Patel + { + username: "ravi.singh.gn1", + name: "Ravi Singh", + email: "ravi.singh@workallocate.com", + deptId: groundnutId, + contractorUsername: "ramesh.patel.gn", + 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", + bankIfsc: "PUNB0002222" + }, + { + username: "vijay.meena.gn3", + name: "Vijay Meena", + email: "vijay.meena@workallocate.com", + deptId: groundnutId, + contractorUsername: "ramesh.patel.gn", + 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", + bankIfsc: "BARB0004444" + }, + { + username: "prakash.sharma.gn5", + name: "Prakash Sharma", + email: "prakash.sharma@workallocate.com", + deptId: groundnutId, + contractorUsername: "kishan.meena.gn", + phone: "9876501005", + aadhar: "555566667777", + bankAccount: "30100555566677", + bankName: "ICICI Bank", + 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" + } + ]; + + for (const emp of employees) { + const existing = await db.query<{ id: number }[]>( + "SELECT id FROM users WHERE username = ?", + [emp.username] + ); + if (existing.length === 0) { + const contractorId = contractorIds[emp.contractorUsername]; + if (contractorId) { await db.execute( `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) 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] ); console.log(` ✅ ${emp.name} created`); - } else { - console.log(` â„šī¸ ${emp.name} already exists`); } + } else { + console.log(` â„šī¸ ${emp.name} already exists`); } } - // 7. Seed Contractor Rates + // 7. Seed Contractor Rates for all contractors 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 }[]>( "SELECT id FROM contractor_rates WHERE contractor_id = ?", - [contractor1[0].id] + [contractorId] ); if (existingRate.length === 0) { - const today = new Date().toISOString().split("T")[0]; - await db.execute( - "INSERT INTO contractor_rates (contractor_id, rate, effective_date) VALUES (?, ?, ?)", - [contractor1[0].id, 500.00, today] - ); - console.log(" ✅ Contractor rates created"); - } else { - console.log(" â„šī¸ Contractor rates already exist"); + // Find the contractor's department + const contractor = contractors.find(c => c.username === username); + if (contractor) { + // 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(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -289,17 +702,18 @@ async function seedDatabase() { Username: admin Password: admin123 - Supervisor (Groundnut): - Username: supervisor_groundnut - Password: supervisor123 + Supervisors (password: supervisor123): + - Tudki: rajesh.sharma.tudki + - Dana: sunil.verma.dana + - Groundnut: mahesh.agarwal.groundnut - Contractor: - Username: contractor1 - Password: contractor123 + Contractors (password: contractor123): + - Groundnut: ramesh.patel.gn, kishan.meena.gn + - Dana: gopal.sharma.dana, mohan.yadav.dana + - Tudki: suresh.kumar.tudki, dinesh.gupta.tudki - Employee: - Username: employee1 - Password: employee123 + Employees (password: employee123): + - Use any employee username like ravi.singh.gn1, rampal.verma.dn1, ganesh.kumar.tk1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `); diff --git a/backend-deno/types/index.ts b/backend-deno/types/index.ts index 1fcc242..244e934 100644 --- a/backend-deno/types/index.ts +++ b/backend-deno/types/index.ts @@ -242,3 +242,25 @@ export interface CreateContractorRateRequest { rate: number; 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; +} diff --git a/backend/database/init-schema.sql b/backend/database/init-schema.sql index 51e3a19..c971e50 100644 --- a/backend/database/init-schema.sql +++ b/backend/database/init-schema.sql @@ -13,12 +13,22 @@ CREATE TABLE IF NOT EXISTS sub_departments ( id INT AUTO_INCREMENT PRIMARY KEY, department_id INT NOT NULL, name VARCHAR(100) NOT NULL, - primary_activity VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE, 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 TABLE IF NOT EXISTS users ( 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 ); +-- 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 INDEX idx_users_role ON users(role); 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_contractor_rates_contractor ON contractor_rates(contractor_id); 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); diff --git a/backend/database/migrations/add_standard_rates.sql b/backend/database/migrations/add_standard_rates.sql new file mode 100644 index 0000000..0aad916 --- /dev/null +++ b/backend/database/migrations/add_standard_rates.sql @@ -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; diff --git a/docker-compose.yml b/docker-compose.yml index e1b8a39..0bf66c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: MYSQL_ROOT_PASSWORD: admin123 MYSQL_DATABASE: work_allocation ports: - - "3306:3306" + - "3307:3306" volumes: - mysql_data:/var/lib/mysql - ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro diff --git a/src/App.tsx b/src/App.tsx index f81ffb9..96fec0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,8 +9,11 @@ import { AttendancePage } from './pages/AttendancePage'; import { RatesPage } from './pages/RatesPage'; import { EmployeeSwapPage } from './pages/EmployeeSwapPage'; 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 [activePage, setActivePage] = useState('dashboard'); @@ -30,6 +33,12 @@ const AppContent: React.FC = () => { return ; case 'swaps': return ; + case 'reports': + return ; + case 'standard-rates': + return ; + case 'all-rates': + return ; default: return ; } diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index c9be3f6..4f5c5e9 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,5 +1,5 @@ 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'; interface SidebarItemProps { @@ -112,6 +112,36 @@ export const Sidebar: React.FC = ({ activePage, onNavigate }) => { onClick={() => onNavigate('swaps')} /> )} + + {/* Reports - SuperAdmin and Supervisor */} + {canManageRates && ( + onNavigate('reports')} + /> + )} + + {/* Standard Rates - SuperAdmin and Supervisor */} + {canManageRates && ( + onNavigate('standard-rates')} + /> + )} + + {/* All Rates View - SuperAdmin only */} + {isSuperAdmin && ( + onNavigate('all-rates')} + /> + )} {/* Role indicator at bottom */} diff --git a/src/hooks/useDepartments.ts b/src/hooks/useDepartments.ts index 65675df..eb4648e 100644 --- a/src/hooks/useDepartments.ts +++ b/src/hooks/useDepartments.ts @@ -65,4 +65,4 @@ export const useSubDepartments = (departmentId?: string) => { error, refresh: fetchSubDepartments, }; -}; +}; \ No newline at end of file diff --git a/src/pages/AllRatesPage.tsx b/src/pages/AllRatesPage.tsx new file mode 100644 index 0000000..60c7977 --- /dev/null +++ b/src/pages/AllRatesPage.tsx @@ -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([]); + 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) => { + 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 ( +
+ + +
+ +

Access Restricted

+

+ This page is only accessible to Super Admin accounts. +

+
+
+
+
+ ); + } + + return ( +
+ +
+
+
+ +
+

All Rates Overview

+

View all contractor and standard rates across all departments

+
+
+
+
+ + + {/* Filters */} +
+
+ +

Filters

+
+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ + {/* Summary Cards */} + {summary && ( +
+
+
Total Rates
+
{summary.totalRates}
+
+
+
Contractor Rates
+
{summary.totalContractorRates}
+
+
+
Standard Rates
+
{summary.totalStandardRates}
+
+
+ )} + + {/* Search and Refresh */} +
+
+ + 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" + /> +
+ +
+ + {/* Error */} + {error && ( +
+ Error: {error} +
+ )} + + {/* Table */} + {loading ? ( +
Loading all rates...
+ ) : filteredRates.length > 0 ? ( +
+ + + Type + Contractor + Department + Sub-Department + Activity + Rate (₹) + Effective Date + Created By + + + {filteredRates.map((rate, idx) => ( + + + + {rate.rate_type === 'contractor' ? 'Contractor' : 'Standard'} + + + + {rate.contractor_name || '-'} + + {rate.department_name || '-'} + {rate.sub_department_name || '-'} + + + {rate.activity || 'Standard'} + + + + ₹{rate.rate} + + +
+ + {new Date(rate.effective_date).toLocaleDateString()} +
+
+ + {rate.created_by_name || '-'} + +
+ ))} +
+
+
+ ) : ( +
+ No rates found. Adjust your filters or check back later. +
+ )} +
+
+
+ ); +}; diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 536462c..04790b7 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -1,5 +1,5 @@ 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 { Card, CardHeader, CardContent } from '../components/ui/Card'; import { useEmployees } from '../hooks/useEmployees'; @@ -43,15 +43,25 @@ interface HierarchyNode { } export const DashboardPage: React.FC = () => { - const { employees, loading: employeesLoading } = useEmployees(); + const { employees, loading: employeesLoading, refresh: refreshEmployees } = useEmployees(); const { departments, loading: deptLoading } = useDepartments(); - const { allocations, loading: allocLoading } = useWorkAllocations(); + const { allocations, loading: allocLoading, refresh: refreshAllocations } = useWorkAllocations(); const { user } = useAuth(); const [attendance, setAttendance] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [contractorRates, setContractorRates] = useState>({}); + // 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 isSupervisor = user?.role === 'Supervisor'; const isContractor = user?.role === 'Contractor'; @@ -161,41 +171,78 @@ export const DashboardPage: React.FC = () => { e => e.role === 'Contractor' && e.department_id === supervisor.department_id ); + // Get employees without a contractor but in this department (e.g., swapped employees) + const unassignedEmployees = employees.filter( + e => e.role === 'Employee' && + e.department_id === supervisor.department_id && + !e.contractor_id + ); + + const contractorNodes = deptContractors.map(contractor => { + const contractorEmployees = employees.filter( + e => e.role === 'Employee' && e.contractor_id === contractor.id + ); + + return { + id: contractor.id, + name: contractor.name, + role: 'Contractor', + department: contractor.department_name || '', + children: contractorEmployees.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 || '-', + 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: [], + }; + }), + }; + }); + + // 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: deptContractors.map(contractor => { - const contractorEmployees = employees.filter( - e => e.role === 'Employee' && e.contractor_id === contractor.id - ); - - return { - id: contractor.id, - name: contractor.name, - role: 'Contractor', - department: contractor.department_name || '', - children: contractorEmployees.map(emp => { - const empAttendance = attendance.find(a => a.employee_id === emp.id); - const empAllocation = allocations.find(a => a.employee_id === emp.id); - - return { - id: emp.id, - name: emp.name, - role: 'Employee', - department: emp.department_name || '', - subDepartment: emp.sub_department_name, - activity: empAllocation?.description || 'Loading', - 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: [], - }; - }), - }; - }), + children: contractorNodes, }; return supervisorNode; @@ -211,27 +258,34 @@ export const DashboardPage: React.FC = () => { 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( e => e.role === 'Employee' && e.contractor_id === contractor.id ); - const contractorNode: HierarchyNode = { + return { id: contractor.id, name: contractor.name, role: 'Contractor', department: contractor.department_name || '', children: contractorEmployees.map(emp => { 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 { id: emp.id, name: emp.name, role: 'Employee', department: emp.department_name || '', - subDepartment: emp.sub_department_name, - activity: empAllocation?.description || '-', + subDepartment: empAllocation?.sub_department_name || '-', + activity: empAllocation?.description || empAllocation?.activity || '-', 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), @@ -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 const departmentPresenceData = useMemo(() => { @@ -405,10 +487,19 @@ export const DashboardPage: React.FC = () => { {/* Daily Attendance Report Header */}

Daily Attendance Report

- +
+ + +
{/* Search Bar */} @@ -647,10 +738,19 @@ export const DashboardPage: React.FC = () => {

{departmentName} Dashboard

Daily Attendance & Work Overview

- +
+ + +
{/* Search Bar */} diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 5dc46ce..0d32f71 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -202,16 +202,11 @@ export const LoginPage: React.FC = () => {
- Work Allocation System +
{/* Footer Info */} -
-

- Secure login powered by JWT authentication -

-
{/* Version badge */} diff --git a/src/pages/ReportingPage.tsx b/src/pages/ReportingPage.tsx new file mode 100644 index 0000000..0812b11 --- /dev/null +++ b/src/pages/ReportingPage.tsx @@ -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([]); + const [summary, setSummary] = useState<{ totalAllocations: number; totalAmount: string; totalUnits: string } | null>(null); + const [contractors, setContractors] = useState([]); + 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) => { + const { name, value } = e.target; + setFilters(prev => ({ ...prev, [name]: value })); + }; + + const applyFilters = () => { + fetchReport(); + }; + + const clearFilters = () => { + setFilters({ + startDate: '', + endDate: '', + departmentId: '', + contractorId: '', + }); + setTimeout(fetchReport, 0); + }; + + return ( +
+ +
+
+
+ +

Work Allocation Reports

+
+ +
+
+ + + {/* Filters */} +
+
+ +

Filters

+
+
+
+ + +
+
+ + +
+ {isSuperAdmin && ( + ({ value: String(c.id), label: c.name })) + ]} + /> +
+
+ + +
+
+ + {/* Summary Cards */} + {summary && ( +
+
+
Total Completed
+
{summary.totalAllocations}
+
+
+
Total Amount
+
₹{parseFloat(summary.totalAmount).toLocaleString()}
+
+
+
Total Units
+
{parseFloat(summary.totalUnits).toLocaleString()}
+
+
+ )} + + {/* Search and Refresh */} +
+
+ + 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" + /> +
+ +
+ + {/* Error */} + {error && ( +
+ Error: {error} +
+ )} + + {/* Table */} + {loading ? ( +
Loading report data...
+ ) : filteredAllocations.length > 0 ? ( +
+ + + ID + Employee + Contractor + Department + Sub-Department + Activity + Assigned + Completed + Rate (₹) + Units + Total (₹) + + + {filteredAllocations.map((allocation) => { + const rate = parseFloat(allocation.rate) || 0; + const units = parseFloat(allocation.units) || 0; + const total = parseFloat(allocation.total_amount) || rate; + + return ( + + {allocation.id} + {allocation.employee_name || '-'} + {allocation.contractor_name || '-'} + {allocation.department_name || '-'} + {allocation.sub_department_name || '-'} + + + {allocation.activity || 'Standard'} + + + {new Date(allocation.assigned_date).toLocaleDateString()} + + {allocation.completion_date + ? new Date(allocation.completion_date).toLocaleDateString() + : '-'} + + ₹{rate.toFixed(2)} + {units > 0 ? units : '-'} + ₹{total.toFixed(2)} + + ); + })} + +
+
+ ) : ( +
+ No completed work allocations found. Adjust your filters or check back later. +
+ )} +
+
+
+ ); +}; diff --git a/src/pages/StandardRatesPage.tsx b/src/pages/StandardRatesPage.tsx new file mode 100644 index 0000000..4a68b8e --- /dev/null +++ b/src/pages/StandardRatesPage.tsx @@ -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([]); + const [contractors, setContractors] = useState([]); + const [comparisons, setComparisons] = useState([]); + 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(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) => { + 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 ( +
+ +
+
+ + {canManageRates && ( + + )} + +
+
+ + + {activeTab === 'list' && ( +
+
+
+ + 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" + /> +
+ +
+ +
+

+ Standard Rates are default rates set by supervisors for sub-departments and activities. + These are used as benchmarks to compare against contractor-specific rates. +

+
+ + {error && ( +
+ Error: {error} +
+ )} + + {loading ? ( +
Loading standard rates...
+ ) : filteredRates.length > 0 ? ( + + + Department + Sub-Department + Activity + Rate (₹) + Effective Date + Created By + {canManageRates && Actions} + + + {filteredRates.map((rate) => ( + + {rate.department_name || '-'} + {rate.sub_department_name || 'All'} + + + {rate.activity || 'Standard'} + + + + ₹{rate.rate} + + {new Date(rate.effective_date).toLocaleDateString()} + {rate.created_by_name || '-'} + {canManageRates && ( + +
+ + +
+
+ )} +
+ ))} +
+
+ ) : ( +
+ No standard rates configured yet. Add one to get started! +
+ )} +
+ )} + + {activeTab === 'add' && canManageRates && ( +
+

+ {editingId ? 'Edit Standard Rate' : 'Add New Standard Rate'} +

+ +
+

About Standard Rates

+

+ Standard rates serve as default benchmarks for sub-departments and activities. + Contractor rates can be compared against these to identify deviations. +

+
+ + {formError && ( +
+ {formError} +
+ )} + +
+ {isSupervisor ? ( + d.id === user?.department_id)?.name || 'Loading...'} + disabled + /> + ) : ( + ({ value: String(s.id), label: s.name })) + ]} + /> + + +
+ +
+ + +
+
+ )} + + {activeTab === 'compare' && ( +
+
+

+ + Compare Contractor Rates vs Standard Rates +

+ +
+
+