diff --git a/backend-deno/main.ts b/backend-deno/main.ts index 4dc668f..cf71338 100644 --- a/backend-deno/main.ts +++ b/backend-deno/main.ts @@ -10,6 +10,7 @@ import departmentRoutes from "./routes/departments.ts"; 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"; // Initialize database connection await db.connect(); @@ -61,6 +62,7 @@ router.use("/api/departments", departmentRoutes.routes(), departmentRoutes.allow router.use("/api/work-allocations", workAllocationRoutes.routes(), workAllocationRoutes.allowedMethods()); 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()); // Apply routes app.use(router.routes()); diff --git a/backend-deno/routes/attendance.ts b/backend-deno/routes/attendance.ts index 3783ecb..3b95723 100644 --- a/backend-deno/routes/attendance.ts +++ b/backend-deno/routes/attendance.ts @@ -1,7 +1,7 @@ import { Router } from "@oak/oak"; import { db } from "../config/database.ts"; import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; -import type { Attendance, CheckInOutRequest, User } from "../types/index.ts"; +import type { Attendance, CheckInOutRequest, User, UpdateAttendanceStatusRequest, AttendanceStatus } from "../types/index.ts"; const router = new Router(); @@ -237,6 +237,136 @@ router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin } }); +// Update attendance status (mark as Absent, HalfDay, Late) +router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { + try { + const attendanceId = ctx.params.id; + const body = await ctx.request.body.json() as UpdateAttendanceStatusRequest; + const { status, remark } = body; + + // Validate status + const validStatuses: AttendanceStatus[] = ["CheckedIn", "CheckedOut", "Absent", "HalfDay", "Late"]; + if (!validStatuses.includes(status)) { + ctx.response.status = 400; + ctx.response.body = { error: "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late" }; + return; + } + + // Check if record exists + const existing = await db.query( + "SELECT * FROM attendance WHERE id = ?", + [attendanceId] + ); + + if (existing.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Attendance record not found" }; + return; + } + + // Update the status + await db.execute( + "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", + [status, remark || null, attendanceId] + ); + + const updatedRecord = await db.query( + `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [attendanceId] + ); + + ctx.response.body = updatedRecord[0]; + } catch (error) { + console.error("Update attendance status error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// Mark employee as absent (create absent record) +router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json(); + const { employeeId, workDate, remark } = body; + + if (!employeeId || !workDate) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee ID and work date required" }; + return; + } + + // Check if record already exists for this date + const existing = await db.query( + "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?", + [employeeId, workDate] + ); + + if (existing.length > 0) { + // Update existing record to Absent + await db.execute( + "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", + ["Absent", remark || "Marked absent", existing[0].id] + ); + + const updatedRecord = await db.query( + `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [existing[0].id] + ); + + ctx.response.body = updatedRecord[0]; + } else { + // Create new absent record + const result = await db.execute( + "INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)", + [employeeId, currentUser.id, workDate, "Absent", remark || "Marked absent"] + ); + + const newRecord = await db.query( + `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [result.insertId] + ); + + ctx.response.status = 201; + ctx.response.body = newRecord[0]; + } + } catch (error) { + console.error("Mark absent error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + // Get attendance summary router.get("/summary/stats", authenticateToken, async (ctx) => { try { diff --git a/backend-deno/routes/auth.ts b/backend-deno/routes/auth.ts index 676d022..1089d23 100644 --- a/backend-deno/routes/auth.ts +++ b/backend-deno/routes/auth.ts @@ -1,7 +1,13 @@ import { Router } from "@oak/oak"; -import { hash, compare } from "bcrypt"; +import { hash, compare, genSalt } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; + +// Helper function to hash password with proper salt generation +async function hashPassword(password: string): Promise { + const salt = await genSalt(config.BCRYPT_ROUNDS); + return await hash(password, salt); +} import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts"; import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts"; import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.ts"; @@ -144,7 +150,7 @@ router.post("/change-password", authenticateToken, async (ctx) => { } // Hash new password with configured rounds - const hashedPassword = await hash(newPassword, config.BCRYPT_ROUNDS); + const hashedPassword = await hashPassword(newPassword); // Update password await db.execute( diff --git a/backend-deno/routes/employee-swaps.ts b/backend-deno/routes/employee-swaps.ts new file mode 100644 index 0000000..b4e4e9c --- /dev/null +++ b/backend-deno/routes/employee-swaps.ts @@ -0,0 +1,340 @@ +import { Router } from "@oak/oak"; +import { db } from "../config/database.ts"; +import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; +import type { EmployeeSwap, CreateSwapRequest, User } from "../types/index.ts"; + +const router = new Router(); + +// Get all employee swaps (SuperAdmin only) +router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { + try { + const params = ctx.request.url.searchParams; + const status = params.get("status"); + const employeeId = params.get("employeeId"); + const startDate = params.get("startDate"); + const endDate = params.get("endDate"); + + let query = ` + SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE 1=1 + `; + const queryParams: unknown[] = []; + + if (status) { + query += " AND es.status = ?"; + queryParams.push(status); + } + + if (employeeId) { + query += " AND es.employee_id = ?"; + queryParams.push(employeeId); + } + + if (startDate) { + query += " AND es.swap_date >= ?"; + queryParams.push(startDate); + } + + if (endDate) { + query += " AND es.swap_date <= ?"; + queryParams.push(endDate); + } + + query += " ORDER BY es.created_at DESC"; + + const swaps = await db.query(query, queryParams); + ctx.response.body = swaps; + } catch (error) { + console.error("Get employee swaps error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// Get swap by ID +router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => { + try { + const swapId = ctx.params.id; + + const swaps = await db.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [swapId] + ); + + if (swaps.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Swap record not found" }; + return; + } + + ctx.response.body = swaps[0]; + } catch (error) { + console.error("Get swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// Create new employee swap (SuperAdmin only) +router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as CreateSwapRequest; + const { + employeeId, + targetDepartmentId, + targetContractorId, + swapReason, + reasonDetails, + workCompletionPercentage, + swapDate + } = body; + + // Validate required fields + if (!employeeId || !targetDepartmentId || !swapReason || !swapDate) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee ID, target department, swap reason, and swap date are required" }; + return; + } + + // Validate swap reason + const validReasons = ["LeftWork", "Sick", "FinishedEarly", "Other"]; + if (!validReasons.includes(swapReason)) { + ctx.response.status = 400; + ctx.response.body = { error: "Invalid swap reason" }; + return; + } + + // Get employee's current department and contractor + const employees = await db.query( + "SELECT * FROM users WHERE id = ? AND role = 'Employee'", + [employeeId] + ); + + if (employees.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Employee not found" }; + return; + } + + const employee = employees[0]; + + if (!employee.department_id) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee has no current department" }; + return; + } + + // Check if there's already an active swap for this employee + const activeSwaps = await db.query( + "SELECT * FROM employee_swaps WHERE employee_id = ? AND status = 'Active'", + [employeeId] + ); + + if (activeSwaps.length > 0) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee already has an active swap. Complete or cancel it first." }; + return; + } + + // Create the swap record + const result = await db.execute( + `INSERT INTO employee_swaps + (employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id, + swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`, + [ + employeeId, + employee.department_id, + targetDepartmentId, + employee.contractor_id || null, + targetContractorId || null, + swapReason, + reasonDetails || null, + workCompletionPercentage || 0, + swapDate, + currentUser.id + ] + ); + + // Update the employee's department and contractor + await db.execute( + "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", + [targetDepartmentId, targetContractorId || null, employeeId] + ); + + // Fetch the created swap + const newSwap = await db.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [result.insertId] + ); + + ctx.response.status = 201; + ctx.response.body = newSwap[0]; + } catch (error) { + console.error("Create swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// Complete a swap (return employee to original department) +router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (ctx) => { + try { + const swapId = ctx.params.id; + + // Get the swap record + const swaps = await db.query( + "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", + [swapId] + ); + + if (swaps.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Active swap not found" }; + return; + } + + const swap = swaps[0]; + + // Return employee to original department and contractor + await db.execute( + "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", + [swap.original_department_id, swap.original_contractor_id, swap.employee_id] + ); + + // Mark swap as completed + await db.execute( + "UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?", + [swapId] + ); + + // Fetch updated swap + const updatedSwap = await db.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [swapId] + ); + + ctx.response.body = updatedSwap[0]; + } catch (error) { + console.error("Complete swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// Cancel a swap (return employee to original department) +router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx) => { + try { + const swapId = ctx.params.id; + + // Get the swap record + const swaps = await db.query( + "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", + [swapId] + ); + + if (swaps.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Active swap not found" }; + return; + } + + const swap = swaps[0]; + + // Return employee to original department and contractor + await db.execute( + "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", + [swap.original_department_id, swap.original_contractor_id, swap.employee_id] + ); + + // Mark swap as cancelled + await db.execute( + "UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?", + [swapId] + ); + + // Fetch updated swap + const updatedSwap = await db.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [swapId] + ); + + ctx.response.body = updatedSwap[0]; + } catch (error) { + console.error("Cancel swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +export default router; diff --git a/backend-deno/routes/users.ts b/backend-deno/routes/users.ts index bf19f1f..dc8abd8 100644 --- a/backend-deno/routes/users.ts +++ b/backend-deno/routes/users.ts @@ -1,7 +1,13 @@ import { Router } from "@oak/oak"; -import { hash } from "bcrypt"; +import { hash, genSalt } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; + +// Helper function to hash password with proper salt generation +async function hashPassword(password: string): Promise { + const salt = await genSalt(config.BCRYPT_ROUNDS); + return await hash(password, salt); +} import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import { sanitizeInput, isValidEmail } from "../middleware/security.ts"; import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts"; @@ -19,6 +25,9 @@ router.get("/", authenticateToken, async (ctx) => { let query = ` SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, + u.phone_number, u.aadhar_number, u.bank_account_number, + u.bank_name, u.bank_ifsc, + u.contractor_agreement_number, u.pf_number, u.esic_number, d.name as department_name, c.name as contractor_name FROM users u @@ -64,6 +73,9 @@ router.get("/:id", authenticateToken, async (ctx) => { const users = await db.query( `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, + u.phone_number, u.aadhar_number, u.bank_account_number, + u.bank_name, u.bank_ifsc, + u.contractor_agreement_number, u.pf_number, u.esic_number, d.name as department_name, c.name as contractor_name FROM users u @@ -99,7 +111,11 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async try { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json() as CreateUserRequest; - const { username, name, email, password, role, departmentId, contractorId } = body; + const { + username, name, email, password, role, departmentId, contractorId, + phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc, + contractorAgreementNumber, pfNumber, esicNumber + } = body; // Input validation if (!username || !name || !email || !password || !role) { @@ -135,16 +151,28 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async } // Hash password - const hashedPassword = await hash(password, config.BCRYPT_ROUNDS); + const hashedPassword = await hashPassword(password); const result = await db.execute( - "INSERT INTO users (username, name, email, password, role, department_id, contractor_id) VALUES (?, ?, ?, ?, ?, ?, ?)", - [sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, departmentId || null, contractorId || null] + `INSERT INTO users (username, name, email, password, role, department_id, contractor_id, + phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc, + contractor_agreement_number, pf_number, esic_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, + departmentId || null, contractorId || null, + phoneNumber || null, aadharNumber || null, bankAccountNumber || null, + bankName || null, bankIfsc || null, + contractorAgreementNumber || null, pfNumber || null, esicNumber || null + ] ); const newUser = await db.query( `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, + u.phone_number, u.aadhar_number, u.bank_account_number, + u.bank_name, u.bank_ifsc, + u.contractor_agreement_number, u.pf_number, u.esic_number, d.name as department_name, c.name as contractor_name FROM users u @@ -175,7 +203,11 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy const currentUser = getCurrentUser(ctx); const userId = ctx.params.id; const body = await ctx.request.body.json() as UpdateUserRequest; - const { name, email, role, departmentId, contractorId, isActive } = body; + const { + name, email, role, departmentId, contractorId, isActive, + phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc, + contractorAgreementNumber, pfNumber, esicNumber + } = body; // Check if user exists const existingUsers = await db.query( @@ -235,6 +267,39 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy updates.push("is_active = ?"); params.push(isActive); } + // New fields + if (phoneNumber !== undefined) { + updates.push("phone_number = ?"); + params.push(phoneNumber); + } + if (aadharNumber !== undefined) { + updates.push("aadhar_number = ?"); + params.push(aadharNumber); + } + if (bankAccountNumber !== undefined) { + updates.push("bank_account_number = ?"); + params.push(bankAccountNumber); + } + if (bankName !== undefined) { + updates.push("bank_name = ?"); + params.push(bankName); + } + if (bankIfsc !== undefined) { + updates.push("bank_ifsc = ?"); + params.push(bankIfsc); + } + if (contractorAgreementNumber !== undefined) { + updates.push("contractor_agreement_number = ?"); + params.push(contractorAgreementNumber); + } + if (pfNumber !== undefined) { + updates.push("pf_number = ?"); + params.push(pfNumber); + } + if (esicNumber !== undefined) { + updates.push("esic_number = ?"); + params.push(esicNumber); + } if (updates.length === 0) { ctx.response.status = 400; @@ -252,6 +317,9 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy const updatedUser = await db.query( `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, + u.phone_number, u.aadhar_number, u.bank_account_number, + u.bank_name, u.bank_ifsc, + u.contractor_agreement_number, u.pf_number, u.esic_number, d.name as department_name, c.name as contractor_name FROM users u diff --git a/backend-deno/scripts/seed.ts b/backend-deno/scripts/seed.ts index 292ad9f..2ddae7d 100644 --- a/backend-deno/scripts/seed.ts +++ b/backend-deno/scripts/seed.ts @@ -1,7 +1,13 @@ -import { hash } from "bcrypt"; +import { hash, genSalt } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; +// Helper function to hash password with proper salt generation +async function hashPassword(password: string): Promise { + const salt = await genSalt(config.BCRYPT_ROUNDS); + return await hash(password, salt); +} + async function seedDatabase() { try { console.log("🔌 Connecting to database..."); @@ -82,7 +88,7 @@ async function seedDatabase() { ["admin"] ); - const adminPassword = await hash("admin123", config.BCRYPT_ROUNDS); + const adminPassword = await hashPassword("admin123"); if (existingAdmin.length > 0) { await db.execute( @@ -109,7 +115,7 @@ async function seedDatabase() { ["Dana"] ); - const supervisorPassword = await hash("supervisor123", config.BCRYPT_ROUNDS); + const supervisorPassword = await hashPassword("supervisor123"); const supervisors = [ { username: "supervisor_tudki", name: "Tudki Supervisor", email: "supervisor.tudki@workallocate.com", deptId: tudkiDept[0]?.id }, @@ -137,11 +143,37 @@ async function seedDatabase() { // 5. Seed Sample Contractors console.log("đŸ—ī¸ Seeding sample contractors..."); - const contractorPassword = await hash("contractor123", config.BCRYPT_ROUNDS); + const contractorPassword = await hashPassword("contractor123"); const contractors = [ - { username: "contractor1", name: "Contractor One", email: "contractor1@workallocate.com", deptId: groundnutId }, - { username: "contractor2", name: "Contractor Two", email: "contractor2@workallocate.com", deptId: groundnutId } + { + username: "contractor1", + name: "Contractor One", + email: "contractor1@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", + bankName: "HDFC Bank", + bankIfsc: "HDFC0001234", + agreementNo: "AGR-2024-002", + pfNo: "PF/GJ/12345/67891", + esicNo: "12-34-567890-123-0002" + } ]; for (const con of contractors) { @@ -151,8 +183,13 @@ async function seedDatabase() { ); if (existing.length === 0) { await db.execute( - "INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)", - [con.username, con.name, con.email, contractorPassword, "Contractor", con.deptId, true] + `INSERT INTO users (username, name, email, password, role, department_id, is_active, + phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc, + contractor_agreement_number, pf_number, esic_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [con.username, con.name, con.email, contractorPassword, "Contractor", con.deptId, true, + con.phone, con.aadhar, con.bankAccount, con.bankName, con.bankIfsc, + con.agreementNo, con.pfNo, con.esicNo] ); console.log(` ✅ ${con.name} created`); } else { @@ -166,13 +203,40 @@ async function seedDatabase() { "SELECT id FROM users WHERE username = ?", ["contractor1"] ); - const employeePassword = await hash("employee123", config.BCRYPT_ROUNDS); + const employeePassword = await hashPassword("employee123"); if (contractor1.length > 0) { const employees = [ - { username: "employee1", name: "Employee One", email: "employee1@workallocate.com" }, - { username: "employee2", name: "Employee Two", email: "employee2@workallocate.com" }, - { username: "employee3", name: "Employee Three", email: "employee3@workallocate.com" } + { + 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" + } ]; for (const emp of employees) { @@ -182,8 +246,11 @@ async function seedDatabase() { ); if (existing.length === 0) { await db.execute( - "INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [emp.username, emp.name, emp.email, employeePassword, "Employee", groundnutId, contractor1[0].id, true] + `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.phone, emp.aadhar, emp.bankAccount, emp.bankName, emp.bankIfsc] ); console.log(` ✅ ${emp.name} created`); } else { diff --git a/backend-deno/types/index.ts b/backend-deno/types/index.ts index 3d0a050..1fcc242 100644 --- a/backend-deno/types/index.ts +++ b/backend-deno/types/index.ts @@ -14,6 +14,16 @@ export interface User { created_at: Date; department_name?: string; contractor_name?: string; + // Common fields for Employee and Contractor + phone_number?: string | null; + aadhar_number?: string | null; + bank_account_number?: string | null; + bank_name?: string | null; + bank_ifsc?: string | null; + // Contractor-specific fields + contractor_agreement_number?: string | null; + pf_number?: string | null; + esic_number?: string | null; } export interface JWTPayload { @@ -66,7 +76,7 @@ export interface WorkAllocation { } // Attendance types -export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent"; +export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent" | "HalfDay" | "Late"; export interface Attendance { id: number; @@ -76,6 +86,7 @@ export interface Attendance { check_out_time: Date | null; work_date: Date; status: AttendanceStatus; + remark?: string | null; created_at: Date; employee_name?: string; supervisor_name?: string; @@ -83,6 +94,49 @@ export interface Attendance { contractor_name?: string; } +// Employee swap types +export type SwapReason = "LeftWork" | "Sick" | "FinishedEarly" | "Other"; +export type SwapStatus = "Active" | "Completed" | "Cancelled"; + +export interface EmployeeSwap { + id: number; + employee_id: number; + original_department_id: number; + target_department_id: number; + original_contractor_id: number | null; + target_contractor_id: number | null; + swap_reason: SwapReason; + reason_details: string | null; + work_completion_percentage: number; + swap_date: Date; + swapped_by: number; + status: SwapStatus; + created_at: Date; + completed_at: Date | null; + // Joined fields + employee_name?: string; + original_department_name?: string; + target_department_name?: string; + original_contractor_name?: string; + target_contractor_name?: string; + swapped_by_name?: string; +} + +export interface CreateSwapRequest { + employeeId: number; + targetDepartmentId: number; + targetContractorId?: number; + swapReason: SwapReason; + reasonDetails?: string; + workCompletionPercentage?: number; + swapDate: string; +} + +export interface UpdateAttendanceStatusRequest { + status: AttendanceStatus; + remark?: string; +} + // Contractor rate types export interface ContractorRate { id: number; @@ -127,6 +181,16 @@ export interface CreateUserRequest { role: UserRole; departmentId?: number | null; contractorId?: number | null; + // Common fields for Employee and Contractor + phoneNumber?: string | null; + aadharNumber?: string | null; + bankAccountNumber?: string | null; + bankName?: string | null; + bankIfsc?: string | null; + // Contractor-specific fields + contractorAgreementNumber?: string | null; + pfNumber?: string | null; + esicNumber?: string | null; } export interface UpdateUserRequest { @@ -136,6 +200,16 @@ export interface UpdateUserRequest { departmentId?: number | null; contractorId?: number | null; isActive?: boolean; + // Common fields for Employee and Contractor + phoneNumber?: string | null; + aadharNumber?: string | null; + bankAccountNumber?: string | null; + bankName?: string | null; + bankIfsc?: string | null; + // Contractor-specific fields + contractorAgreementNumber?: string | null; + pfNumber?: string | null; + esicNumber?: string | null; } export interface ChangePasswordRequest { diff --git a/backend/.env b/backend/.env deleted file mode 100644 index ad0da3e..0000000 --- a/backend/.env +++ /dev/null @@ -1,10 +0,0 @@ -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=admin123 -DB_NAME=work_allocation -DB_PORT=3306 - -JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024 -JWT_EXPIRES_IN=7d - -PORT=3000 diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 92d8837..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=your_password -DB_NAME=work_allocation -DB_PORT=3306 - -JWT_SECRET=your_jwt_secret_key_change_this_in_production -JWT_EXPIRES_IN=7d - -PORT=3000 diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 4228753..0000000 --- a/backend/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Work Allocation Backend API - -Simple Node.js/Express backend with MySQL database for the Work Allocation System. - -## Setup - -### 1. Install Dependencies - -```bash -cd backend -npm install -``` - -### 2. Setup MySQL Database - -1. Install MySQL if not already installed -2. Create the database and tables: - -```bash -mysql -u root -p < database/schema.sql -``` - -Or manually: - -- Login to MySQL: `mysql -u root -p` -- Run the SQL commands from `database/schema.sql` - -### 3. Configure Environment - -Copy `.env.example` to `.env` and update with your database credentials: - -```bash -cp .env.example .env -``` - -Edit `.env`: - -```env -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=your_mysql_password -DB_NAME=work_allocation -DB_PORT=3306 - -JWT_SECRET=your_secret_key_here -JWT_EXPIRES_IN=7d - -PORT=3000 -``` - -### 4. Start Server - -Development mode (with auto-reload): - -```bash -npm run dev -``` - -Production mode: - -```bash -npm start -``` - -The server will run on `http://localhost:3000` - -## Default Credentials - -**Super Admin:** - -- Username: `admin` -- Password: `admin123` - -**Note:** Change the default password immediately after first login! - -## API Endpoints - -### Authentication - -- `POST /api/auth/login` - Login -- `GET /api/auth/me` - Get current user -- `POST /api/auth/change-password` - Change password - -### Users - -- `GET /api/users` - Get all users (with filters) -- `GET /api/users/:id` - Get user by ID -- `POST /api/users` - Create user -- `PUT /api/users/:id` - Update user -- `DELETE /api/users/:id` - Delete user - -### Departments - -- `GET /api/departments` - Get all departments -- `GET /api/departments/:id` - Get department by ID -- `GET /api/departments/:id/sub-departments` - Get sub-departments -- `POST /api/departments` - Create department (SuperAdmin only) -- `POST /api/departments/:id/sub-departments` - Create sub-department (SuperAdmin only) - -### Work Allocations - -- `GET /api/work-allocations` - Get all work allocations -- `GET /api/work-allocations/:id` - Get work allocation by ID -- `POST /api/work-allocations` - Create work allocation (Supervisor only) -- `PUT /api/work-allocations/:id/status` - Update status (Supervisor only) -- `DELETE /api/work-allocations/:id` - Delete work allocation (Supervisor only) - -### Attendance - -- `GET /api/attendance` - Get all attendance records -- `GET /api/attendance/:id` - Get attendance by ID -- `POST /api/attendance/check-in` - Check in employee (Supervisor only) -- `POST /api/attendance/check-out` - Check out employee (Supervisor only) -- `GET /api/attendance/summary/stats` - Get attendance summary - -### Contractor Rates - -- `GET /api/contractor-rates` - Get contractor rates -- `GET /api/contractor-rates/contractor/:contractorId/current` - Get current rate -- `POST /api/contractor-rates` - Set contractor rate (Supervisor/SuperAdmin only) - -## Roles & Permissions - -### SuperAdmin - -- Full access to all features -- Can create/manage all users and departments -- Can view all data across departments - -### Supervisor - -- Can manage users (employees, contractors) in their department -- Can create work allocations for their department -- Can check in/out employees -- Can set contractor rates -- Can mark work as completed - -### Contractor - -- Can view work allocations assigned to them -- Can view employees under them - -### Employee - -- Can view their own work allocations -- Can view their attendance records -- Can see contractor rates - -## Database Schema - -### Tables - -- `departments` - Main departments (Tudki, Dana, Groundnut) -- `sub_departments` - Sub-departments (17 for Groundnut) -- `users` - All users (SuperAdmin, Supervisor, Contractor, Employee) -- `contractor_rates` - Contractor rate history -- `work_allocations` - Work assignments -- `attendance` - Check-in/out records - -## Development Notes - -- The server uses ES modules (type: "module" in package.json) -- JWT tokens are used for authentication -- Passwords are hashed using bcryptjs -- All timestamps are in UTC -- The API uses role-based access control (RBAC) diff --git a/backend/config/database.js b/backend/config/database.js deleted file mode 100644 index d6af991..0000000 --- a/backend/config/database.js +++ /dev/null @@ -1,33 +0,0 @@ -import mysql from 'mysql2/promise'; -import dotenv from 'dotenv'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Load .env from backend directory -dotenv.config({ path: join(__dirname, '..', '.env') }); - -const pool = mysql.createPool({ - host: process.env.DB_HOST || 'localhost', - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || 'admin123', - database: process.env.DB_NAME || 'work_allocation', - port: process.env.DB_PORT || 3306, - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0 -}); - -// Test connection -pool.getConnection() - .then(connection => { - console.log('✅ Database connected successfully'); - connection.release(); - }) - .catch(err => { - console.error('❌ Database connection failed:', err.message); - }); - -export default pool; diff --git a/backend/database/database_seed.js b/backend/database/database_seed.js deleted file mode 100644 index 7377a86..0000000 --- a/backend/database/database_seed.js +++ /dev/null @@ -1,240 +0,0 @@ -import bcrypt from 'bcryptjs'; -import mysql from 'mysql2/promise'; -import dotenv from 'dotenv'; - -dotenv.config(); - -async function seedDatabase() { - let connection; - - try { - // Connect to database with retry logic - console.log('🔌 Connecting to database...'); - - let retries = 5; - while (retries > 0) { - try { - connection = await mysql.createConnection({ - host: process.env.DB_HOST || 'localhost', - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || 'admin123', - database: process.env.DB_NAME || 'work_allocation', - port: process.env.DB_PORT || 3306, - connectTimeout: 10000, - enableKeepAlive: true, - keepAliveInitialDelay: 0 - }); - break; - } catch (err) { - retries--; - if (retries === 0) throw err; - console.log(` âŗ Retrying connection... (${5 - retries}/5)`); - await new Promise(resolve => setTimeout(resolve, 2000)); - } - } - - console.log('✅ Connected to database'); - console.log(''); - - // 1. Seed Departments - console.log('📁 Seeding departments...'); - const [existingDepts] = await connection.query('SELECT COUNT(*) as count FROM departments'); - - if (existingDepts[0].count === 0) { - await connection.query(` - INSERT INTO departments (name) VALUES - ('Tudki'), - ('Dana'), - ('Groundnut') - `); - console.log(' ✅ Departments created'); - } else { - console.log(' â„šī¸ Departments already exist'); - } - - // 2. Seed Sub-departments for Groundnut - console.log('📂 Seeding sub-departments...'); - const [groundnutDept] = await connection.query('SELECT id FROM departments WHERE name = ?', ['Groundnut']); - let groundnutId = null; - - if (groundnutDept.length > 0) { - groundnutId = groundnutDept[0].id; - const [existingSubDepts] = await connection.query('SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?', [groundnutId]); - - if (existingSubDepts[0].count === 0) { - await connection.query(` - INSERT INTO sub_departments (department_id, name, primary_activity) VALUES - (?, 'Mufali Aavak Katai', 'Loading/Unloading'), - (?, 'Mufali Aavak Dhang', 'Loading/Unloading'), - (?, 'Dhang Se Katai', 'Loading/Unloading'), - (?, 'Guthli Bori Silai Dhang', 'Loading/Unloading'), - (?, 'Guthali dada Pala Tulai Silai Dhang', 'Loading/Unloading'), - (?, 'Mufali Patthar Bori silai dhang', 'Loading/Unloading'), - (?, 'Mufali Patthar Bori Utrai', 'Loading/Unloading'), - (?, 'Bardana Bandal Loading Unloading', 'Loading/Unloading'), - (?, 'Bardana Gatthi Loading', 'Loading/Unloading'), - (?, 'Black Dana Loading/Unloading', 'Loading/Unloading'), - (?, 'Pre Cleaning', 'Pre Cleaning'), - (?, 'Destoner', 'Destoner'), - (?, 'Water', 'Water'), - (?, 'Decordicater', 'Decordicater & Cleaning'), - (?, 'Round Chalna', 'Round Chalna & Cleaning'), - (?, 'Cleaning', 'Decordicater & Cleaning'), - (?, 'Round Chalna No.1', 'Round Chalna No.1') - `, Array(17).fill(groundnutId)); - console.log(' ✅ Sub-departments created'); - } else { - console.log(' â„šī¸ Sub-departments already exist'); - } - } - - // 3. Seed SuperAdmin - console.log('👤 Seeding SuperAdmin user...'); - const [existingAdmin] = await connection.query('SELECT id FROM users WHERE username = ?', ['admin']); - - const adminPassword = await bcrypt.hash('admin123', 10); - - if (existingAdmin.length > 0) { - await connection.query( - 'UPDATE users SET password = ?, is_active = TRUE WHERE username = ?', - [adminPassword, 'admin'] - ); - console.log(' ✅ SuperAdmin password updated'); - } else { - await connection.query( - 'INSERT INTO users (username, name, email, password, role, is_active) VALUES (?, ?, ?, ?, ?, ?)', - ['admin', 'Super Admin', 'admin@workallocate.com', adminPassword, 'SuperAdmin', true] - ); - console.log(' ✅ SuperAdmin created'); - } - - // 4. Seed Sample Supervisors - console.log('đŸ‘Ĩ Seeding sample supervisors...'); - const [tudkiDept] = await connection.query('SELECT id FROM departments WHERE name = ?', ['Tudki']); - const [danaDept] = await connection.query('SELECT id FROM departments WHERE name = ?', ['Dana']); - - const supervisorPassword = await bcrypt.hash('supervisor123', 10); - - 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 } - ]; - - for (const sup of supervisors) { - if (sup.deptId) { - const [existing] = await connection.query('SELECT id FROM users WHERE username = ?', [sup.username]); - if (existing.length === 0) { - await connection.query( - 'INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)', - [sup.username, sup.name, sup.email, supervisorPassword, 'Supervisor', sup.deptId, true] - ); - console.log(` ✅ ${sup.name} created`); - } else { - console.log(` â„šī¸ ${sup.name} already exists`); - } - } - } - - // 5. Seed Sample Contractors - console.log('đŸ—ī¸ Seeding sample contractors...'); - const contractorPassword = await bcrypt.hash('contractor123', 10); - - const contractors = [ - { username: 'contractor1', name: 'Contractor One', email: 'contractor1@workallocate.com', deptId: groundnutId }, - { username: 'contractor2', name: 'Contractor Two', email: 'contractor2@workallocate.com', deptId: groundnutId } - ]; - - for (const con of contractors) { - const [existing] = await connection.query('SELECT id FROM users WHERE username = ?', [con.username]); - if (existing.length === 0) { - await connection.query( - 'INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)', - [con.username, con.name, con.email, contractorPassword, 'Contractor', con.deptId, true] - ); - console.log(` ✅ ${con.name} created`); - } else { - console.log(` â„šī¸ ${con.name} already exists`); - } - } - - // 6. Seed Sample Employees - console.log('👷 Seeding sample employees...'); - const [contractor1] = await connection.query('SELECT id FROM users WHERE username = ?', ['contractor1']); - const employeePassword = await bcrypt.hash('employee123', 10); - - if (contractor1.length > 0) { - const employees = [ - { username: 'employee1', name: 'Employee One', email: 'employee1@workallocate.com' }, - { username: 'employee2', name: 'Employee Two', email: 'employee2@workallocate.com' }, - { username: 'employee3', name: 'Employee Three', email: 'employee3@workallocate.com' } - ]; - - for (const emp of employees) { - const [existing] = await connection.query('SELECT id FROM users WHERE username = ?', [emp.username]); - if (existing.length === 0) { - await connection.query( - 'INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [emp.username, emp.name, emp.email, employeePassword, 'Employee', groundnutId, contractor1[0].id, true] - ); - console.log(` ✅ ${emp.name} created`); - } else { - console.log(` â„šī¸ ${emp.name} already exists`); - } - } - } - - // 7. Seed Contractor Rates - console.log('💰 Seeding contractor rates...'); - if (contractor1.length > 0) { - const [existingRate] = await connection.query( - 'SELECT id FROM contractor_rates WHERE contractor_id = ?', - [contractor1[0].id] - ); - - if (existingRate.length === 0) { - await connection.query( - 'INSERT INTO contractor_rates (contractor_id, rate, effective_date) VALUES (?, ?, CURDATE())', - [contractor1[0].id, 500.00] - ); - console.log(' ✅ Contractor rates created'); - } else { - console.log(' â„šī¸ Contractor rates already exist'); - } - } - - console.log(''); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('✅ Database seeding completed successfully!'); - console.log(''); - console.log('🔑 Default Login Credentials:'); - console.log(''); - console.log(' SuperAdmin:'); - console.log(' Username: admin'); - console.log(' Password: admin123'); - console.log(''); - console.log(' Supervisor (Groundnut):'); - console.log(' Username: supervisor_groundnut'); - console.log(' Password: supervisor123'); - console.log(''); - console.log(' Contractor:'); - console.log(' Username: contractor1'); - console.log(' Password: contractor123'); - console.log(''); - console.log(' Employee:'); - console.log(' Username: employee1'); - console.log(' Password: employee123'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(''); - - } catch (error) { - console.error('❌ Error seeding database:', error.message); - process.exit(1); - } finally { - if (connection) { - await connection.end(); - } - } -} - -seedDatabase(); diff --git a/backend/database/init-schema.sql b/backend/database/init-schema.sql index 4d650f9..51e3a19 100644 --- a/backend/database/init-schema.sql +++ b/backend/database/init-schema.sql @@ -30,6 +30,16 @@ CREATE TABLE IF NOT EXISTS users ( department_id INT, contractor_id INT, is_active BOOLEAN DEFAULT TRUE, + -- Common fields for Employee and Contractor + phone_number VARCHAR(20), + aadhar_number VARCHAR(12), + bank_account_number VARCHAR(30), + bank_name VARCHAR(100), + bank_ifsc VARCHAR(20), + -- Contractor-specific fields + contractor_agreement_number VARCHAR(50), + pf_number VARCHAR(30), + esic_number VARCHAR(30), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL, FOREIGN KEY (contractor_id) REFERENCES users(id) ON DELETE SET NULL @@ -65,13 +75,38 @@ CREATE TABLE IF NOT EXISTS attendance ( check_in_time DATETIME, check_out_time DATETIME, work_date DATE NOT NULL, - status ENUM('CheckedIn', 'CheckedOut', 'Absent') DEFAULT 'CheckedIn', + status ENUM('CheckedIn', 'CheckedOut', 'Absent', 'HalfDay', 'Late') DEFAULT 'CheckedIn', + remark VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (supervisor_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY unique_attendance (employee_id, work_date) ); +-- Create employee_swaps table for tracking employee department transfers +CREATE TABLE IF NOT EXISTS employee_swaps ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_id INT NOT NULL, + original_department_id INT NOT NULL, + target_department_id INT NOT NULL, + original_contractor_id INT, + target_contractor_id INT, + swap_reason ENUM('LeftWork', 'Sick', 'FinishedEarly', 'Other') NOT NULL, + reason_details VARCHAR(500), + work_completion_percentage INT DEFAULT 0, + swap_date DATE NOT NULL, + swapped_by INT NOT NULL, + status ENUM('Active', 'Completed', 'Cancelled') DEFAULT 'Active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (original_department_id) REFERENCES departments(id) ON DELETE CASCADE, + FOREIGN KEY (target_department_id) REFERENCES departments(id) ON DELETE CASCADE, + FOREIGN KEY (original_contractor_id) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (target_contractor_id) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (swapped_by) REFERENCES users(id) ON DELETE CASCADE +); + -- Create contractor_rates table CREATE TABLE IF NOT EXISTS contractor_rates ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -89,6 +124,8 @@ CREATE TABLE IF NOT EXISTS contractor_rates ( CREATE INDEX idx_users_role ON users(role); CREATE INDEX idx_users_department ON users(department_id); CREATE INDEX idx_users_contractor ON users(contractor_id); +CREATE INDEX idx_users_phone ON users(phone_number); +CREATE INDEX idx_users_aadhar ON users(aadhar_number); CREATE INDEX idx_work_allocations_employee ON work_allocations(employee_id); CREATE INDEX idx_work_allocations_supervisor ON work_allocations(supervisor_id); CREATE INDEX idx_work_allocations_contractor ON work_allocations(contractor_id); diff --git a/backend/database/migrations/add_activity_to_work_allocations.sql b/backend/database/migrations/add_activity_to_work_allocations.sql deleted file mode 100644 index 90e3597..0000000 --- a/backend/database/migrations/add_activity_to_work_allocations.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Migration: Add activity, units, and total_amount columns to work_allocations table --- Run this if you have an existing database without these columns - -ALTER TABLE work_allocations -ADD COLUMN IF NOT EXISTS activity VARCHAR(100) AFTER sub_department_id; - -ALTER TABLE work_allocations -ADD COLUMN IF NOT EXISTS units DECIMAL(10, 2) AFTER rate; - -ALTER TABLE work_allocations -ADD COLUMN IF NOT EXISTS total_amount DECIMAL(10, 2) AFTER units; diff --git a/backend/database/migrations/add_contractor_rates_columns.sql b/backend/database/migrations/add_contractor_rates_columns.sql deleted file mode 100644 index 44ae273..0000000 --- a/backend/database/migrations/add_contractor_rates_columns.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Migration: Add sub_department_id and activity columns to contractor_rates table --- Run this if you have an existing database - --- Add sub_department_id column if it doesn't exist -ALTER TABLE contractor_rates -ADD COLUMN IF NOT EXISTS sub_department_id INT NULL, -ADD COLUMN IF NOT EXISTS activity VARCHAR(200) NULL; - --- Add foreign key constraint for sub_department_id --- Note: This may fail if the constraint already exists -ALTER TABLE contractor_rates -ADD CONSTRAINT fk_contractor_rates_sub_department -FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL; diff --git a/backend/database/migrations/add_user_details.sql b/backend/database/migrations/add_user_details.sql new file mode 100644 index 0000000..e0c2c2f --- /dev/null +++ b/backend/database/migrations/add_user_details.sql @@ -0,0 +1,18 @@ +-- Migration: Add personal and bank details to users table +-- Run this migration on existing databases to add the new fields + +-- Common fields for Employee and Contractor +ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) AFTER is_active; +ALTER TABLE users ADD COLUMN aadhar_number VARCHAR(12) AFTER phone_number; +ALTER TABLE users ADD COLUMN bank_account_number VARCHAR(30) AFTER aadhar_number; +ALTER TABLE users ADD COLUMN bank_name VARCHAR(100) AFTER bank_account_number; +ALTER TABLE users ADD COLUMN bank_ifsc VARCHAR(20) AFTER bank_name; + +-- Contractor-specific fields +ALTER TABLE users ADD COLUMN contractor_agreement_number VARCHAR(50) AFTER bank_ifsc; +ALTER TABLE users ADD COLUMN pf_number VARCHAR(30) AFTER contractor_agreement_number; +ALTER TABLE users ADD COLUMN esic_number VARCHAR(30) AFTER pf_number; + +-- Add indexes for commonly queried fields +CREATE INDEX idx_users_phone ON users(phone_number); +CREATE INDEX idx_users_aadhar ON users(aadhar_number); diff --git a/backend/database/schema.sql b/backend/database/schema.sql deleted file mode 100644 index b8e8d08..0000000 --- a/backend/database/schema.sql +++ /dev/null @@ -1,134 +0,0 @@ --- Work Allocation System Database Schema - --- Create database -CREATE DATABASE IF NOT EXISTS work_allocation; -USE work_allocation; - --- Departments table -CREATE TABLE departments ( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(100) NOT NULL UNIQUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - --- Sub-departments table (for Groundnut department) -CREATE TABLE sub_departments ( - id INT PRIMARY KEY AUTO_INCREMENT, - department_id INT NOT NULL, - name VARCHAR(200) NOT NULL, - primary_activity VARCHAR(200) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE -); - --- Users table (for all roles: SuperAdmin, Supervisor, Contractor, Employee) -CREATE TABLE users ( - id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(100) NOT NULL UNIQUE, - name VARCHAR(200) NOT NULL, - email VARCHAR(200) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - role ENUM('SuperAdmin', 'Supervisor', 'Contractor', 'Employee') NOT NULL, - department_id INT, - contractor_id INT, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL, - FOREIGN KEY (contractor_id) REFERENCES users(id) ON DELETE SET NULL -); - --- Contractor rates table -CREATE TABLE contractor_rates ( - id INT PRIMARY KEY AUTO_INCREMENT, - contractor_id INT NOT NULL, - rate DECIMAL(10, 2) NOT NULL, - effective_date DATE NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (contractor_id) REFERENCES users(id) ON DELETE CASCADE -); - --- Work allocations table -CREATE TABLE work_allocations ( - id INT PRIMARY KEY AUTO_INCREMENT, - employee_id INT NOT NULL, - supervisor_id INT NOT NULL, - contractor_id INT NOT NULL, - sub_department_id INT, - description TEXT, - assigned_date DATE NOT NULL, - status ENUM('Pending', 'InProgress', 'Completed', 'Cancelled') DEFAULT 'Pending', - completion_date DATE, - rate DECIMAL(10, 2), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (supervisor_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (contractor_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL -); - --- Attendance table -CREATE TABLE attendance ( - id INT PRIMARY KEY AUTO_INCREMENT, - employee_id INT NOT NULL, - supervisor_id INT NOT NULL, - check_in_time DATETIME NOT NULL, - check_out_time DATETIME, - work_date DATE NOT NULL, - status ENUM('CheckedIn', 'CheckedOut') DEFAULT 'CheckedIn', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (supervisor_id) REFERENCES users(id) ON DELETE CASCADE -); - --- Insert default departments -INSERT INTO departments (name) VALUES - ('Tudki'), - ('Dana'), - ('Groundnut'); - --- Insert Groundnut sub-departments -INSERT INTO sub_departments (department_id, name, primary_activity) -SELECT id, 'Mufali Aavak Katai', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Mufali Aavak Dhang', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Dhang Se Katai', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Guthli Bori Silai Dhang', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Guthali dada Pala Tulai Silai Dhang', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Mufali Patthar Bori silai dhang', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Mufali Patthar Bori Utrai', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Bardana Bandal Loading Unloading', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Bardana Gatthi Loading', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Black Dana Loading/Unloading', 'Loading/Unloading' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Pre Cleaning', 'Pre Cleaning' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Destoner', 'Destoner' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Water', 'Water' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Decordicater', 'Decordicater & Cleaning' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Round Chalna', 'Round Chalna & Cleaning' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Cleaning', 'Decordicater & Cleaning' FROM departments WHERE name = 'Groundnut' -UNION ALL -SELECT id, 'Round Chalna No.1', 'Round Chalna No.1' FROM departments WHERE name = 'Groundnut'; - --- Insert default SuperAdmin (password: admin123) --- Password is hashed using bcrypt: $2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi -INSERT INTO users (username, name, email, password, role) VALUES - ('admin', 'Super Admin', 'admin@workallocate.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'SuperAdmin'); diff --git a/backend/database/seed-admin.js b/backend/database/seed-admin.js deleted file mode 100644 index d599620..0000000 --- a/backend/database/seed-admin.js +++ /dev/null @@ -1,72 +0,0 @@ -import bcrypt from 'bcryptjs'; -import mysql from 'mysql2/promise'; -import dotenv from 'dotenv'; - -dotenv.config(); - -async function seedAdmin() { - let connection; - - try { - // Connect to database (use root for seeding) - connection = await mysql.createConnection({ - host: process.env.DB_HOST || 'localhost', - user: 'root', - password: 'rootpassword', - database: process.env.DB_NAME || 'work_allocation', - port: process.env.DB_PORT || 3306 - }); - - console.log('✅ Connected to database'); - - // Check if admin already exists - const [existingUsers] = await connection.query( - 'SELECT id FROM users WHERE username = ?', - ['admin'] - ); - - if (existingUsers.length > 0) { - console.log('â„šī¸ Admin user already exists, updating password...'); - - // Generate new password hash - const passwordHash = await bcrypt.hash('admin123', 10); - - // Update existing admin user - await connection.query( - 'UPDATE users SET password = ? WHERE username = ?', - [passwordHash, 'admin'] - ); - - console.log('✅ Admin password updated successfully'); - } else { - console.log('📝 Creating admin user...'); - - // Generate password hash - const passwordHash = await bcrypt.hash('admin123', 10); - - // Insert admin user - await connection.query( - 'INSERT INTO users (username, name, email, password, role) VALUES (?, ?, ?, ?, ?)', - ['admin', 'Super Admin', 'admin@workallocate.com', passwordHash, 'SuperAdmin'] - ); - - console.log('✅ Admin user created successfully'); - } - - console.log(''); - console.log('🔑 Default Login Credentials:'); - console.log(' Username: admin'); - console.log(' Password: admin123'); - console.log(''); - - } catch (error) { - console.error('❌ Error seeding admin user:', error.message); - process.exit(1); - } finally { - if (connection) { - await connection.end(); - } - } -} - -seedAdmin(); diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js deleted file mode 100644 index 6071f0f..0000000 --- a/backend/middleware/auth.js +++ /dev/null @@ -1,32 +0,0 @@ -import jwt from 'jsonwebtoken'; - -export const authenticateToken = (req, res, next) => { - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - return res.status(401).json({ error: 'Access token required' }); - } - - jwt.verify(token, process.env.JWT_SECRET, (err, user) => { - if (err) { - return res.status(403).json({ error: 'Invalid or expired token' }); - } - req.user = user; - next(); - }); -}; - -export const authorize = (...roles) => { - return (req, res, next) => { - if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - if (!roles.includes(req.user.role)) { - return res.status(403).json({ error: 'Insufficient permissions' }); - } - - next(); - }; -}; diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index dedcf48..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,1120 +0,0 @@ -{ - "name": "work-allocation-backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "work-allocation-backend", - "version": "1.0.0", - "dependencies": { - "bcryptjs": "^2.4.3", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "jsonwebtoken": "^9.0.2", - "mysql2": "^3.6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/lru.min": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", - "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.1", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", - "license": "MIT", - "dependencies": { - "lru-cache": "^7.14.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index c7440da..0000000 --- a/backend/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "work-allocation-backend", - "version": "1.0.0", - "description": "Simple backend for Work Allocation System", - "main": "server.js", - "type": "module", - "scripts": { - "start": "node server.js", - "dev": "node --watch server.js", - "seed": "node database/database_seed.js" - }, - "dependencies": { - "express": "^4.18.2", - "mysql2": "^3.6.5", - "bcryptjs": "^2.4.3", - "jsonwebtoken": "^9.0.2", - "cors": "^2.8.5", - "dotenv": "^16.3.1" - } -} diff --git a/backend/routes/attendance.js b/backend/routes/attendance.js deleted file mode 100644 index 27451ca..0000000 --- a/backend/routes/attendance.js +++ /dev/null @@ -1,259 +0,0 @@ -import express from 'express'; -import db from '../config/database.js'; -import { authenticateToken, authorize } from '../middleware/auth.js'; - -const router = express.Router(); - -// Get all attendance records -router.get('/', authenticateToken, async (req, res) => { - try { - const { employeeId, startDate, endDate, status } = req.query; - - let query = ` - SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE 1=1 - `; - const params = []; - - // Role-based filtering - if (req.user.role === 'Supervisor') { - query += ' AND a.supervisor_id = ?'; - params.push(req.user.id); - } else if (req.user.role === 'Employee') { - query += ' AND a.employee_id = ?'; - params.push(req.user.id); - } - - if (employeeId) { - query += ' AND a.employee_id = ?'; - params.push(employeeId); - } - - if (startDate) { - query += ' AND a.work_date >= ?'; - params.push(startDate); - } - - if (endDate) { - query += ' AND a.work_date <= ?'; - params.push(endDate); - } - - if (status) { - query += ' AND a.status = ?'; - params.push(status); - } - - query += ' ORDER BY a.work_date DESC, a.check_in_time DESC'; - - const [records] = await db.query(query, params); - res.json(records); - } catch (error) { - console.error('Get attendance error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get attendance by ID -router.get('/:id', authenticateToken, async (req, res) => { - try { - const [records] = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [req.params.id] - ); - - if (records.length === 0) { - return res.status(404).json({ error: 'Attendance record not found' }); - } - - res.json(records[0]); - } catch (error) { - console.error('Get attendance error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Check in employee (Supervisor or SuperAdmin) -router.post('/check-in', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const { employeeId, workDate } = req.body; - - if (!employeeId || !workDate) { - return res.status(400).json({ error: 'Employee ID and work date required' }); - } - - // Verify employee exists (SuperAdmin can check in any employee, Supervisor only their department) - let employeeQuery = 'SELECT * FROM users WHERE id = ? AND role = ?'; - let employeeParams = [employeeId, 'Employee']; - - if (req.user.role === 'Supervisor') { - employeeQuery += ' AND department_id = ?'; - employeeParams.push(req.user.departmentId); - } - - const [employees] = await db.query(employeeQuery, employeeParams); - - if (employees.length === 0) { - return res.status(403).json({ error: 'Employee not found or not in your department' }); - } - - // Check if already checked in today - const [existing] = await db.query( - 'SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?', - [employeeId, workDate, 'CheckedIn'] - ); - - if (existing.length > 0) { - return res.status(400).json({ error: 'Employee already checked in today' }); - } - - const checkInTime = new Date(); - - const [result] = await db.query( - 'INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)', - [employeeId, req.user.id, checkInTime, workDate, 'CheckedIn'] - ); - - const [newRecord] = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [result.insertId] - ); - - res.status(201).json(newRecord[0]); - } catch (error) { - console.error('Check in error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Check out employee (Supervisor or SuperAdmin) -router.post('/check-out', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const { employeeId, workDate } = req.body; - - if (!employeeId || !workDate) { - return res.status(400).json({ error: 'Employee ID and work date required' }); - } - - // Find the check-in record (SuperAdmin can check out any, Supervisor only their own) - let query = 'SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?'; - let params = [employeeId, workDate, 'CheckedIn']; - - if (req.user.role === 'Supervisor') { - query += ' AND supervisor_id = ?'; - params.push(req.user.id); - } - - const [records] = await db.query(query, params); - - if (records.length === 0) { - return res.status(404).json({ error: 'No check-in record found for today' }); - } - - const checkOutTime = new Date(); - - await db.query( - 'UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?', - [checkOutTime, 'CheckedOut', records[0].id] - ); - - const [updatedRecord] = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [records[0].id] - ); - - res.json(updatedRecord[0]); - } catch (error) { - console.error('Check out error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get attendance summary -router.get('/summary/stats', authenticateToken, async (req, res) => { - try { - const { startDate, endDate, departmentId } = req.query; - - let query = ` - SELECT - COUNT(DISTINCT a.employee_id) as total_employees, - COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in, - COUNT(DISTINCT CASE WHEN a.status = 'CheckedOut' THEN a.employee_id END) as checked_out, - d.name as department_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - LEFT JOIN departments d ON e.department_id = d.id - WHERE 1=1 - `; - const params = []; - - if (req.user.role === 'Supervisor') { - query += ' AND a.supervisor_id = ?'; - params.push(req.user.id); - } - - if (startDate) { - query += ' AND a.work_date >= ?'; - params.push(startDate); - } - - if (endDate) { - query += ' AND a.work_date <= ?'; - params.push(endDate); - } - - if (departmentId) { - query += ' AND e.department_id = ?'; - params.push(departmentId); - } - - query += ' GROUP BY d.id, d.name'; - - const [summary] = await db.query(query, params); - res.json(summary); - } catch (error) { - console.error('Get attendance summary error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/backend/routes/auth.js b/backend/routes/auth.js deleted file mode 100644 index 47dbf7a..0000000 --- a/backend/routes/auth.js +++ /dev/null @@ -1,114 +0,0 @@ -import express from 'express'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; -import db from '../config/database.js'; -import { authenticateToken } from '../middleware/auth.js'; - -const router = express.Router(); - -// Login -router.post('/login', async (req, res) => { - try { - const { username, password } = req.body; - - if (!username || !password) { - return res.status(400).json({ error: 'Username and password required' }); - } - - const [users] = await db.query( - 'SELECT * FROM users WHERE username = ? AND is_active = TRUE', - [username] - ); - - if (users.length === 0) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - const user = users[0]; - const validPassword = await bcrypt.compare(password, user.password); - - if (!validPassword) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - const token = jwt.sign( - { - id: user.id, - username: user.username, - role: user.role, - departmentId: user.department_id - }, - process.env.JWT_SECRET, - { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } - ); - - res.json({ - token, - user: { - id: user.id, - username: user.username, - name: user.name, - email: user.email, - role: user.role, - department_id: user.department_id, - contractor_id: user.contractor_id, - is_active: user.is_active - } - }); - } catch (error) { - console.error('Login error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get current user -router.get('/me', authenticateToken, async (req, res) => { - try { - const [users] = await db.query( - 'SELECT id, username, name, email, role, department_id, contractor_id FROM users WHERE id = ?', - [req.user.id] - ); - - if (users.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json(users[0]); - } catch (error) { - console.error('Get user error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Change password -router.post('/change-password', authenticateToken, async (req, res) => { - try { - const { currentPassword, newPassword } = req.body; - - if (!currentPassword || !newPassword) { - return res.status(400).json({ error: 'Current and new password required' }); - } - - const [users] = await db.query('SELECT password FROM users WHERE id = ?', [req.user.id]); - - if (users.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - const validPassword = await bcrypt.compare(currentPassword, users[0].password); - - if (!validPassword) { - return res.status(401).json({ error: 'Current password is incorrect' }); - } - - const hashedPassword = await bcrypt.hash(newPassword, 10); - await db.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, req.user.id]); - - res.json({ message: 'Password changed successfully' }); - } catch (error) { - console.error('Change password error:', error); - res.status(500).json({ error: 'Internal server error' });p - } -}); - -export default router; diff --git a/backend/routes/contractor-rates.js b/backend/routes/contractor-rates.js deleted file mode 100644 index f9aa103..0000000 --- a/backend/routes/contractor-rates.js +++ /dev/null @@ -1,198 +0,0 @@ -import express from 'express'; -import db from '../config/database.js'; -import { authenticateToken, authorize } from '../middleware/auth.js'; - -const router = express.Router(); - -// Get contractor rates -router.get('/', authenticateToken, async (req, res) => { - try { - const { contractorId, subDepartmentId } = req.query; - - let query = ` - SELECT cr.*, - u.name as contractor_name, u.username as contractor_username, - sd.name as sub_department_name, - d.name as department_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 params = []; - - if (contractorId) { - query += ' AND cr.contractor_id = ?'; - params.push(contractorId); - } - - if (subDepartmentId) { - query += ' AND cr.sub_department_id = ?'; - params.push(subDepartmentId); - } - - query += ' ORDER BY cr.effective_date DESC, cr.created_at DESC'; - - const [rates] = await db.query(query, params); - res.json(rates); - } catch (error) { - console.error('Get contractor rates error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get current rate for a contractor + sub-department combination -router.get('/contractor/:contractorId/current', authenticateToken, async (req, res) => { - try { - const { subDepartmentId } = req.query; - - let query = ` - SELECT cr.*, - u.name as contractor_name, u.username as contractor_username, - sd.name as sub_department_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 - WHERE cr.contractor_id = ? - `; - const params = [req.params.contractorId]; - - if (subDepartmentId) { - query += ' AND cr.sub_department_id = ?'; - params.push(subDepartmentId); - } - - query += ' ORDER BY cr.effective_date DESC LIMIT 1'; - - const [rates] = await db.query(query, params); - - if (rates.length === 0) { - return res.status(404).json({ error: 'No rate found for contractor' }); - } - - res.json(rates[0]); - } catch (error) { - console.error('Get current rate error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Set contractor rate (Supervisor or SuperAdmin) -router.post('/', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const { contractorId, subDepartmentId, activity, rate, effectiveDate } = req.body; - - if (!contractorId || !rate || !effectiveDate) { - return res.status(400).json({ error: 'Missing required fields (contractorId, rate, effectiveDate)' }); - } - - // Verify contractor exists - const [contractors] = await db.query( - 'SELECT * FROM users WHERE id = ? AND role = ?', - [contractorId, 'Contractor'] - ); - - if (contractors.length === 0) { - return res.status(404).json({ error: 'Contractor not found' }); - } - - // Supervisors can only set rates for contractors in their department - if (req.user.role === 'Supervisor' && contractors[0].department_id !== req.user.departmentId) { - return res.status(403).json({ error: 'Contractor not in your department' }); - } - - const [result] = await db.query( - 'INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)', - [contractorId, subDepartmentId || null, activity || null, rate, effectiveDate] - ); - - const [newRate] = await db.query( - `SELECT cr.*, - u.name as contractor_name, u.username as contractor_username, - sd.name as sub_department_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 - WHERE cr.id = ?`, - [result.insertId] - ); - - res.status(201).json(newRate[0]); - } catch (error) { - console.error('Set contractor rate error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update contractor rate -router.put('/:id', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const { rate, activity, effectiveDate } = req.body; - - const [existing] = await db.query('SELECT * FROM contractor_rates WHERE id = ?', [req.params.id]); - - if (existing.length === 0) { - return res.status(404).json({ error: 'Rate not found' }); - } - - const updates = []; - const params = []; - - if (rate !== undefined) { - updates.push('rate = ?'); - params.push(rate); - } - if (activity !== undefined) { - updates.push('activity = ?'); - params.push(activity); - } - if (effectiveDate !== undefined) { - updates.push('effective_date = ?'); - params.push(effectiveDate); - } - - if (updates.length === 0) { - return res.status(400).json({ error: 'No fields to update' }); - } - - params.push(req.params.id); - - await db.query(`UPDATE contractor_rates SET ${updates.join(', ')} WHERE id = ?`, params); - - const [updatedRate] = await db.query( - `SELECT cr.*, - u.name as contractor_name, u.username as contractor_username, - sd.name as sub_department_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 - WHERE cr.id = ?`, - [req.params.id] - ); - - res.json(updatedRate[0]); - } catch (error) { - console.error('Update contractor rate error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Delete contractor rate -router.delete('/:id', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const [existing] = await db.query('SELECT * FROM contractor_rates WHERE id = ?', [req.params.id]); - - if (existing.length === 0) { - return res.status(404).json({ error: 'Rate not found' }); - } - - await db.query('DELETE FROM contractor_rates WHERE id = ?', [req.params.id]); - res.json({ message: 'Rate deleted successfully' }); - } catch (error) { - console.error('Delete contractor rate error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/backend/routes/departments.js b/backend/routes/departments.js deleted file mode 100644 index a61b65e..0000000 --- a/backend/routes/departments.js +++ /dev/null @@ -1,96 +0,0 @@ -import express from 'express'; -import db from '../config/database.js'; -import { authenticateToken, authorize } from '../middleware/auth.js'; - -const router = express.Router(); - -// Get all departments -router.get('/', authenticateToken, async (req, res) => { - try { - const [departments] = await db.query('SELECT * FROM departments ORDER BY name'); - res.json(departments); - } catch (error) { - console.error('Get departments error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get department by ID -router.get('/:id', authenticateToken, async (req, res) => { - try { - const [departments] = await db.query('SELECT * FROM departments WHERE id = ?', [req.params.id]); - - if (departments.length === 0) { - return res.status(404).json({ error: 'Department not found' }); - } - - res.json(departments[0]); - } catch (error) { - console.error('Get department error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get sub-departments by department ID -router.get('/:id/sub-departments', authenticateToken, async (req, res) => { - try { - const [subDepartments] = await db.query( - 'SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name', - [req.params.id] - ); - res.json(subDepartments); - } catch (error) { - console.error('Get sub-departments error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Create department (SuperAdmin only) -router.post('/', authenticateToken, authorize('SuperAdmin'), async (req, res) => { - try { - const { name } = req.body; - - if (!name) { - return res.status(400).json({ error: 'Department name required' }); - } - - const [result] = await db.query('INSERT INTO departments (name) VALUES (?)', [name]); - const [newDepartment] = await db.query('SELECT * FROM departments WHERE id = ?', [result.insertId]); - - res.status(201).json(newDepartment[0]); - } catch (error) { - if (error.code === 'ER_DUP_ENTRY') { - return res.status(400).json({ error: 'Department already exists' }); - } - console.error('Create department error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Create sub-department (SuperAdmin only) -router.post('/:id/sub-departments', authenticateToken, authorize('SuperAdmin'), async (req, res) => { - try { - const { name, primaryActivity } = req.body; - - if (!name || !primaryActivity) { - return res.status(400).json({ error: 'Name and primary activity required' }); - } - - const [result] = await db.query( - 'INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)', - [req.params.id, name, primaryActivity] - ); - - const [newSubDepartment] = await db.query( - 'SELECT * FROM sub_departments WHERE id = ?', - [result.insertId] - ); - - res.status(201).json(newSubDepartment[0]); - } catch (error) { - console.error('Create sub-department error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/backend/routes/users.js b/backend/routes/users.js deleted file mode 100644 index 8772ea1..0000000 --- a/backend/routes/users.js +++ /dev/null @@ -1,236 +0,0 @@ -import express from 'express'; -import bcrypt from 'bcryptjs'; -import db from '../config/database.js'; -import { authenticateToken, authorize } from '../middleware/auth.js'; - -const router = express.Router(); - -// Get all users (with filters) -router.get('/', authenticateToken, async (req, res) => { - try { - const { role, departmentId } = req.query; - let query = ` - SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, - u.contractor_id, u.is_active, u.created_at, - d.name as department_name, - c.name as contractor_name - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN users c ON u.contractor_id = c.id - WHERE 1=1 - `; - const params = []; - - // Supervisors can only see users in their department - if (req.user.role === 'Supervisor') { - query += ' AND u.department_id = ?'; - params.push(req.user.departmentId); - } - - if (role) { - query += ' AND u.role = ?'; - params.push(role); - } - - if (departmentId) { - query += ' AND u.department_id = ?'; - params.push(departmentId); - } - - query += ' ORDER BY u.created_at DESC'; - - const [users] = await db.query(query, params); - res.json(users); - } catch (error) { - console.error('Get users error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get user by ID -router.get('/:id', authenticateToken, async (req, res) => { - try { - const [users] = await db.query( - `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, - u.contractor_id, u.is_active, u.created_at, - d.name as department_name, - c.name as contractor_name - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN users c ON u.contractor_id = c.id - WHERE u.id = ?`, - [req.params.id] - ); - - if (users.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - // Supervisors can only view users in their department - if (req.user.role === 'Supervisor' && users[0].department_id !== req.user.departmentId) { - return res.status(403).json({ error: 'Access denied' }); - } - - res.json(users[0]); - } catch (error) { - console.error('Get user error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Create user -router.post('/', authenticateToken, authorize('SuperAdmin', 'Supervisor'), async (req, res) => { - try { - const { username, name, email, password, role, departmentId, contractorId } = req.body; - - if (!username || !name || !email || !password || !role) { - return res.status(400).json({ error: 'Missing required fields' }); - } - - // Supervisors can only create users in their department - if (req.user.role === 'Supervisor') { - if (departmentId !== req.user.departmentId) { - return res.status(403).json({ error: 'Can only create users in your department' }); - } - if (role === 'SuperAdmin' || role === 'Supervisor') { - return res.status(403).json({ error: 'Cannot create admin or supervisor users' }); - } - } - - const hashedPassword = await bcrypt.hash(password, 10); - - const [result] = await db.query( - 'INSERT INTO users (username, name, email, password, role, department_id, contractor_id) VALUES (?, ?, ?, ?, ?, ?, ?)', - [username, name, email, hashedPassword, role, departmentId || null, contractorId || null] - ); - - const [newUser] = await db.query( - `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, - u.contractor_id, u.is_active, u.created_at, - d.name as department_name, - c.name as contractor_name - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN users c ON u.contractor_id = c.id - WHERE u.id = ?`, - [result.insertId] - ); - - res.status(201).json(newUser[0]); - } catch (error) { - if (error.code === 'ER_DUP_ENTRY') { - return res.status(400).json({ error: 'Username or email already exists' }); - } - console.error('Create user error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update user -router.put('/:id', authenticateToken, authorize('SuperAdmin', 'Supervisor'), async (req, res) => { - try { - const { name, email, role, departmentId, contractorId, isActive } = req.body; - - // Check if user exists - const [existingUsers] = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]); - - if (existingUsers.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - // Supervisors can only update users in their department - if (req.user.role === 'Supervisor') { - if (existingUsers[0].department_id !== req.user.departmentId) { - return res.status(403).json({ error: 'Can only update users in your department' }); - } - if (role === 'SuperAdmin' || role === 'Supervisor') { - return res.status(403).json({ error: 'Cannot modify admin or supervisor roles' }); - } - } - - const updates = []; - const params = []; - - if (name !== undefined) { - updates.push('name = ?'); - params.push(name); - } - if (email !== undefined) { - updates.push('email = ?'); - params.push(email); - } - if (role !== undefined) { - updates.push('role = ?'); - params.push(role); - } - if (departmentId !== undefined) { - updates.push('department_id = ?'); - params.push(departmentId); - } - if (contractorId !== undefined) { - updates.push('contractor_id = ?'); - params.push(contractorId); - } - if (isActive !== undefined) { - updates.push('is_active = ?'); - params.push(isActive); - } - - if (updates.length === 0) { - return res.status(400).json({ error: 'No fields to update' }); - } - - params.push(req.params.id); - - await db.query( - `UPDATE users SET ${updates.join(', ')} WHERE id = ?`, - params - ); - - const [updatedUser] = await db.query( - `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, - u.contractor_id, u.is_active, u.created_at, - d.name as department_name, - c.name as contractor_name - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN users c ON u.contractor_id = c.id - WHERE u.id = ?`, - [req.params.id] - ); - - res.json(updatedUser[0]); - } catch (error) { - console.error('Update user error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Delete user -router.delete('/:id', authenticateToken, authorize('SuperAdmin', 'Supervisor'), async (req, res) => { - try { - const [users] = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]); - - if (users.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - // Supervisors can only delete users in their department - if (req.user.role === 'Supervisor') { - if (users[0].department_id !== req.user.departmentId) { - return res.status(403).json({ error: 'Can only delete users in your department' }); - } - if (users[0].role === 'SuperAdmin' || users[0].role === 'Supervisor') { - return res.status(403).json({ error: 'Cannot delete admin or supervisor users' }); - } - } - - await db.query('DELETE FROM users WHERE id = ?', [req.params.id]); - res.json({ message: 'User deleted successfully' }); - } catch (error) { - console.error('Delete user error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/backend/routes/work-allocations.js b/backend/routes/work-allocations.js deleted file mode 100644 index b39c9d9..0000000 --- a/backend/routes/work-allocations.js +++ /dev/null @@ -1,244 +0,0 @@ -import express from 'express'; -import db from '../config/database.js'; -import { authenticateToken, authorize } from '../middleware/auth.js'; - -const router = express.Router(); - -// Get all work allocations -router.get('/', authenticateToken, async (req, res) => { - try { - const { employeeId, status, departmentId } = req.query; - - let query = ` - SELECT wa.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - c.name as contractor_name, - sd.name as sub_department_name, - d.name as department_name - 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 1=1 - `; - const params = []; - - // Role-based filtering - if (req.user.role === 'Supervisor') { - query += ' AND wa.supervisor_id = ?'; - params.push(req.user.id); - } else if (req.user.role === 'Employee') { - query += ' AND wa.employee_id = ?'; - params.push(req.user.id); - } else if (req.user.role === 'Contractor') { - query += ' AND wa.contractor_id = ?'; - params.push(req.user.id); - } - - if (employeeId) { - query += ' AND wa.employee_id = ?'; - params.push(employeeId); - } - - if (status) { - query += ' AND wa.status = ?'; - params.push(status); - } - - if (departmentId) { - query += ' AND e.department_id = ?'; - params.push(departmentId); - } - - query += ' ORDER BY wa.assigned_date DESC, wa.created_at DESC'; - - const [allocations] = await db.query(query, params); - res.json(allocations); - } catch (error) { - console.error('Get work allocations error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get work allocation by ID -router.get('/:id', authenticateToken, async (req, res) => { - try { - const [allocations] = await db.query( - `SELECT wa.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - c.name as contractor_name, - sd.name as sub_department_name, - d.name as department_name - 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.id = ?`, - [req.params.id] - ); - - if (allocations.length === 0) { - return res.status(404).json({ error: 'Work allocation not found' }); - } - - res.json(allocations[0]); - } catch (error) { - console.error('Get work allocation error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Create work allocation (Supervisor or SuperAdmin) -router.post('/', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const { employeeId, contractorId, subDepartmentId, activity, description, assignedDate, rate, units, totalAmount, departmentId } = req.body; - - if (!employeeId || !contractorId || !assignedDate) { - return res.status(400).json({ error: 'Missing required fields' }); - } - - // SuperAdmin can create for any department, Supervisor only for their own - let targetDepartmentId = req.user.role === 'SuperAdmin' ? departmentId : req.user.departmentId; - - // Verify employee exists (SuperAdmin can assign any employee, Supervisor only their department) - let employeeQuery = 'SELECT * FROM users WHERE id = ?'; - let employeeParams = [employeeId]; - - if (req.user.role === 'Supervisor') { - employeeQuery += ' AND department_id = ?'; - employeeParams.push(req.user.departmentId); - } - - const [employees] = await db.query(employeeQuery, employeeParams); - - if (employees.length === 0) { - return res.status(403).json({ error: 'Employee not found or not in your department' }); - } - - // Use provided rate or get contractor's current rate - let finalRate = rate; - if (!finalRate) { - const [rates] = await db.query( - 'SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1', - [contractorId] - ); - finalRate = rates.length > 0 ? rates[0].rate : null; - } - - const [result] = await db.query( - `INSERT INTO work_allocations - (employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [employeeId, req.user.id, contractorId, subDepartmentId || null, activity || null, description || null, assignedDate, finalRate, units || null, totalAmount || null] - ); - - const [newAllocation] = await db.query( - `SELECT wa.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - c.name as contractor_name, - sd.name as sub_department_name, - d.name as department_name - 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.id = ?`, - [result.insertId] - ); - - res.status(201).json(newAllocation[0]); - } catch (error) { - console.error('Create work allocation error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update work allocation status (Supervisor or SuperAdmin) -router.put('/:id/status', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - const { status, completionDate } = req.body; - - if (!status) { - return res.status(400).json({ error: 'Status required' }); - } - - // SuperAdmin can update any allocation, Supervisor only their own - let query = 'SELECT * FROM work_allocations WHERE id = ?'; - let params = [req.params.id]; - - if (req.user.role === 'Supervisor') { - query += ' AND supervisor_id = ?'; - params.push(req.user.id); - } - - const [allocations] = await db.query(query, params); - - if (allocations.length === 0) { - return res.status(403).json({ error: 'Work allocation not found or access denied' }); - } - - await db.query( - 'UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?', - [status, completionDate || null, req.params.id] - ); - - const [updatedAllocation] = await db.query( - `SELECT wa.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - c.name as contractor_name, - sd.name as sub_department_name, - d.name as department_name - 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.id = ?`, - [req.params.id] - ); - - res.json(updatedAllocation[0]); - } catch (error) { - console.error('Update work allocation error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Delete work allocation (Supervisor or SuperAdmin) -router.delete('/:id', authenticateToken, authorize('Supervisor', 'SuperAdmin'), async (req, res) => { - try { - // SuperAdmin can delete any allocation, Supervisor only their own - let query = 'SELECT * FROM work_allocations WHERE id = ?'; - let params = [req.params.id]; - - if (req.user.role === 'Supervisor') { - query += ' AND supervisor_id = ?'; - params.push(req.user.id); - } - - const [allocations] = await db.query(query, params); - - if (allocations.length === 0) { - return res.status(403).json({ error: 'Work allocation not found or access denied' }); - } - - await db.query('DELETE FROM work_allocations WHERE id = ?', [req.params.id]); - res.json({ message: 'Work allocation deleted successfully' }); - } catch (error) { - console.error('Delete work allocation error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/backend/scripts/hash-password.js b/backend/scripts/hash-password.js deleted file mode 100644 index f595b25..0000000 --- a/backend/scripts/hash-password.js +++ /dev/null @@ -1,13 +0,0 @@ -import bcrypt from 'bcryptjs'; - -const password = process.argv[2] || 'admin123'; - -bcrypt.hash(password, 10, (err, hash) => { - if (err) { - console.error('Error hashing password:', err); - process.exit(1); - } - console.log('Password:', password); - console.log('Hash:', hash); - console.log('\nUse this hash in the database schema or when creating users.'); -}); diff --git a/backend/server.js b/backend/server.js deleted file mode 100644 index dab8d5e..0000000 --- a/backend/server.js +++ /dev/null @@ -1,57 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; -import authRoutes from './routes/auth.js'; -import userRoutes from './routes/users.js'; -import departmentRoutes from './routes/departments.js'; -import workAllocationRoutes from './routes/work-allocations.js'; -import attendanceRoutes from './routes/attendance.js'; -import contractorRateRoutes from './routes/contractor-rates.js'; - -dotenv.config(); - -const app = express(); -const PORT = process.env.PORT || 3000; - -// Middleware -app.use(cors()); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); - -// Request logging -app.use((req, res, next) => { - console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); - next(); -}); - -// Health check -app.get('/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); -}); - -// API Routes -app.use('/api/auth', authRoutes); -app.use('/api/users', userRoutes); -app.use('/api/departments', departmentRoutes); -app.use('/api/work-allocations', workAllocationRoutes); -app.use('/api/attendance', attendanceRoutes); -app.use('/api/contractor-rates', contractorRateRoutes); - -// Error handling middleware -app.use((err, req, res, next) => { - console.error('Error:', err); - res.status(err.status || 500).json({ - error: err.message || 'Internal server error' - }); -}); - -// 404 handler -app.use((req, res) => { - res.status(404).json({ error: 'Route not found' }); -}); - -// Start server -app.listen(PORT, () => { - console.log(`🚀 Server running on http://localhost:${PORT}`); - console.log(`📊 Health check: http://localhost:${PORT}/health`); -}); diff --git a/docker-compose.yml b/docker-compose.yml index bcbabe4..e1b8a39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: mysql: image: mysql:8.0 diff --git a/package-lock.json b/package-lock.json index 64d7839..65b37d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3220,6 +3220,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, diff --git a/src/App.tsx b/src/App.tsx index 1e57b37..f81ffb9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,9 +7,10 @@ import { UsersPage } from './pages/UsersPage'; import { WorkAllocationPage } from './pages/WorkAllocationPage'; import { AttendancePage } from './pages/AttendancePage'; import { RatesPage } from './pages/RatesPage'; +import { EmployeeSwapPage } from './pages/EmployeeSwapPage'; import { LoginPage } from './pages/LoginPage'; -type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates'; +type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps'; const AppContent: React.FC = () => { const [activePage, setActivePage] = useState('dashboard'); @@ -27,6 +28,8 @@ const AppContent: React.FC = () => { return ; case 'rates': return ; + case 'swaps': + return ; default: return ; } diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 46b3278..329e580 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,7 +1,9 @@ -import React, { useState } from 'react'; -import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp, Phone, CreditCard, Landmark, FileText } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import { useDepartments } from '../../hooks/useDepartments'; +import { api } from '../../services/api'; +import type { User as UserType } from '../../types'; interface ProfilePopupProps { isOpen: boolean; @@ -54,14 +56,25 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout } const { user } = useAuth(); const { departments } = useDepartments(); const [showPermissions, setShowPermissions] = useState(false); + const [showDetails, setShowDetails] = useState(false); + const [fullUserData, setFullUserData] = useState(null); + + // Fetch full user details when popup opens + useEffect(() => { + if (isOpen && user?.id) { + api.getUser(user.id).then(setFullUserData).catch(console.error); + } + }, [isOpen, user?.id]); if (!isOpen) return null; const userDepartment = departments.find(d => d.id === user?.department_id); const userPermissions = rolePermissions[user?.role || 'Employee']; + const isEmployeeOrContractor = user?.role === 'Employee' || user?.role === 'Contractor'; + const isContractor = user?.role === 'Contractor'; return ( -
+
{/* Header */}
@@ -120,6 +133,98 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
)} + {/* Personal & Bank Details Section - for Employee and Contractor */} + {isEmployeeOrContractor && ( + + )} + + {showDetails && isEmployeeOrContractor && fullUserData && ( +
+ {/* Personal Details */} +
+

+ Personal Details +

+
+
+ Phone Number + {fullUserData.phone_number || 'Not provided'} +
+
+ Aadhar Number + + {fullUserData.aadhar_number + ? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}` + : 'Not provided'} + +
+
+
+ + {/* Bank Details */} +
+

+ Bank Details +

+
+
+ Bank Name + {fullUserData.bank_name || 'Not provided'} +
+
+ Account Number + + {fullUserData.bank_account_number + ? `XXXX${fullUserData.bank_account_number.slice(-4)}` + : 'Not provided'} + +
+
+ IFSC Code + {fullUserData.bank_ifsc || 'Not provided'} +
+
+
+ + {/* Contractor-specific Details */} + {isContractor && ( +
+

+ Contractor Details +

+
+
+ Agreement No. + {fullUserData.contractor_agreement_number || 'Not provided'} +
+
+ PF Number + {fullUserData.pf_number || 'Not provided'} +
+
+ ESIC Number + {fullUserData.esic_number || 'Not provided'} +
+
+
+ )} +
+ )} + {/* Permissions Section */}
+ + {/* Role indicator at bottom */} +
+
Logged in as
+
+ {user?.role || 'Unknown'} +
+
); }; diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 8e17ef8..87e7ffb 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -1,4 +1,5 @@ -import React, { InputHTMLAttributes } from 'react'; +import React, { InputHTMLAttributes, useState } from 'react'; +import { Eye, EyeOff } from 'lucide-react'; interface InputProps extends InputHTMLAttributes { label?: string; @@ -26,6 +27,45 @@ export const Input: React.FC = ({ label, error, required, className ); }; +interface PasswordInputProps extends Omit, 'type'> { + label?: string; + error?: string; + required?: boolean; +} + +export const PasswordInput: React.FC = ({ label, error, required, className = '', disabled, ...props }) => { + const [showPassword, setShowPassword] = useState(false); + + return ( +
+ {label && ( + + )} +
+ + +
+ {error &&

{error}

} +
+ ); +}; + interface SelectProps extends InputHTMLAttributes { label?: string; error?: string; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 93db50e..346f3f7 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -12,7 +12,6 @@ interface AuthContextType { } const AuthContext = createContext(undefined); - export const useAuth = () => { const context = useContext(AuthContext); if (!context) { diff --git a/src/pages/AttendancePage.tsx b/src/pages/AttendancePage.tsx index 7ee2b9e..bcf87b2 100644 --- a/src/pages/AttendancePage.tsx +++ b/src/pages/AttendancePage.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { AlertTriangle, CheckCircle, Clock, RefreshCw, LogIn, LogOut, Search, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; +import { AlertTriangle, CheckCircle, Clock, RefreshCw, LogIn, LogOut, Search, ArrowUpDown, ArrowUp, ArrowDown, UserX, Edit2, X } 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 { Select, Input } from '../components/ui/Input'; import { api } from '../services/api'; import { useEmployees } from '../hooks/useEmployees'; +import { useAuth } from '../contexts/AuthContext'; +import type { AttendanceStatus } from '../types'; export const AttendancePage: React.FC = () => { const [activeTab, setActiveTab] = useState<'records' | 'checkin'>('records'); @@ -22,6 +24,10 @@ export const AttendancePage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [sortField, setSortField] = useState<'date' | 'employee' | 'status'>('date'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + const [editingRecord, setEditingRecord] = useState(null); + const [editStatus, setEditStatus] = useState('CheckedIn'); + const [editRemark, setEditRemark] = useState(''); + const { user } = useAuth(); // Fetch attendance records const fetchAttendance = async () => { @@ -88,6 +94,47 @@ export const AttendancePage: React.FC = () => { } }; + const handleMarkAbsent = async () => { + if (!selectedEmployee) { + alert('Please select an employee'); + return; + } + setCheckInLoading(true); + try { + await api.markAbsent(parseInt(selectedEmployee), workDate, 'Marked absent by supervisor'); + await fetchAttendance(); + setEmployeeStatus({ status: 'Absent' }); + } catch (err: any) { + alert(err.message || 'Failed to mark absent'); + } finally { + setCheckInLoading(false); + } + }; + + const handleUpdateStatus = async (recordId: number) => { + try { + await api.updateAttendanceStatus(recordId, editStatus, editRemark); + await fetchAttendance(); + setEditingRecord(null); + setEditRemark(''); + } catch (err: any) { + alert(err.message || 'Failed to update status'); + } + }; + + const startEditing = (record: any) => { + setEditingRecord(record.id); + setEditStatus(record.status); + setEditRemark(record.remark || ''); + }; + + const cancelEditing = () => { + setEditingRecord(null); + setEditRemark(''); + }; + + const canEditAttendance = user?.role === 'SuperAdmin' || user?.role === 'Supervisor'; + const employeeOptions = [ { value: '', label: 'Select Employee' }, ...employees.filter(e => e.role === 'Employee').map(e => ({ @@ -233,6 +280,8 @@ export const AttendancePage: React.FC = () => { Status + Remark + {canEditAttendance && Actions} {filteredAndSortedAttendance.map((record) => ( @@ -251,15 +300,76 @@ export const AttendancePage: React.FC = () => { : '-'} - - {record.status === 'CheckedOut' ? 'Completed' : - record.status === 'CheckedIn' ? 'Checked In' : record.status} - + {editingRecord === record.id ? ( + + ) : ( + + {record.status === 'CheckedOut' ? 'Completed' : + record.status === 'CheckedIn' ? 'Checked In' : + record.status === 'HalfDay' ? 'Half Day' : record.status} + + )} + + {editingRecord === record.id ? ( + setEditRemark(e.target.value)} + placeholder="Add remark..." + className="px-2 py-1 border border-gray-300 rounded text-sm w-32" + /> + ) : ( + {record.remark || '-'} + )} + + {canEditAttendance && ( + + {editingRecord === record.id ? ( +
+ + +
+ ) : ( + + )} +
+ )} ))}
@@ -332,7 +442,7 @@ export const AttendancePage: React.FC = () => { +
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index befb783..7e39cd8 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import { Users, Briefcase, Clock, Building2 } from 'lucide-react'; -import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; +import React, { useState, useEffect, useMemo } from 'react'; +import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink } 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'; import { useDepartments } from '../hooks/useDepartments'; @@ -8,17 +8,54 @@ import { useWorkAllocations } from '../hooks/useWorkAllocations'; import { useAuth } from '../contexts/AuthContext'; import { api } from '../services/api'; +// Types for attendance hierarchy +interface AttendanceRecord { + id: number; + employee_id: number; + employee_name: string; + work_date: string; + check_in_time: string | null; + check_out_time: string | null; + status: string; + department_id: number; + department_name: string; + sub_department_id?: number; + sub_department_name?: string; + role: string; + contractor_id?: number; + contractor_name?: string; + remark?: string; +} + +interface HierarchyNode { + id: number; + name: string; + role: string; + department: string; + subDepartment?: string; + activity?: string; + status?: string; + inTime?: string; + outTime?: string; + remark?: string; + children: HierarchyNode[]; + isExpanded?: boolean; +} + export const DashboardPage: React.FC = () => { const { employees, loading: employeesLoading } = useEmployees(); const { departments, loading: deptLoading } = useDepartments(); const { allocations, loading: allocLoading } = useWorkAllocations(); const { user } = useAuth(); - const [attendance, setAttendance] = useState([]); + const [attendance, setAttendance] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedNodes, setExpandedNodes] = useState>(new Set()); + const [contractorRates, setContractorRates] = useState>({}); - const [roleData, setRoleData] = useState>([]); - - // Filter departments for supervisors (only show their department) + const isSuperAdmin = user?.role === 'SuperAdmin'; const isSupervisor = user?.role === 'Supervisor'; + const isContractor = user?.role === 'Contractor'; + const isEmployee = user?.role === 'Employee'; const filteredDepartments = isSupervisor ? departments.filter(d => d.id === user?.department_id) : departments; @@ -31,205 +68,1126 @@ export const DashboardPage: React.FC = () => { .catch(console.error); }, []); - // Calculate role distribution + // Fetch contractor rates for supervisor view useEffect(() => { - if (employees.length > 0) { - const roleCounts: Record = {}; - employees.forEach(e => { - roleCounts[e.role] = (roleCounts[e.role] || 0) + 1; - }); - - const colors: Record = { - 'SuperAdmin': '#8b5cf6', - 'Supervisor': '#3b82f6', - 'Contractor': '#f59e0b', - 'Employee': '#10b981' - }; - - setRoleData( - Object.entries(roleCounts).map(([role, count]) => ({ - name: role, - value: count, - fill: colors[role] || '#6b7280' - })) - ); + if (isSupervisor) { + api.getContractorRates() + .then((rates: { contractor_id: number; rate: number }[]) => { + const rateMap: Record = {}; + rates.forEach(r => { + // Keep the latest rate for each contractor + if (!rateMap[r.contractor_id] || r.rate > rateMap[r.contractor_id]) { + rateMap[r.contractor_id] = r.rate; + } + }); + setContractorRates(rateMap); + }) + .catch(console.error); } + }, [isSupervisor]); + + // Calculate role distribution using useMemo instead of useEffect + const roleData = useMemo(() => { + if (employees.length === 0) return []; + + const roleCounts: Record = {}; + employees.forEach(e => { + roleCounts[e.role] = (roleCounts[e.role] || 0) + 1; + }); + + const colors: Record = { + 'SuperAdmin': '#8b5cf6', + 'Supervisor': '#3b82f6', + 'Contractor': '#f59e0b', + 'Employee': '#10b981' + }; + + return Object.entries(roleCounts).map(([role, count]) => ({ + name: role, + value: count, + fill: colors[role] || '#6b7280' + })); }, [employees]); const loading = employeesLoading || deptLoading || allocLoading; - // Stats calculations - const stats = { - totalUsers: employees.length, - totalDepartments: filteredDepartments.length, - totalAllocations: allocations.length, - pendingAllocations: allocations.filter(a => a.status === 'Pending').length, - completedAllocations: allocations.filter(a => a.status === 'Completed').length, - todayAttendance: attendance.length, - checkedIn: attendance.filter(a => a.status === 'CheckedIn').length, - checkedOut: attendance.filter(a => a.status === 'CheckedOut').length, + // Stats calculations (filtered for supervisor's department if applicable) + const stats = useMemo(() => { + // Filter employees based on role + const relevantEmployees = isSupervisor && user?.department_id + ? employees.filter(e => e.department_id === user.department_id) + : employees; + + const contractorCount = relevantEmployees.filter(e => e.role === 'Contractor').length; + const employeeCount = relevantEmployees.filter(e => e.role === 'Employee').length; + + // Filter attendance for relevant employees + const relevantAttendance = isSupervisor && user?.department_id + ? attendance.filter(a => relevantEmployees.some(e => e.id === a.employee_id)) + : attendance; + + const presentCount = relevantAttendance.filter(a => a.status === 'CheckedIn' || a.status === 'CheckedOut').length; + const absentCount = employeeCount - presentCount; + + // Filter allocations for relevant employees + const relevantAllocations = isSupervisor && user?.department_id + ? allocations.filter(a => relevantEmployees.some(e => e.id === a.employee_id)) + : allocations; + + return { + totalUsers: relevantEmployees.length, + totalDepartments: filteredDepartments.length, + totalAllocations: relevantAllocations.length, + pendingAllocations: relevantAllocations.filter(a => a.status === 'Pending').length, + completedAllocations: relevantAllocations.filter(a => a.status === 'Completed').length, + todayAttendance: relevantAttendance.length, + checkedIn: relevantAttendance.filter(a => a.status === 'CheckedIn').length, + checkedOut: relevantAttendance.filter(a => a.status === 'CheckedOut').length, + contractorCount, + employeeCount, + presentCount, + absentCount: Math.max(0, absentCount), + }; + }, [employees, attendance, allocations, filteredDepartments, isSupervisor, user]); + + // Build hierarchy data for SuperAdmin view + const hierarchyData = useMemo(() => { + if (!isSuperAdmin) return []; + + const supervisors = employees.filter(e => e.role === 'Supervisor'); + + return supervisors.map(supervisor => { + const deptContractors = employees.filter( + e => e.role === 'Contractor' && e.department_id === supervisor.department_id + ); + + 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: [], + }; + }), + }; + }), + }; + + return supervisorNode; + }); + }, [isSuperAdmin, employees, attendance, allocations]); + + // Build hierarchy data for Supervisor view (department-specific) + const supervisorHierarchyData = useMemo(() => { + if (!isSupervisor || !user?.department_id) return []; + + // Get contractors in supervisor's department + const deptContractors = employees.filter( + e => e.role === 'Contractor' && e.department_id === user.department_id + ); + + return deptContractors.map(contractor => { + const contractorEmployees = employees.filter( + e => e.role === 'Employee' && e.contractor_id === contractor.id + ); + + const contractorNode: HierarchyNode = { + 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 || '-', + 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 contractorNode; + }); + }, [isSupervisor, user, employees, attendance, allocations]); + + // Department presence data for bar chart + const departmentPresenceData = useMemo(() => { + return filteredDepartments.map(dept => { + const deptEmployees = employees.filter(e => e.department_id === dept.id && e.role === 'Employee'); + const presentInDept = attendance.filter(a => + deptEmployees.some(e => e.id === a.employee_id) && + (a.status === 'CheckedIn' || a.status === 'CheckedOut') + ).length; + + return { + name: dept.name.substring(0, 10), + fullName: dept.name, + present: presentInDept, + total: deptEmployees.length, + }; + }); + }, [filteredDepartments, employees, attendance]); + + const toggleNode = (nodeKey: string) => { + setExpandedNodes(prev => { + const next = new Set(prev); + if (next.has(nodeKey)) { + next.delete(nodeKey); + } else { + next.add(nodeKey); + } + return next; + }); }; - return ( -
- {loading && ( -
-
- Loading... -
- )} + // Filter hierarchy based on search (for SuperAdmin) + const filteredHierarchy = useMemo(() => { + if (!searchQuery) return hierarchyData; + + const query = searchQuery.toLowerCase(); + + const filterNode = (node: HierarchyNode): HierarchyNode | null => { + const matchesSearch = node.name.toLowerCase().includes(query) || + node.department.toLowerCase().includes(query) || + node.subDepartment?.toLowerCase().includes(query); + + const filteredChildren = node.children + .map(child => filterNode(child)) + .filter((child): child is HierarchyNode => child !== null); + + if (matchesSearch || filteredChildren.length > 0) { + return { ...node, children: filteredChildren }; + } + + return null; + }; + + return hierarchyData + .map(node => filterNode(node)) + .filter((node): node is HierarchyNode => node !== null); + }, [hierarchyData, searchQuery]); - {/* Stats Cards */} -
-
-
-

TOTAL USERS

- + // Filter hierarchy based on search (for Supervisor) + const filteredSupervisorHierarchy = useMemo(() => { + if (!searchQuery) return supervisorHierarchyData; + + const query = searchQuery.toLowerCase(); + + const filterNode = (node: HierarchyNode): HierarchyNode | null => { + const matchesSearch = node.name.toLowerCase().includes(query) || + node.department.toLowerCase().includes(query) || + node.subDepartment?.toLowerCase().includes(query); + + const filteredChildren = node.children + .map(child => filterNode(child)) + .filter((child): child is HierarchyNode => child !== null); + + if (matchesSearch || filteredChildren.length > 0) { + return { ...node, children: filteredChildren }; + } + + return null; + }; + + return supervisorHierarchyData + .map(node => filterNode(node)) + .filter((node): node is HierarchyNode => node !== null); + }, [supervisorHierarchyData, searchQuery]); + + // Render hierarchy row + const renderHierarchyRow = (node: HierarchyNode, level: number = 0, parentKey: string = '') => { + const nodeKey = `${parentKey}-${node.id}`; + const isExpanded = expandedNodes.has(nodeKey); + const hasChildren = node.children.length > 0; + const indent = level * 24; + + const roleColors: Record = { + 'Supervisor': { bg: 'bg-blue-500', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700' }, + 'Contractor': { bg: 'bg-orange-500', text: 'text-orange-700', badge: 'bg-orange-100 text-orange-700' }, + 'Employee': { bg: 'bg-gray-400', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600' }, + }; + + const colors = roleColors[node.role] || roleColors['Employee']; + + return ( + + + +
+ {hasChildren ? ( + + ) : ( + + )} +
+ {node.name} + + {node.role} + +
+ + {node.department} + {node.subDepartment || '-'} + {node.activity || '-'} + + {node.status && ( + + {node.status} + + )} + + {node.inTime || '-'} + {node.outTime || '-'} + {node.remark || '-'} + + {isExpanded && node.children.map(child => renderHierarchyRow(child, level + 1, nodeKey))} + + ); + }; + + // SuperAdmin Dashboard View + if (isSuperAdmin) { + return ( +
+ {loading && ( +
+
+ Loading...
-
{stats.totalUsers}
-
Registered in system
+ )} + + {/* Daily Attendance Report Header */} +
+

Daily Attendance Report

+
-
-
-

DEPARTMENTS

- -
-
{stats.totalDepartments}
-
Active departments
+ {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm" + />
-
-
-

WORK ALLOCATIONS

- -
-
{stats.totalAllocations}
-
- {stats.pendingAllocations} pending, {stats.completedAllocations} completed -
-
- -
-
-

TODAY'S ATTENDANCE

- -
-
{stats.todayAttendance}
-
- {stats.checkedIn} in, {stats.checkedOut} out -
-
-
- - {/* Charts Row */} -
- {/* User Distribution */} + {/* Hierarchical Attendance Table */} - - - {roleData.length > 0 ? ( -
-
- - - - {roleData.map((entry, index) => ( - - ))} - - - +
+ + + + + + + + + + + + + + + {filteredHierarchy.length > 0 ? ( + filteredHierarchy.map(node => renderHierarchyRow(node, 0, 'root')) + ) : ( + + + + )} + +
Level / NameDeptSub-DeptActivityStatusIn TimeOut TimeRemark
+ {searchQuery ? 'No matching records found' : 'No attendance data available'} +
+
+ + + {/* Comparative Analysis Section */} +

Comparative Analysis

+ +
+ {/* Total Workforce Card */} + + +
+

Total Workforce

+ +
+
+ {stats.contractorCount + stats.employeeCount} + Total Personnel +
+
+
+ Contractors + {stats.contractorCount}
-
- {roleData.map((item) => ( -
-
-
- {item.name} +
+ Employees + {stats.employeeCount} +
+
+ + + + {/* Attendance Ratio Donut Chart */} + + +

Attendance Ratio

+
+ + + + + + + + +
+
+
+
+ Absent +
+
+
+ Present +
+
+ + + + {/* Department Presence Bar Chart */} + + +

Department Presence

+ + + + + [`${value} / ${props.payload.total}`, 'Present']} + labelFormatter={(label) => departmentPresenceData.find(d => d.name === label)?.fullName || label} + /> + + {departmentPresenceData.map((entry, index) => { + const colors = ['#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ef4444']; + return ; + })} + + + +
+
+
+ + {/* User Distribution & Recent Allocations Row */} +
+ {/* User Distribution */} + + + + {roleData.length > 0 ? ( +
+
+ + + + {roleData.map((entry, index) => ( + + ))} + + + +
+
+ {roleData.map((item) => ( +
+
+
+ {item.name} +
+ {item.value}
- {item.value} + ))} +
+
+ ) : ( +
No user data
+ )} +
+
+ + {/* Recent Work Allocations */} + + + + {allocations.length > 0 ? ( +
+ {allocations.slice(0, 5).map((alloc) => ( +
+
+
+ {alloc.employee_name || 'Unknown Employee'} +
+
+ {alloc.description || 'No description'} â€ĸ {new Date(alloc.assigned_date).toLocaleDateString()} +
+
+ + {alloc.status} +
))}
-
- ) : ( -
No user data
- )} - + ) : ( +
No recent allocations
+ )} + + +
+
+ ); + } + + // Supervisor Dashboard View + if (isSupervisor) { + const departmentName = filteredDepartments[0]?.name || 'My Department'; + + return ( +
+ {loading && ( +
+
+ Loading... +
+ )} + + {/* Department Header */} +
+
+

{departmentName} Dashboard

+

Daily Attendance & Work Overview

+
+ +
+ + {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm" + /> +
+ + {/* Hierarchical Attendance Table */} + +
+ + + + + + + + + + + + + + + + {filteredSupervisorHierarchy.length > 0 ? ( + filteredSupervisorHierarchy.map(node => { + const rate = contractorRates[node.id]; + return ( + + {/* Contractor Row */} + + + + + + + + + + + + {/* Employee Rows */} + {expandedNodes.has(`supervisor-${node.id}`) && node.children.map(emp => ( + + + + + + + + + + + + ))} + + ); + }) + ) : ( + + + + )} + +
Contractor / EmployeeDeptSub-DeptActivityRate (₹)StatusIn TimeOut TimeRemark
+
+ {node.children.length > 0 && ( + + )} + {node.name} + + Contractor + +
+
{node.department}-- + {rate ? ( + ₹{rate.toFixed(2)} + ) : ( + Not set + )} + ----
+
+ {emp.name} + + Employee + +
+
{emp.department}{emp.subDepartment || '-'}{emp.activity || '-'}- + {emp.status && ( + + {emp.status} + + )} + {emp.inTime || '-'}{emp.outTime || '-'}{emp.remark || '-'}
+ {searchQuery ? 'No matching records found' : 'No contractors or employees in your department'} +
+
- {/* Departments Overview */} + {/* Comparative Analysis Section */} +

Department Overview

+ +
+ {/* Total Workforce Card */} + + +
+

Department Workforce

+ +
+
+ {stats.contractorCount + stats.employeeCount} + Total Personnel +
+
+
+ Contractors + {stats.contractorCount} +
+
+ Employees + {stats.employeeCount} +
+
+
+
+ + {/* Attendance Ratio Donut Chart */} + + +

Attendance Ratio

+
+ + + + + + + + +
+
+
+
+ Present ({stats.presentCount}) +
+
+
+ Absent ({stats.absentCount}) +
+
+ + + + {/* Work Allocation Status */} + + +

Work Allocations

+
+ {stats.totalAllocations} + Total +
+
+
+
+
+ Pending +
+ {stats.pendingAllocations} +
+
+
+
+ Completed +
+ {stats.completedAllocations} +
+
+
+
+ In Progress +
+ {stats.totalAllocations - stats.pendingAllocations - stats.completedAllocations} +
+
+ + +
+ + {/* Recent Work Allocations */} - + - {filteredDepartments.length > 0 ? ( -
- {filteredDepartments.map((dept, idx) => { - const deptUsers = employees.filter(e => e.department_id === dept.id).length; - const colors = ['#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444']; - return ( -
-
-
- {dept.name} + {allocations.length > 0 ? ( +
+ {allocations.slice(0, 5).map((alloc) => ( +
+
+
+ {alloc.employee_name || 'Unknown Employee'} +
+
+ {alloc.description || 'No description'} â€ĸ {new Date(alloc.assigned_date).toLocaleDateString()}
- {deptUsers} users
- ); - })} + + {alloc.status} + +
+ ))}
) : ( -
No departments
+
No recent allocations
)}
+ ); + } - {/* Recent Activity */} - - - - {allocations.length > 0 ? ( -
- {allocations.slice(0, 5).map((alloc) => ( -
-
-
- {alloc.employee_name || 'Unknown Employee'} + // Employee Dashboard View + if (isEmployee) { + // Get employee's own data + const myAttendance = attendance.find(a => a.employee_id === user?.id); + const myAllocations = allocations.filter(a => a.employee_id === user?.id); + const myContractor = employees.find(e => e.id === user?.contractor_id); + const myDepartment = departments.find(d => d.id === user?.department_id); + + return ( +
+ {loading && ( +
+
+ Loading... +
+ )} + + {/* Welcome Header */} +
+

Welcome, {user?.name || 'Employee'}

+

Your personal dashboard

+
+ + {/* Employee Info Cards */} +
+ {/* Department Info */} + + +
+
+ +
+
+
Department
+
{myDepartment?.name || 'Not Assigned'}
+
+
+
+
+ + {/* Contractor Info */} + + +
+
+ +
+
+
Assigned To
+
{myContractor?.name || 'Not Assigned'}
+
+
+
+
+ + {/* Today's Status */} + + +
+
+ +
+
+
Today's Status
+
+ {myAttendance?.status === 'CheckedIn' ? 'Checked In' : + myAttendance?.status === 'CheckedOut' ? 'Checked Out' : + myAttendance?.status || 'Not Checked In'} +
+
+
+ {myAttendance && ( +
+ {myAttendance.check_in_time && ( +
In: {new Date(myAttendance.check_in_time).toLocaleTimeString()}
+ )} + {myAttendance.check_out_time && ( +
Out: {new Date(myAttendance.check_out_time).toLocaleTimeString()}
+ )} +
+ )} +
+
+
+ + {/* My Work Allocations */} + + + + {myAllocations.length > 0 ? ( +
+ {myAllocations.map((alloc) => ( +
+
+
+ {alloc.description || 'Work Assignment'} +
+ + {alloc.status} +
-
- {alloc.description || 'No description'} â€ĸ {new Date(alloc.assigned_date).toLocaleDateString()} +
+
Assigned: {new Date(alloc.assigned_date).toLocaleDateString()}
+ {alloc.sub_department_name &&
Sub-Dept: {alloc.sub_department_name}
} + {alloc.supervisor_name &&
Supervisor: {alloc.supervisor_name}
}
- - {alloc.status} - -
- ))} + ))} +
+ ) : ( +
+ +

No work allocations assigned yet

+
+ )} +
+
+
+ ); + } + + // Contractor Dashboard View + if (isContractor) { + // Get contractor's employees + const myEmployees = employees.filter(e => e.contractor_id === user?.id); + const myDepartment = departments.find(d => d.id === user?.department_id); + const myEmployeeIds = myEmployees.map(e => e.id); + const myEmployeesAttendance = attendance.filter(a => myEmployeeIds.includes(a.employee_id)); + const myEmployeesAllocations = allocations.filter(a => myEmployeeIds.includes(a.employee_id)); + + const presentCount = myEmployeesAttendance.filter( + a => a.status === 'CheckedIn' || a.status === 'CheckedOut' + ).length; + + return ( +
+ {loading && ( +
+
+ Loading... +
+ )} + + {/* Welcome Header */} +
+

Welcome, {user?.name || 'Contractor'}

+

Manage your assigned employees

+
+ + {/* Stats Cards */} +
+
+
+

MY EMPLOYEES

+
- ) : ( -
No recent allocations
- )} - - +
{myEmployees.length}
+
Assigned to you
+
+ +
+
+

DEPARTMENT

+ +
+
{myDepartment?.name || 'N/A'}
+
Your department
+
+ +
+
+

PRESENT TODAY

+ +
+
{presentCount}
+
of {myEmployees.length} employees
+
+ +
+
+

WORK TASKS

+ +
+
{myEmployeesAllocations.length}
+
Active allocations
+
+
+ + {/* My Employees Table */} + + + + {myEmployees.length > 0 ? ( + + + + + + + + + + + + {myEmployees.map(emp => { + const empAttendance = myEmployeesAttendance.find(a => a.employee_id === emp.id); + const empAllocation = myEmployeesAllocations.find(a => a.employee_id === emp.id); + const isPresent = empAttendance?.status === 'CheckedIn' || empAttendance?.status === 'CheckedOut'; + + return ( + + + + + + + + ); + })} + +
EmployeeToday's StatusCheck InCheck OutCurrent Work
+
+
+ + {emp.name.charAt(0).toUpperCase()} + +
+
+
{emp.name}
+
{emp.username}
+
+
+
+ + {empAttendance?.status === 'CheckedIn' ? 'Checked In' : + empAttendance?.status === 'CheckedOut' ? 'Checked Out' : + empAttendance?.status || 'Absent'} + + + {empAttendance?.check_in_time + ? new Date(empAttendance.check_in_time).toLocaleTimeString() + : '-'} + + {empAttendance?.check_out_time + ? new Date(empAttendance.check_out_time).toLocaleTimeString() + : '-'} + + {empAllocation?.description || 'No work assigned'} +
+ ) : ( +
+ +

No employees assigned to you yet

+
+ )} +
+
+
+ ); + } + + // Fallback - should not reach here + return ( +
+
+

Dashboard not available for your role

+
); }; diff --git a/src/pages/EmployeeSwapPage.tsx b/src/pages/EmployeeSwapPage.tsx new file mode 100644 index 0000000..d064a33 --- /dev/null +++ b/src/pages/EmployeeSwapPage.tsx @@ -0,0 +1,594 @@ +import React, { useState, useEffect } from 'react'; +import { + ArrowRightLeft, + Plus, + CheckCircle, + XCircle, + Clock, + Building2, + User, + AlertCircle, + RefreshCw, + Search, + 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 { Select, Input } from '../components/ui/Input'; +import { api } from '../services/api'; +import { useEmployees } from '../hooks/useEmployees'; +import { useDepartments } from '../hooks/useDepartments'; +import type { EmployeeSwap, SwapReason, SwapStatus } from '../types'; + +export const EmployeeSwapPage: React.FC = () => { + const [activeTab, setActiveTab] = useState<'list' | 'create'>('list'); + const [swaps, setSwaps] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + + const { employees } = useEmployees(); + const { departments } = useDepartments(); + + // Form state + const [formData, setFormData] = useState({ + employeeId: '', + targetDepartmentId: '', + targetContractorId: '', + swapReason: '' as SwapReason | '', + reasonDetails: '', + workCompletionPercentage: 0, + swapDate: new Date().toISOString().split('T')[0], + }); + const [submitting, setSubmitting] = useState(false); + + const fetchSwaps = async () => { + setLoading(true); + setError(''); + try { + const params: { status?: string } = {}; + if (statusFilter) params.status = statusFilter; + const data = await api.getEmployeeSwaps(params); + setSwaps(data); + } catch (err: any) { + setError(err.message || 'Failed to fetch swaps'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchSwaps(); + }, [statusFilter]); + + const handleCreateSwap = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.employeeId || !formData.targetDepartmentId || !formData.swapReason) { + alert('Please fill in all required fields'); + return; + } + + setSubmitting(true); + try { + await api.createEmployeeSwap({ + employeeId: parseInt(formData.employeeId), + targetDepartmentId: parseInt(formData.targetDepartmentId), + targetContractorId: formData.targetContractorId ? parseInt(formData.targetContractorId) : undefined, + swapReason: formData.swapReason as SwapReason, + reasonDetails: formData.reasonDetails || undefined, + workCompletionPercentage: formData.workCompletionPercentage, + swapDate: formData.swapDate, + }); + + // Reset form and switch to list + setFormData({ + employeeId: '', + targetDepartmentId: '', + targetContractorId: '', + swapReason: '', + reasonDetails: '', + workCompletionPercentage: 0, + swapDate: new Date().toISOString().split('T')[0], + }); + setActiveTab('list'); + await fetchSwaps(); + } catch (err: any) { + alert(err.message || 'Failed to create swap'); + } finally { + setSubmitting(false); + } + }; + + const handleCompleteSwap = async (id: number) => { + if (!confirm('Complete this swap and return employee to original department?')) return; + try { + await api.completeEmployeeSwap(id); + await fetchSwaps(); + } catch (err: any) { + alert(err.message || 'Failed to complete swap'); + } + }; + + const handleCancelSwap = async (id: number) => { + if (!confirm('Cancel this swap and return employee to original department?')) return; + try { + await api.cancelEmployeeSwap(id); + await fetchSwaps(); + } catch (err: any) { + alert(err.message || 'Failed to cancel swap'); + } + }; + + // Filter employees (only show employees) + const employeeList = employees.filter(e => e.role === 'Employee'); + + // Get contractors for selected target department + const targetContractors = employees.filter( + e => e.role === 'Contractor' && + e.department_id === parseInt(formData.targetDepartmentId) + ); + + // Get selected employee details + const selectedEmployee = employeeList.find(e => e.id === parseInt(formData.employeeId)); + + // Filter swaps based on search + const filteredSwaps = swaps.filter(swap => { + if (!searchQuery) return true; + const query = searchQuery.toLowerCase(); + return ( + swap.employee_name?.toLowerCase().includes(query) || + swap.original_department_name?.toLowerCase().includes(query) || + swap.target_department_name?.toLowerCase().includes(query) + ); + }); + + const getStatusBadge = (status: SwapStatus) => { + switch (status) { + case 'Active': + return Active; + case 'Completed': + return Completed; + case 'Cancelled': + return Cancelled; + } + }; + + const getReasonBadge = (reason: SwapReason) => { + const colors: Record = { + 'LeftWork': 'bg-orange-100 text-orange-700', + 'Sick': 'bg-red-100 text-red-700', + 'FinishedEarly': 'bg-green-100 text-green-700', + 'Other': 'bg-gray-100 text-gray-700', + }; + const labels: Record = { + 'LeftWork': 'Left Work', + 'Sick': 'Sick', + 'FinishedEarly': 'Finished Early', + 'Other': 'Other', + }; + return {labels[reason]}; + }; + + return ( +
+ + {/* Header */} +
+
+
+
+ +
+
+

Employee Work Swap

+

Transfer employees between departments temporarily

+
+
+
+
+ + {/* Tabs */} +
+
+ + +
+
+ + + {activeTab === 'list' && ( +
+ {/* Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+
+ + +
+ +
+ + {/* Stats Cards */} +
+
+
+ + Active +
+
+ {swaps.filter(s => s.status === 'Active').length} +
+
+
+
+ + Completed +
+
+ {swaps.filter(s => s.status === 'Completed').length} +
+
+
+
+ + Cancelled +
+
+ {swaps.filter(s => s.status === 'Cancelled').length} +
+
+
+
+ + Total Swaps +
+
{swaps.length}
+
+
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+ Loading swaps... +
+ ) : filteredSwaps.length > 0 ? ( + + + Employee + From → To + Reason + Completion % + Swap Date + Status + Actions + + + {filteredSwaps.map((swap) => ( + + +
+
+ +
+
+
{swap.employee_name}
+
+ {swap.original_contractor_name && `Under: ${swap.original_contractor_name}`} +
+
+
+
+ +
+ {swap.original_department_name} + + {swap.target_department_name} +
+ {swap.target_contractor_name && ( +
+ New contractor: {swap.target_contractor_name} +
+ )} +
+ +
+ {getReasonBadge(swap.swap_reason)} + {swap.reason_details && ( +
+ {swap.reason_details} +
+ )} +
+
+ +
+
+
+
+ {swap.work_completion_percentage}% +
+ + +
+ {new Date(swap.swap_date).toLocaleDateString()} +
+
+ by {swap.swapped_by_name} +
+
+ {getStatusBadge(swap.status)} + + {swap.status === 'Active' && ( +
+ + +
+ )} +
+ + ))} + +
+ ) : ( +
+ +

No swap records found

+ +
+ )} +
+ )} + + {activeTab === 'create' && ( +
+
+

Create Employee Swap

+

Transfer an employee to a different department temporarily

+
+ +
+ {/* Employee Selection */} +
+

+ + Select Employee +

+ setFormData({ + ...formData, + targetDepartmentId: e.target.value, + targetContractorId: '' // Reset contractor when department changes + })} + options={[ + { value: '', label: 'Select department...' }, + ...departments + .filter(d => d.id !== selectedEmployee?.department_id) + .map(d => ({ value: String(d.id), label: d.name })) + ]} + required + /> + setFormData({ ...formData, swapReason: e.target.value as SwapReason })} + options={[ + { value: '', label: 'Select reason...' }, + { value: 'LeftWork', label: 'Left Work Early' }, + { value: 'Sick', label: 'Sick / Unwell' }, + { value: 'FinishedEarly', label: 'Finished Work Early' }, + { value: 'Other', label: 'Other Reason' }, + ]} + required + /> +
+ +
+ setFormData({ + ...formData, + workCompletionPercentage: parseInt(e.target.value) + })} + className="flex-1" + /> + + {formData.workCompletionPercentage}% + +
+
+
+
+ setFormData({ ...formData, reasonDetails: e.target.value })} + placeholder="Provide more context about the swap..." + /> +
+
+ + {/* Swap Date */} +
+

+ + Swap Date +

+ setFormData({ ...formData, swapDate: e.target.value })} + required + /> +
+ + {/* Submit */} +
+ + +
+ +
+ )} + + +
+ ); +}; diff --git a/src/pages/UsersPage.tsx b/src/pages/UsersPage.tsx index f348da4..6e05d05 100644 --- a/src/pages/UsersPage.tsx +++ b/src/pages/UsersPage.tsx @@ -1,9 +1,9 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import { RefreshCw, Plus, Trash2, Edit, Save, X, Search, AlertTriangle, UserX } from 'lucide-react'; -import { Card, CardHeader, CardContent } from '../components/ui/Card'; +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 { Input, Select, PasswordInput } from '../components/ui/Input'; import { useEmployees } from '../hooks/useEmployees'; import { useDepartments } from '../hooks/useDepartments'; import { useAuth } from '../contexts/AuthContext'; @@ -29,6 +29,16 @@ export const UsersPage: React.FC = () => { departmentId: '', contractorId: '', isActive: true, + // New fields + phoneNumber: '', + aadharNumber: '', + bankAccountNumber: '', + bankName: '', + bankIfsc: '', + // Contractor-specific + contractorAgreementNumber: '', + pfNumber: '', + esicNumber: '', }); const [formError, setFormError] = useState(''); const [formLoading, setFormLoading] = useState(false); @@ -99,6 +109,16 @@ export const UsersPage: React.FC = () => { role: formData.role, departmentId: formData.departmentId ? parseInt(formData.departmentId) : null, contractorId: formData.contractorId ? parseInt(formData.contractorId) : null, + // New fields + phoneNumber: formData.phoneNumber || null, + aadharNumber: formData.aadharNumber || null, + bankAccountNumber: formData.bankAccountNumber || null, + bankName: formData.bankName || null, + bankIfsc: formData.bankIfsc || null, + // Contractor-specific + contractorAgreementNumber: formData.contractorAgreementNumber || null, + pfNumber: formData.pfNumber || null, + esicNumber: formData.esicNumber || null, }); // Reset form and switch to list @@ -111,6 +131,15 @@ export const UsersPage: React.FC = () => { role: 'Employee', departmentId: '', contractorId: '', + isActive: true, + phoneNumber: '', + aadharNumber: '', + bankAccountNumber: '', + bankName: '', + bankIfsc: '', + contractorAgreementNumber: '', + pfNumber: '', + esicNumber: '', }); setActiveTab('list'); refresh(); @@ -143,6 +172,15 @@ export const UsersPage: React.FC = () => { departmentId: user.department_id ? String(user.department_id) : '', contractorId: user.contractor_id ? String(user.contractor_id) : '', isActive: user.is_active, + // New fields + phoneNumber: user.phone_number || '', + aadharNumber: user.aadhar_number || '', + bankAccountNumber: user.bank_account_number || '', + bankName: user.bank_name || '', + bankIfsc: user.bank_ifsc || '', + contractorAgreementNumber: user.contractor_agreement_number || '', + pfNumber: user.pf_number || '', + esicNumber: user.esic_number || '', }); setEditingUserId(user.id); setActiveTab('edit'); @@ -166,6 +204,15 @@ export const UsersPage: React.FC = () => { departmentId: formData.departmentId ? parseInt(formData.departmentId) : null, contractorId: formData.contractorId ? parseInt(formData.contractorId) : null, isActive: formData.isActive, + // New fields + phoneNumber: formData.phoneNumber || null, + aadharNumber: formData.aadharNumber || null, + bankAccountNumber: formData.bankAccountNumber || null, + bankName: formData.bankName || null, + bankIfsc: formData.bankIfsc || null, + contractorAgreementNumber: formData.contractorAgreementNumber || null, + pfNumber: formData.pfNumber || null, + esicNumber: formData.esicNumber || null, }); resetForm(); @@ -189,6 +236,14 @@ export const UsersPage: React.FC = () => { departmentId: '', contractorId: '', isActive: true, + phoneNumber: '', + aadharNumber: '', + bankAccountNumber: '', + bankName: '', + bankIfsc: '', + contractorAgreementNumber: '', + pfNumber: '', + esicNumber: '', }); setEditingUserId(null); setFormError(''); @@ -329,60 +384,89 @@ export const UsersPage: React.FC = () => { EMAIL ROLE DEPARTMENT + REPORTS TO STATUS ACTIONS - {filteredEmployees.map((user) => ( - - {user.id} - {user.username} - {user.name} - {user.email} - - - {user.role} - - - {user.department_name || '-'} - - - {user.is_active ? 'Active' : 'Inactive'} - - - - {canManageUsers && ( -
- - -
- )} -
-
- ))} + {filteredEmployees.map((user) => { + // Find supervisor for contractors (supervisor in same department) + const getSupervisorName = () => { + if (user.role !== 'Contractor') return null; + const supervisor = employees.find( + e => e.role === 'Supervisor' && e.department_id === user.department_id + ); + return supervisor?.name || null; + }; + + // Get reports to info based on role + const getReportsTo = () => { + if (user.role === 'Employee') { + return user.contractor_name ? ( + {user.contractor_name} + ) : '-'; + } + if (user.role === 'Contractor') { + const supervisorName = getSupervisorName(); + return supervisorName ? ( + {supervisorName} + ) : '-'; + } + return '-'; + }; + + return ( + + {user.id} + {user.username} + {user.name} + {user.email} + + + {user.role} + + + {user.department_name || '-'} + {getReportsTo()} + + + {user.is_active ? 'Active' : 'Inactive'} + + + + {canManageUsers && ( +
+ + +
+ )} +
+
+ ); + })}
) : !loading && ( @@ -410,10 +494,9 @@ export const UsersPage: React.FC = () => { onChange={handleInputChange} required /> - { onChange={handleInputChange} required /> - { )}
+ {/* Personal & Bank Details - for Employee and Contractor */} + {(formData.role === 'Employee' || formData.role === 'Contractor') && ( + <> +

Personal Details

+
+ + +
+ +

Bank Details

+
+ + + +
+ + )} + + {/* Contractor-specific fields */} + {formData.role === 'Contractor' && ( + <> +

Contractor Details

+
+ + + +
+ + )} +
+ {/* Personal & Bank Details - for Employee and Contractor */} + {(formData.role === 'Employee' || formData.role === 'Contractor') && ( + <> +

Personal Details

+
+ + +
+ +

Bank Details

+
+ + + +
+ + )} + + {/* Contractor-specific fields */} + {formData.role === 'Contractor' && ( + <> +

Contractor Details

+
+ + + +
+ + )} +
- - - ))} + {deletableUsers.map((user) => { + // Find supervisor for contractors (supervisor in same department) + const getSupervisorName = () => { + if (user.role !== 'Contractor') return null; + const supervisor = employees.find( + e => e.role === 'Supervisor' && e.department_id === user.department_id + ); + return supervisor?.name || null; + }; + + // Get reports to info based on role + const getReportsTo = () => { + if (user.role === 'Employee') { + return user.contractor_name ? ( + {user.contractor_name} + ) : '-'; + } + if (user.role === 'Contractor') { + const supervisorName = getSupervisorName(); + return supervisorName ? ( + {supervisorName} + ) : '-'; + } + return '-'; + }; + + return ( + + {user.id} + {user.username} + {user.name} + {user.email} + + + {user.role} + + + {user.department_name || '-'} + {getReportsTo()} + + + {user.is_active ? 'Active' : 'Inactive'} + + + + + + + ); + })} ) : ( diff --git a/src/services/api.ts b/src/services/api.ts index 4ff6935..9d50fb5 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -166,6 +166,57 @@ class ApiService { return this.request(`/attendance/summary/stats${query ? `?${query}` : ''}`); } + async updateAttendanceStatus(id: number, status: string, remark?: string) { + return this.request(`/attendance/${id}/status`, { + method: 'PUT', + body: JSON.stringify({ status, remark }), + }); + } + + async markAbsent(employeeId: number, workDate: string, remark?: string) { + return this.request('/attendance/mark-absent', { + method: 'POST', + body: JSON.stringify({ employeeId, workDate, remark }), + }); + } + + // Employee Swaps + async getEmployeeSwaps(params?: { status?: string; employeeId?: number; startDate?: string; endDate?: string }) { + const query = params ? new URLSearchParams(params as any).toString() : ''; + return this.request(`/employee-swaps${query ? `?${query}` : ''}`); + } + + async getEmployeeSwap(id: number) { + return this.request(`/employee-swaps/${id}`); + } + + async createEmployeeSwap(data: { + employeeId: number; + targetDepartmentId: number; + targetContractorId?: number; + swapReason: string; + reasonDetails?: string; + workCompletionPercentage?: number; + swapDate: string; + }) { + return this.request('/employee-swaps', { + method: 'POST', + body: JSON.stringify(data), + }); + } + + async completeEmployeeSwap(id: number) { + return this.request(`/employee-swaps/${id}/complete`, { + method: 'PUT', + }); + } + + async cancelEmployeeSwap(id: number) { + return this.request(`/employee-swaps/${id}/cancel`, { + method: 'PUT', + }); + } + // Contractor Rates async getContractorRates(params?: { contractorId?: number; subDepartmentId?: number }) { const query = params ? new URLSearchParams(params as any).toString() : ''; diff --git a/src/types/index.ts b/src/types/index.ts index 7099e4e..c898be7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,6 +10,18 @@ export interface User { created_at: string; department_name?: string; contractor_name?: string; + sub_department_id?: number; + sub_department_name?: string; + // Common fields for Employee and Contractor + phone_number?: string; + aadhar_number?: string; + bank_account_number?: string; + bank_name?: string; + bank_ifsc?: string; + // Contractor-specific fields + contractor_agreement_number?: string; + pf_number?: string; + esic_number?: string; } export interface Department { @@ -49,6 +61,8 @@ export interface WorkAllocation { department_name?: string; } +export type AttendanceStatus = 'CheckedIn' | 'CheckedOut' | 'Absent' | 'HalfDay' | 'Late'; + export interface Attendance { id: number; employee_id: number; @@ -56,7 +70,8 @@ export interface Attendance { check_in_time: string; check_out_time?: string; work_date: string; - status: 'CheckedIn' | 'CheckedOut'; + status: AttendanceStatus; + remark?: string; created_at: string; updated_at: string; employee_name?: string; @@ -66,6 +81,33 @@ export interface Attendance { contractor_name?: string; } +export type SwapReason = 'LeftWork' | 'Sick' | 'FinishedEarly' | 'Other'; +export type SwapStatus = 'Active' | 'Completed' | 'Cancelled'; + +export interface EmployeeSwap { + id: number; + employee_id: number; + original_department_id: number; + target_department_id: number; + original_contractor_id?: number; + target_contractor_id?: number; + swap_reason: SwapReason; + reason_details?: string; + work_completion_percentage: number; + swap_date: string; + swapped_by: number; + status: SwapStatus; + created_at: string; + completed_at?: string; + // Joined fields + employee_name?: string; + original_department_name?: string; + target_department_name?: string; + original_contractor_name?: string; + target_contractor_name?: string; + swapped_by_name?: string; +} + export interface ContractorRate { id: number; contractor_id: number;