(Feat): More changes
This commit is contained in:
@@ -10,6 +10,7 @@ import departmentRoutes from "./routes/departments.ts";
|
|||||||
import workAllocationRoutes from "./routes/work-allocations.ts";
|
import workAllocationRoutes from "./routes/work-allocations.ts";
|
||||||
import attendanceRoutes from "./routes/attendance.ts";
|
import attendanceRoutes from "./routes/attendance.ts";
|
||||||
import contractorRateRoutes from "./routes/contractor-rates.ts";
|
import contractorRateRoutes from "./routes/contractor-rates.ts";
|
||||||
|
import employeeSwapRoutes from "./routes/employee-swaps.ts";
|
||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
await db.connect();
|
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/work-allocations", workAllocationRoutes.routes(), workAllocationRoutes.allowedMethods());
|
||||||
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods());
|
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods());
|
||||||
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods());
|
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods());
|
||||||
|
router.use("/api/employee-swaps", employeeSwapRoutes.routes(), employeeSwapRoutes.allowedMethods());
|
||||||
|
|
||||||
// Apply routes
|
// Apply routes
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from "@oak/oak";
|
import { Router } from "@oak/oak";
|
||||||
import { db } from "../config/database.ts";
|
import { db } from "../config/database.ts";
|
||||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.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();
|
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<Attendance[]>(
|
||||||
|
"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<Attendance[]>(
|
||||||
|
`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<Attendance[]>(
|
||||||
|
"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<Attendance[]>(
|
||||||
|
`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<Attendance[]>(
|
||||||
|
`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
|
// Get attendance summary
|
||||||
router.get("/summary/stats", authenticateToken, async (ctx) => {
|
router.get("/summary/stats", authenticateToken, async (ctx) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Router } from "@oak/oak";
|
import { Router } from "@oak/oak";
|
||||||
import { hash, compare } from "bcrypt";
|
import { hash, compare, genSalt } from "bcrypt";
|
||||||
import { db } from "../config/database.ts";
|
import { db } from "../config/database.ts";
|
||||||
import { config } from "../config/env.ts";
|
import { config } from "../config/env.ts";
|
||||||
|
|
||||||
|
// Helper function to hash password with proper salt generation
|
||||||
|
async function hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = await genSalt(config.BCRYPT_ROUNDS);
|
||||||
|
return await hash(password, salt);
|
||||||
|
}
|
||||||
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts";
|
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts";
|
||||||
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts";
|
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts";
|
||||||
import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.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
|
// Hash new password with configured rounds
|
||||||
const hashedPassword = await hash(newPassword, config.BCRYPT_ROUNDS);
|
const hashedPassword = await hashPassword(newPassword);
|
||||||
|
|
||||||
// Update password
|
// Update password
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|||||||
340
backend-deno/routes/employee-swaps.ts
Normal file
340
backend-deno/routes/employee-swaps.ts
Normal file
@@ -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<EmployeeSwap[]>(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<EmployeeSwap[]>(
|
||||||
|
`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<User[]>(
|
||||||
|
"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<EmployeeSwap[]>(
|
||||||
|
"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<EmployeeSwap[]>(
|
||||||
|
`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<EmployeeSwap[]>(
|
||||||
|
"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<EmployeeSwap[]>(
|
||||||
|
`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<EmployeeSwap[]>(
|
||||||
|
"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<EmployeeSwap[]>(
|
||||||
|
`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;
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Router } from "@oak/oak";
|
import { Router } from "@oak/oak";
|
||||||
import { hash } from "bcrypt";
|
import { hash, genSalt } from "bcrypt";
|
||||||
import { db } from "../config/database.ts";
|
import { db } from "../config/database.ts";
|
||||||
import { config } from "../config/env.ts";
|
import { config } from "../config/env.ts";
|
||||||
|
|
||||||
|
// Helper function to hash password with proper salt generation
|
||||||
|
async function hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = await genSalt(config.BCRYPT_ROUNDS);
|
||||||
|
return await hash(password, salt);
|
||||||
|
}
|
||||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||||
import { sanitizeInput, isValidEmail } from "../middleware/security.ts";
|
import { sanitizeInput, isValidEmail } from "../middleware/security.ts";
|
||||||
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts";
|
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts";
|
||||||
@@ -19,6 +25,9 @@ router.get("/", authenticateToken, async (ctx) => {
|
|||||||
let query = `
|
let query = `
|
||||||
SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||||
u.contractor_id, u.is_active, u.created_at,
|
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,
|
d.name as department_name,
|
||||||
c.name as contractor_name
|
c.name as contractor_name
|
||||||
FROM users u
|
FROM users u
|
||||||
@@ -64,6 +73,9 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
|||||||
const users = await db.query<User[]>(
|
const users = await db.query<User[]>(
|
||||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||||
u.contractor_id, u.is_active, u.created_at,
|
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,
|
d.name as department_name,
|
||||||
c.name as contractor_name
|
c.name as contractor_name
|
||||||
FROM users u
|
FROM users u
|
||||||
@@ -99,7 +111,11 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async
|
|||||||
try {
|
try {
|
||||||
const currentUser = getCurrentUser(ctx);
|
const currentUser = getCurrentUser(ctx);
|
||||||
const body = await ctx.request.body.json() as CreateUserRequest;
|
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
|
// Input validation
|
||||||
if (!username || !name || !email || !password || !role) {
|
if (!username || !name || !email || !password || !role) {
|
||||||
@@ -135,16 +151,28 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
// Hash password
|
||||||
const hashedPassword = await hash(password, config.BCRYPT_ROUNDS);
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|
||||||
const result = await db.execute(
|
const result = await db.execute(
|
||||||
"INSERT INTO users (username, name, email, password, role, department_id, contractor_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id,
|
||||||
[sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, departmentId || null, contractorId || null]
|
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<User[]>(
|
const newUser = await db.query<User[]>(
|
||||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||||
u.contractor_id, u.is_active, u.created_at,
|
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,
|
d.name as department_name,
|
||||||
c.name as contractor_name
|
c.name as contractor_name
|
||||||
FROM users u
|
FROM users u
|
||||||
@@ -175,7 +203,11 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
|
|||||||
const currentUser = getCurrentUser(ctx);
|
const currentUser = getCurrentUser(ctx);
|
||||||
const userId = ctx.params.id;
|
const userId = ctx.params.id;
|
||||||
const body = await ctx.request.body.json() as UpdateUserRequest;
|
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
|
// Check if user exists
|
||||||
const existingUsers = await db.query<User[]>(
|
const existingUsers = await db.query<User[]>(
|
||||||
@@ -235,6 +267,39 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
|
|||||||
updates.push("is_active = ?");
|
updates.push("is_active = ?");
|
||||||
params.push(isActive);
|
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) {
|
if (updates.length === 0) {
|
||||||
ctx.response.status = 400;
|
ctx.response.status = 400;
|
||||||
@@ -252,6 +317,9 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
|
|||||||
const updatedUser = await db.query<User[]>(
|
const updatedUser = await db.query<User[]>(
|
||||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||||
u.contractor_id, u.is_active, u.created_at,
|
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,
|
d.name as department_name,
|
||||||
c.name as contractor_name
|
c.name as contractor_name
|
||||||
FROM users u
|
FROM users u
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { hash } from "bcrypt";
|
import { hash, genSalt } from "bcrypt";
|
||||||
import { db } from "../config/database.ts";
|
import { db } from "../config/database.ts";
|
||||||
import { config } from "../config/env.ts";
|
import { config } from "../config/env.ts";
|
||||||
|
|
||||||
|
// Helper function to hash password with proper salt generation
|
||||||
|
async function hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = await genSalt(config.BCRYPT_ROUNDS);
|
||||||
|
return await hash(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
async function seedDatabase() {
|
async function seedDatabase() {
|
||||||
try {
|
try {
|
||||||
console.log("🔌 Connecting to database...");
|
console.log("🔌 Connecting to database...");
|
||||||
@@ -82,7 +88,7 @@ async function seedDatabase() {
|
|||||||
["admin"]
|
["admin"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const adminPassword = await hash("admin123", config.BCRYPT_ROUNDS);
|
const adminPassword = await hashPassword("admin123");
|
||||||
|
|
||||||
if (existingAdmin.length > 0) {
|
if (existingAdmin.length > 0) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -109,7 +115,7 @@ async function seedDatabase() {
|
|||||||
["Dana"]
|
["Dana"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const supervisorPassword = await hash("supervisor123", config.BCRYPT_ROUNDS);
|
const supervisorPassword = await hashPassword("supervisor123");
|
||||||
|
|
||||||
const supervisors = [
|
const supervisors = [
|
||||||
{ username: "supervisor_tudki", name: "Tudki Supervisor", email: "supervisor.tudki@workallocate.com", deptId: tudkiDept[0]?.id },
|
{ username: "supervisor_tudki", name: "Tudki Supervisor", email: "supervisor.tudki@workallocate.com", deptId: tudkiDept[0]?.id },
|
||||||
@@ -137,11 +143,37 @@ async function seedDatabase() {
|
|||||||
|
|
||||||
// 5. Seed Sample Contractors
|
// 5. Seed Sample Contractors
|
||||||
console.log("🏗️ Seeding sample contractors...");
|
console.log("🏗️ Seeding sample contractors...");
|
||||||
const contractorPassword = await hash("contractor123", config.BCRYPT_ROUNDS);
|
const contractorPassword = await hashPassword("contractor123");
|
||||||
|
|
||||||
const contractors = [
|
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) {
|
for (const con of contractors) {
|
||||||
@@ -151,8 +183,13 @@ async function seedDatabase() {
|
|||||||
);
|
);
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
`INSERT INTO users (username, name, email, password, role, department_id, is_active,
|
||||||
[con.username, con.name, con.email, contractorPassword, "Contractor", con.deptId, true]
|
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`);
|
console.log(` ✅ ${con.name} created`);
|
||||||
} else {
|
} else {
|
||||||
@@ -166,13 +203,40 @@ async function seedDatabase() {
|
|||||||
"SELECT id FROM users WHERE username = ?",
|
"SELECT id FROM users WHERE username = ?",
|
||||||
["contractor1"]
|
["contractor1"]
|
||||||
);
|
);
|
||||||
const employeePassword = await hash("employee123", config.BCRYPT_ROUNDS);
|
const employeePassword = await hashPassword("employee123");
|
||||||
|
|
||||||
if (contractor1.length > 0) {
|
if (contractor1.length > 0) {
|
||||||
const employees = [
|
const employees = [
|
||||||
{ username: "employee1", name: "Employee One", email: "employee1@workallocate.com" },
|
{
|
||||||
{ username: "employee2", name: "Employee Two", email: "employee2@workallocate.com" },
|
username: "employee1",
|
||||||
{ username: "employee3", name: "Employee Three", email: "employee3@workallocate.com" }
|
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) {
|
for (const emp of employees) {
|
||||||
@@ -182,8 +246,11 @@ async function seedDatabase() {
|
|||||||
);
|
);
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active,
|
||||||
[emp.username, emp.name, emp.email, employeePassword, "Employee", groundnutId, contractor1[0].id, true]
|
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`);
|
console.log(` ✅ ${emp.name} created`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ export interface User {
|
|||||||
created_at: Date;
|
created_at: Date;
|
||||||
department_name?: string;
|
department_name?: string;
|
||||||
contractor_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 {
|
export interface JWTPayload {
|
||||||
@@ -66,7 +76,7 @@ export interface WorkAllocation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attendance types
|
// Attendance types
|
||||||
export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent";
|
export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent" | "HalfDay" | "Late";
|
||||||
|
|
||||||
export interface Attendance {
|
export interface Attendance {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -76,6 +86,7 @@ export interface Attendance {
|
|||||||
check_out_time: Date | null;
|
check_out_time: Date | null;
|
||||||
work_date: Date;
|
work_date: Date;
|
||||||
status: AttendanceStatus;
|
status: AttendanceStatus;
|
||||||
|
remark?: string | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
employee_name?: string;
|
employee_name?: string;
|
||||||
supervisor_name?: string;
|
supervisor_name?: string;
|
||||||
@@ -83,6 +94,49 @@ export interface Attendance {
|
|||||||
contractor_name?: string;
|
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
|
// Contractor rate types
|
||||||
export interface ContractorRate {
|
export interface ContractorRate {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -127,6 +181,16 @@ export interface CreateUserRequest {
|
|||||||
role: UserRole;
|
role: UserRole;
|
||||||
departmentId?: number | null;
|
departmentId?: number | null;
|
||||||
contractorId?: 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 {
|
export interface UpdateUserRequest {
|
||||||
@@ -136,6 +200,16 @@ export interface UpdateUserRequest {
|
|||||||
departmentId?: number | null;
|
departmentId?: number | null;
|
||||||
contractorId?: number | null;
|
contractorId?: number | null;
|
||||||
isActive?: boolean;
|
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 {
|
export interface ChangePasswordRequest {
|
||||||
|
|||||||
10
backend/.env
10
backend/.env
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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;
|
|
||||||
@@ -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();
|
|
||||||
@@ -30,6 +30,16 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
department_id INT,
|
department_id INT,
|
||||||
contractor_id INT,
|
contractor_id INT,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
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,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL,
|
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (contractor_id) REFERENCES users(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_in_time DATETIME,
|
||||||
check_out_time DATETIME,
|
check_out_time DATETIME,
|
||||||
work_date DATE NOT NULL,
|
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,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE,
|
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (supervisor_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)
|
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 contractor_rates table
|
||||||
CREATE TABLE IF NOT EXISTS contractor_rates (
|
CREATE TABLE IF NOT EXISTS contractor_rates (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
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_role ON users(role);
|
||||||
CREATE INDEX idx_users_department ON users(department_id);
|
CREATE INDEX idx_users_department ON users(department_id);
|
||||||
CREATE INDEX idx_users_contractor ON users(contractor_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_employee ON work_allocations(employee_id);
|
||||||
CREATE INDEX idx_work_allocations_supervisor ON work_allocations(supervisor_id);
|
CREATE INDEX idx_work_allocations_supervisor ON work_allocations(supervisor_id);
|
||||||
CREATE INDEX idx_work_allocations_contractor ON work_allocations(contractor_id);
|
CREATE INDEX idx_work_allocations_contractor ON work_allocations(contractor_id);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
18
backend/database/migrations/add_user_details.sql
Normal file
18
backend/database/migrations/add_user_details.sql
Normal file
@@ -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);
|
||||||
@@ -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');
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
1120
backend/package-lock.json
generated
1120
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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.');
|
|
||||||
});
|
|
||||||
@@ -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`);
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -3220,6 +3220,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { UsersPage } from './pages/UsersPage';
|
|||||||
import { WorkAllocationPage } from './pages/WorkAllocationPage';
|
import { WorkAllocationPage } from './pages/WorkAllocationPage';
|
||||||
import { AttendancePage } from './pages/AttendancePage';
|
import { AttendancePage } from './pages/AttendancePage';
|
||||||
import { RatesPage } from './pages/RatesPage';
|
import { RatesPage } from './pages/RatesPage';
|
||||||
|
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
|
||||||
import { LoginPage } from './pages/LoginPage';
|
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 AppContent: React.FC = () => {
|
||||||
const [activePage, setActivePage] = useState<PageType>('dashboard');
|
const [activePage, setActivePage] = useState<PageType>('dashboard');
|
||||||
@@ -27,6 +28,8 @@ const AppContent: React.FC = () => {
|
|||||||
return <AttendancePage />;
|
return <AttendancePage />;
|
||||||
case 'rates':
|
case 'rates':
|
||||||
return <RatesPage />;
|
return <RatesPage />;
|
||||||
|
case 'swaps':
|
||||||
|
return <EmployeeSwapPage />;
|
||||||
default:
|
default:
|
||||||
return <DashboardPage />;
|
return <DashboardPage />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp } from 'lucide-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 { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useDepartments } from '../../hooks/useDepartments';
|
import { useDepartments } from '../../hooks/useDepartments';
|
||||||
|
import { api } from '../../services/api';
|
||||||
|
import type { User as UserType } from '../../types';
|
||||||
|
|
||||||
interface ProfilePopupProps {
|
interface ProfilePopupProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -54,14 +56,25 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { departments } = useDepartments();
|
const { departments } = useDepartments();
|
||||||
const [showPermissions, setShowPermissions] = useState(false);
|
const [showPermissions, setShowPermissions] = useState(false);
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const [fullUserData, setFullUserData] = useState<UserType | null>(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;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const userDepartment = departments.find(d => d.id === user?.department_id);
|
const userDepartment = departments.find(d => d.id === user?.department_id);
|
||||||
const userPermissions = rolePermissions[user?.role || 'Employee'];
|
const userPermissions = rolePermissions[user?.role || 'Employee'];
|
||||||
|
const isEmployeeOrContractor = user?.role === 'Employee' || user?.role === 'Contractor';
|
||||||
|
const isContractor = user?.role === 'Contractor';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute right-4 top-16 w-[380px] bg-gradient-to-b from-slate-50 to-white rounded-2xl shadow-2xl z-50 border border-gray-200 overflow-hidden font-sans text-sm text-gray-800">
|
<div className="absolute right-4 top-16 w-[400px] bg-gradient-to-b from-slate-50 to-white rounded-2xl shadow-2xl z-50 border border-gray-200 overflow-hidden font-sans text-sm text-gray-800 max-h-[85vh] overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4">
|
<div className="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
@@ -120,6 +133,98 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Personal & Bank Details Section - for Employee and Contractor */}
|
||||||
|
{isEmployeeOrContractor && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
className="w-full flex items-center justify-between p-3 bg-teal-50 hover:bg-teal-100 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-teal-100 rounded-full flex items-center justify-center">
|
||||||
|
<CreditCard size={18} className="text-teal-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-xs text-gray-500 font-medium">Personal & Bank Details</p>
|
||||||
|
<p className="text-sm font-semibold text-gray-800">View your information</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showDetails ? <ChevronUp size={18} className="text-teal-600" /> : <ChevronDown size={18} className="text-teal-600" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDetails && isEmployeeOrContractor && fullUserData && (
|
||||||
|
<div className="bg-teal-50 rounded-xl p-4 border border-teal-200 space-y-4">
|
||||||
|
{/* Personal Details */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-teal-800 mb-2 flex items-center gap-2">
|
||||||
|
<Phone size={14} /> Personal Details
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Phone Number</span>
|
||||||
|
<span className="font-medium text-gray-800">{fullUserData.phone_number || 'Not provided'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Aadhar Number</span>
|
||||||
|
<span className="font-medium text-gray-800">
|
||||||
|
{fullUserData.aadhar_number
|
||||||
|
? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}`
|
||||||
|
: 'Not provided'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bank Details */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-teal-800 mb-2 flex items-center gap-2">
|
||||||
|
<Landmark size={14} /> Bank Details
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Bank Name</span>
|
||||||
|
<span className="font-medium text-gray-800">{fullUserData.bank_name || 'Not provided'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Account Number</span>
|
||||||
|
<span className="font-medium text-gray-800">
|
||||||
|
{fullUserData.bank_account_number
|
||||||
|
? `XXXX${fullUserData.bank_account_number.slice(-4)}`
|
||||||
|
: 'Not provided'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">IFSC Code</span>
|
||||||
|
<span className="font-medium text-gray-800">{fullUserData.bank_ifsc || 'Not provided'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contractor-specific Details */}
|
||||||
|
{isContractor && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-teal-800 mb-2 flex items-center gap-2">
|
||||||
|
<FileText size={14} /> Contractor Details
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Agreement No.</span>
|
||||||
|
<span className="font-medium text-gray-800">{fullUserData.contractor_agreement_number || 'Not provided'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">PF Number</span>
|
||||||
|
<span className="font-medium text-gray-800">{fullUserData.pf_number || 'Not provided'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">ESIC Number</span>
|
||||||
|
<span className="font-medium text-gray-800">{fullUserData.esic_number || 'Not provided'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Permissions Section */}
|
{/* Permissions Section */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPermissions(!showPermissions)}
|
onClick={() => setShowPermissions(!showPermissions)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList } from 'lucide-react';
|
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft } from 'lucide-react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
||||||
interface SidebarItemProps {
|
interface SidebarItemProps {
|
||||||
@@ -30,7 +30,16 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
|
export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
|
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||||
|
const isSupervisor = user?.role === 'Supervisor';
|
||||||
|
const isContractor = user?.role === 'Contractor';
|
||||||
|
const isEmployee = user?.role === 'Employee';
|
||||||
|
|
||||||
|
// Role-based access
|
||||||
|
const canManageUsers = isSuperAdmin || isSupervisor;
|
||||||
|
const canManageAllocations = isSuperAdmin || isSupervisor;
|
||||||
|
const canManageAttendance = isSuperAdmin || isSupervisor;
|
||||||
|
const canManageRates = isSuperAdmin || isSupervisor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 bg-[#1e293b] flex flex-col">
|
<div className="w-64 bg-[#1e293b] flex flex-col">
|
||||||
@@ -46,30 +55,45 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex-1 py-4">
|
<nav className="flex-1 py-4">
|
||||||
|
{/* Dashboard - visible to all */}
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={LayoutDashboard}
|
icon={LayoutDashboard}
|
||||||
label="Dashboard"
|
label="Dashboard"
|
||||||
active={activePage === 'dashboard'}
|
active={activePage === 'dashboard'}
|
||||||
onClick={() => onNavigate('dashboard')}
|
onClick={() => onNavigate('dashboard')}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
|
||||||
icon={Users}
|
{/* User Management - SuperAdmin and Supervisor only */}
|
||||||
label="User Management"
|
{canManageUsers && (
|
||||||
active={activePage === 'users'}
|
<SidebarItem
|
||||||
onClick={() => onNavigate('users')}
|
icon={Users}
|
||||||
/>
|
label="User Management"
|
||||||
<SidebarItem
|
active={activePage === 'users'}
|
||||||
icon={Briefcase}
|
onClick={() => onNavigate('users')}
|
||||||
label="Work Allocation"
|
/>
|
||||||
active={activePage === 'allocation'}
|
)}
|
||||||
onClick={() => onNavigate('allocation')}
|
|
||||||
/>
|
{/* Work Allocation - SuperAdmin and Supervisor only */}
|
||||||
<SidebarItem
|
{canManageAllocations && (
|
||||||
icon={CalendarCheck}
|
<SidebarItem
|
||||||
label="Attendance"
|
icon={Briefcase}
|
||||||
active={activePage === 'attendance'}
|
label="Work Allocation"
|
||||||
onClick={() => onNavigate('attendance')}
|
active={activePage === 'allocation'}
|
||||||
/>
|
onClick={() => onNavigate('allocation')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Attendance - SuperAdmin and Supervisor only */}
|
||||||
|
{canManageAttendance && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={CalendarCheck}
|
||||||
|
label="Attendance"
|
||||||
|
active={activePage === 'attendance'}
|
||||||
|
onClick={() => onNavigate('attendance')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contractor Rates - SuperAdmin and Supervisor only */}
|
||||||
{canManageRates && (
|
{canManageRates && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={DollarSign}
|
icon={DollarSign}
|
||||||
@@ -78,7 +102,30 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
|
|||||||
onClick={() => onNavigate('rates')}
|
onClick={() => onNavigate('rates')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Employee Swap - SuperAdmin only */}
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={ArrowRightLeft}
|
||||||
|
label="Employee Swap"
|
||||||
|
active={activePage === 'swaps'}
|
||||||
|
onClick={() => onNavigate('swaps')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Role indicator at bottom */}
|
||||||
|
<div className="p-4 border-t border-gray-700">
|
||||||
|
<div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Logged in as</div>
|
||||||
|
<div className={`text-sm font-medium ${
|
||||||
|
isSuperAdmin ? 'text-purple-400' :
|
||||||
|
isSupervisor ? 'text-blue-400' :
|
||||||
|
isContractor ? 'text-orange-400' :
|
||||||
|
isEmployee ? 'text-green-400' : 'text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{user?.role || 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<HTMLInputElement> {
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -26,6 +27,45 @@ export const Input: React.FC<InputProps> = ({ label, error, required, className
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordInput: React.FC<PasswordInputProps> = ({ label, error, required, className = '', disabled, ...props }) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{label} {required && <span className="text-red-500">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
className={`w-full px-4 py-2 pr-10 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
error ? 'border-red-500' : ''
|
||||||
|
} ${disabled ? 'bg-gray-100 text-gray-600 cursor-not-allowed' : ''} ${className}`}
|
||||||
|
disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface SelectProps extends InputHTMLAttributes<HTMLSelectElement> {
|
interface SelectProps extends InputHTMLAttributes<HTMLSelectElement> {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ interface AuthContextType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
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 { Card, CardContent } from '../components/ui/Card';
|
||||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { Select, Input } from '../components/ui/Input';
|
import { Select, Input } from '../components/ui/Input';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
import { useEmployees } from '../hooks/useEmployees';
|
import { useEmployees } from '../hooks/useEmployees';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import type { AttendanceStatus } from '../types';
|
||||||
|
|
||||||
export const AttendancePage: React.FC = () => {
|
export const AttendancePage: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<'records' | 'checkin'>('records');
|
const [activeTab, setActiveTab] = useState<'records' | 'checkin'>('records');
|
||||||
@@ -22,6 +24,10 @@ export const AttendancePage: React.FC = () => {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [sortField, setSortField] = useState<'date' | 'employee' | 'status'>('date');
|
const [sortField, setSortField] = useState<'date' | 'employee' | 'status'>('date');
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
|
const [editingRecord, setEditingRecord] = useState<number | null>(null);
|
||||||
|
const [editStatus, setEditStatus] = useState<AttendanceStatus>('CheckedIn');
|
||||||
|
const [editRemark, setEditRemark] = useState('');
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Fetch attendance records
|
// Fetch attendance records
|
||||||
const fetchAttendance = async () => {
|
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 = [
|
const employeeOptions = [
|
||||||
{ value: '', label: 'Select Employee' },
|
{ value: '', label: 'Select Employee' },
|
||||||
...employees.filter(e => e.role === 'Employee').map(e => ({
|
...employees.filter(e => e.role === 'Employee').map(e => ({
|
||||||
@@ -233,6 +280,8 @@ export const AttendancePage: React.FC = () => {
|
|||||||
Status <SortIcon field="status" />
|
Status <SortIcon field="status" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
<TableHead>Remark</TableHead>
|
||||||
|
{canEditAttendance && <TableHead>Actions</TableHead>}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredAndSortedAttendance.map((record) => (
|
{filteredAndSortedAttendance.map((record) => (
|
||||||
@@ -251,15 +300,76 @@ export const AttendancePage: React.FC = () => {
|
|||||||
: '-'}
|
: '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
{editingRecord === record.id ? (
|
||||||
record.status === 'CheckedOut' ? 'bg-green-100 text-green-700' :
|
<select
|
||||||
record.status === 'CheckedIn' ? 'bg-blue-100 text-blue-700' :
|
value={editStatus}
|
||||||
'bg-gray-100 text-gray-700'
|
onChange={(e) => setEditStatus(e.target.value as AttendanceStatus)}
|
||||||
}`}>
|
className="px-2 py-1 border border-gray-300 rounded text-sm"
|
||||||
{record.status === 'CheckedOut' ? 'Completed' :
|
>
|
||||||
record.status === 'CheckedIn' ? 'Checked In' : record.status}
|
<option value="CheckedIn">Checked In</option>
|
||||||
</span>
|
<option value="CheckedOut">Checked Out</option>
|
||||||
|
<option value="Absent">Absent</option>
|
||||||
|
<option value="HalfDay">Half Day</option>
|
||||||
|
<option value="Late">Late</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
record.status === 'CheckedOut' ? 'bg-green-100 text-green-700' :
|
||||||
|
record.status === 'CheckedIn' ? 'bg-blue-100 text-blue-700' :
|
||||||
|
record.status === 'Absent' ? 'bg-red-100 text-red-700' :
|
||||||
|
record.status === 'HalfDay' ? 'bg-orange-100 text-orange-700' :
|
||||||
|
record.status === 'Late' ? 'bg-yellow-100 text-yellow-700' :
|
||||||
|
'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{record.status === 'CheckedOut' ? 'Completed' :
|
||||||
|
record.status === 'CheckedIn' ? 'Checked In' :
|
||||||
|
record.status === 'HalfDay' ? 'Half Day' : record.status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{editingRecord === record.id ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editRemark}
|
||||||
|
onChange={(e) => setEditRemark(e.target.value)}
|
||||||
|
placeholder="Add remark..."
|
||||||
|
className="px-2 py-1 border border-gray-300 rounded text-sm w-32"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500 text-sm">{record.remark || '-'}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
{canEditAttendance && (
|
||||||
|
<TableCell>
|
||||||
|
{editingRecord === record.id ? (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleUpdateStatus(record.id)}
|
||||||
|
className="p-1 text-green-600 hover:bg-green-50 rounded"
|
||||||
|
title="Save"
|
||||||
|
>
|
||||||
|
<CheckCircle size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={cancelEditing}
|
||||||
|
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||||
|
title="Cancel"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => startEditing(record)}
|
||||||
|
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||||
|
title="Edit Status"
|
||||||
|
>
|
||||||
|
<Edit2 size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -332,7 +442,7 @@ export const AttendancePage: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={handleCheckIn}
|
onClick={handleCheckIn}
|
||||||
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut'}
|
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut' || employeeStatus?.status === 'Absent'}
|
||||||
>
|
>
|
||||||
<LogIn size={16} className="mr-2" />
|
<LogIn size={16} className="mr-2" />
|
||||||
{checkInLoading ? 'Processing...' : 'Check In'}
|
{checkInLoading ? 'Processing...' : 'Check In'}
|
||||||
@@ -346,6 +456,15 @@ export const AttendancePage: React.FC = () => {
|
|||||||
<LogOut size={16} className="mr-2" />
|
<LogOut size={16} className="mr-2" />
|
||||||
{checkInLoading ? 'Processing...' : 'Check Out'}
|
{checkInLoading ? 'Processing...' : 'Check Out'}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="danger"
|
||||||
|
onClick={handleMarkAbsent}
|
||||||
|
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut' || employeeStatus?.status === 'Absent'}
|
||||||
|
>
|
||||||
|
<UserX size={16} className="mr-2" />
|
||||||
|
{checkInLoading ? 'Processing...' : 'Mark Absent'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
594
src/pages/EmployeeSwapPage.tsx
Normal file
594
src/pages/EmployeeSwapPage.tsx
Normal file
@@ -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<EmployeeSwap[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<SwapStatus | ''>('');
|
||||||
|
|
||||||
|
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 <span className="px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-700">Active</span>;
|
||||||
|
case 'Completed':
|
||||||
|
return <span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">Completed</span>;
|
||||||
|
case 'Cancelled':
|
||||||
|
return <span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">Cancelled</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReasonBadge = (reason: SwapReason) => {
|
||||||
|
const colors: Record<SwapReason, string> = {
|
||||||
|
'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<SwapReason, string> = {
|
||||||
|
'LeftWork': 'Left Work',
|
||||||
|
'Sick': 'Sick',
|
||||||
|
'FinishedEarly': 'Finished Early',
|
||||||
|
'Other': 'Other',
|
||||||
|
};
|
||||||
|
return <span className={`px-2 py-1 rounded text-xs font-medium ${colors[reason]}`}>{labels[reason]}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-purple-100 rounded-lg">
|
||||||
|
<ArrowRightLeft className="text-purple-600" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-gray-800">Employee Work Swap</h1>
|
||||||
|
<p className="text-sm text-gray-500">Transfer employees between departments temporarily</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="border-b border-gray-200">
|
||||||
|
<div className="flex space-x-8 px-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('list')}
|
||||||
|
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||||
|
activeTab === 'list'
|
||||||
|
? 'border-purple-500 text-purple-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Swap History
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('create')}
|
||||||
|
className={`py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 ${
|
||||||
|
activeTab === 'create'
|
||||||
|
? 'border-purple-500 text-purple-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
New Swap
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{activeTab === 'list' && (
|
||||||
|
<div>
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="flex gap-4 mb-6">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by employee or department..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Filter size={18} className="text-gray-400" />
|
||||||
|
<select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value as SwapStatus | '')}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="Active">Active</option>
|
||||||
|
<option value="Completed">Completed</option>
|
||||||
|
<option value="Cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" onClick={fetchSwaps}>
|
||||||
|
<RefreshCw size={16} className="mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 text-blue-600 mb-1">
|
||||||
|
<Clock size={18} />
|
||||||
|
<span className="text-sm font-medium">Active</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-700">
|
||||||
|
{swaps.filter(s => s.status === 'Active').length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 text-green-600 mb-1">
|
||||||
|
<CheckCircle size={18} />
|
||||||
|
<span className="text-sm font-medium">Completed</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-green-700">
|
||||||
|
{swaps.filter(s => s.status === 'Completed').length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-red-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 text-red-600 mb-1">
|
||||||
|
<XCircle size={18} />
|
||||||
|
<span className="text-sm font-medium">Cancelled</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-red-700">
|
||||||
|
{swaps.filter(s => s.status === 'Cancelled').length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 text-purple-600 mb-1">
|
||||||
|
<ArrowRightLeft size={18} />
|
||||||
|
<span className="text-sm font-medium">Total Swaps</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-purple-700">{swaps.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-purple-600"></div>
|
||||||
|
<span className="ml-2 text-gray-600">Loading swaps...</span>
|
||||||
|
</div>
|
||||||
|
) : filteredSwaps.length > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableHead>Employee</TableHead>
|
||||||
|
<TableHead>From → To</TableHead>
|
||||||
|
<TableHead>Reason</TableHead>
|
||||||
|
<TableHead>Completion %</TableHead>
|
||||||
|
<TableHead>Swap Date</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredSwaps.map((swap) => (
|
||||||
|
<TableRow key={swap.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||||
|
<User size={16} className="text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-800">{swap.employee_name}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{swap.original_contractor_name && `Under: ${swap.original_contractor_name}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-gray-600">{swap.original_department_name}</span>
|
||||||
|
<ArrowRightLeft size={14} className="text-gray-400" />
|
||||||
|
<span className="font-medium text-purple-600">{swap.target_department_name}</span>
|
||||||
|
</div>
|
||||||
|
{swap.target_contractor_name && (
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
New contractor: {swap.target_contractor_name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{getReasonBadge(swap.swap_reason)}
|
||||||
|
{swap.reason_details && (
|
||||||
|
<div className="text-xs text-gray-500 max-w-[150px] truncate" title={swap.reason_details}>
|
||||||
|
{swap.reason_details}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-16 bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-purple-600 h-2 rounded-full"
|
||||||
|
style={{ width: `${swap.work_completion_percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-600">{swap.work_completion_percentage}%</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{new Date(swap.swap_date).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
by {swap.swapped_by_name}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(swap.status)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{swap.status === 'Active' && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleCompleteSwap(swap.id)}
|
||||||
|
className="p-1.5 text-green-600 hover:bg-green-50 rounded"
|
||||||
|
title="Complete & Return"
|
||||||
|
>
|
||||||
|
<CheckCircle size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCancelSwap(swap.id)}
|
||||||
|
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
|
||||||
|
title="Cancel Swap"
|
||||||
|
>
|
||||||
|
<XCircle size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<ArrowRightLeft size={48} className="mx-auto mb-4 text-gray-300" />
|
||||||
|
<p>No swap records found</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={() => setActiveTab('create')}
|
||||||
|
>
|
||||||
|
<Plus size={16} className="mr-2" />
|
||||||
|
Create First Swap
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'create' && (
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">Create Employee Swap</h2>
|
||||||
|
<p className="text-sm text-gray-500">Transfer an employee to a different department temporarily</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreateSwap} className="space-y-6">
|
||||||
|
{/* Employee Selection */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||||
|
<User size={16} />
|
||||||
|
Select Employee
|
||||||
|
</h3>
|
||||||
|
<Select
|
||||||
|
label="Employee"
|
||||||
|
value={formData.employeeId}
|
||||||
|
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select an employee...' },
|
||||||
|
...employeeList.map(e => ({
|
||||||
|
value: String(e.id),
|
||||||
|
label: `${e.name} - ${e.department_name || 'No Dept'}`
|
||||||
|
}))
|
||||||
|
]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedEmployee && (
|
||||||
|
<div className="mt-3 p-3 bg-white rounded border border-gray-200">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
|
||||||
|
<User size={20} className="text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-800">{selectedEmployee.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Current: {selectedEmployee.department_name || 'No Department'}
|
||||||
|
{selectedEmployee.contractor_name && ` • Under: ${selectedEmployee.contractor_name}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Target Department */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||||
|
<Building2 size={16} />
|
||||||
|
Target Department
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Select
|
||||||
|
label="Department"
|
||||||
|
value={formData.targetDepartmentId}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Assign to Contractor (Optional)"
|
||||||
|
value={formData.targetContractorId}
|
||||||
|
onChange={(e) => setFormData({ ...formData, targetContractorId: e.target.value })}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'No contractor' },
|
||||||
|
...targetContractors.map(c => ({ value: String(c.id), label: c.name }))
|
||||||
|
]}
|
||||||
|
disabled={!formData.targetDepartmentId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swap Reason */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
Swap Reason
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Select
|
||||||
|
label="Reason"
|
||||||
|
value={formData.swapReason}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Work Completion %
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="5"
|
||||||
|
value={formData.workCompletionPercentage}
|
||||||
|
onChange={(e) => setFormData({
|
||||||
|
...formData,
|
||||||
|
workCompletionPercentage: parseInt(e.target.value)
|
||||||
|
})}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700 w-12">
|
||||||
|
{formData.workCompletionPercentage}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Input
|
||||||
|
label="Additional Details (Optional)"
|
||||||
|
value={formData.reasonDetails}
|
||||||
|
onChange={(e) => setFormData({ ...formData, reasonDetails: e.target.value })}
|
||||||
|
placeholder="Provide more context about the swap..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swap Date */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||||
|
<Clock size={16} />
|
||||||
|
Swap Date
|
||||||
|
</h3>
|
||||||
|
<Input
|
||||||
|
label="Date"
|
||||||
|
type="date"
|
||||||
|
value={formData.swapDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, swapDate: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setActiveTab('list')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting || !formData.employeeId || !formData.targetDepartmentId || !formData.swapReason}
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightLeft size={16} className="mr-2" />
|
||||||
|
Create Swap
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { 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 { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||||
import { Button } from '../components/ui/Button';
|
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 { useEmployees } from '../hooks/useEmployees';
|
||||||
import { useDepartments } from '../hooks/useDepartments';
|
import { useDepartments } from '../hooks/useDepartments';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
@@ -29,6 +29,16 @@ export const UsersPage: React.FC = () => {
|
|||||||
departmentId: '',
|
departmentId: '',
|
||||||
contractorId: '',
|
contractorId: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
// New fields
|
||||||
|
phoneNumber: '',
|
||||||
|
aadharNumber: '',
|
||||||
|
bankAccountNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
bankIfsc: '',
|
||||||
|
// Contractor-specific
|
||||||
|
contractorAgreementNumber: '',
|
||||||
|
pfNumber: '',
|
||||||
|
esicNumber: '',
|
||||||
});
|
});
|
||||||
const [formError, setFormError] = useState('');
|
const [formError, setFormError] = useState('');
|
||||||
const [formLoading, setFormLoading] = useState(false);
|
const [formLoading, setFormLoading] = useState(false);
|
||||||
@@ -99,6 +109,16 @@ export const UsersPage: React.FC = () => {
|
|||||||
role: formData.role,
|
role: formData.role,
|
||||||
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
|
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
|
||||||
contractorId: formData.contractorId ? parseInt(formData.contractorId) : 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
|
// Reset form and switch to list
|
||||||
@@ -111,6 +131,15 @@ export const UsersPage: React.FC = () => {
|
|||||||
role: 'Employee',
|
role: 'Employee',
|
||||||
departmentId: '',
|
departmentId: '',
|
||||||
contractorId: '',
|
contractorId: '',
|
||||||
|
isActive: true,
|
||||||
|
phoneNumber: '',
|
||||||
|
aadharNumber: '',
|
||||||
|
bankAccountNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
bankIfsc: '',
|
||||||
|
contractorAgreementNumber: '',
|
||||||
|
pfNumber: '',
|
||||||
|
esicNumber: '',
|
||||||
});
|
});
|
||||||
setActiveTab('list');
|
setActiveTab('list');
|
||||||
refresh();
|
refresh();
|
||||||
@@ -143,6 +172,15 @@ export const UsersPage: React.FC = () => {
|
|||||||
departmentId: user.department_id ? String(user.department_id) : '',
|
departmentId: user.department_id ? String(user.department_id) : '',
|
||||||
contractorId: user.contractor_id ? String(user.contractor_id) : '',
|
contractorId: user.contractor_id ? String(user.contractor_id) : '',
|
||||||
isActive: user.is_active,
|
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);
|
setEditingUserId(user.id);
|
||||||
setActiveTab('edit');
|
setActiveTab('edit');
|
||||||
@@ -166,6 +204,15 @@ export const UsersPage: React.FC = () => {
|
|||||||
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
|
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
|
||||||
contractorId: formData.contractorId ? parseInt(formData.contractorId) : null,
|
contractorId: formData.contractorId ? parseInt(formData.contractorId) : null,
|
||||||
isActive: formData.isActive,
|
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();
|
resetForm();
|
||||||
@@ -189,6 +236,14 @@ export const UsersPage: React.FC = () => {
|
|||||||
departmentId: '',
|
departmentId: '',
|
||||||
contractorId: '',
|
contractorId: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
phoneNumber: '',
|
||||||
|
aadharNumber: '',
|
||||||
|
bankAccountNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
bankIfsc: '',
|
||||||
|
contractorAgreementNumber: '',
|
||||||
|
pfNumber: '',
|
||||||
|
esicNumber: '',
|
||||||
});
|
});
|
||||||
setEditingUserId(null);
|
setEditingUserId(null);
|
||||||
setFormError('');
|
setFormError('');
|
||||||
@@ -329,60 +384,89 @@ export const UsersPage: React.FC = () => {
|
|||||||
<TableHead>EMAIL</TableHead>
|
<TableHead>EMAIL</TableHead>
|
||||||
<TableHead>ROLE</TableHead>
|
<TableHead>ROLE</TableHead>
|
||||||
<TableHead>DEPARTMENT</TableHead>
|
<TableHead>DEPARTMENT</TableHead>
|
||||||
|
<TableHead>REPORTS TO</TableHead>
|
||||||
<TableHead>STATUS</TableHead>
|
<TableHead>STATUS</TableHead>
|
||||||
<TableHead>ACTIONS</TableHead>
|
<TableHead>ACTIONS</TableHead>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredEmployees.map((user) => (
|
{filteredEmployees.map((user) => {
|
||||||
<TableRow key={user.id}>
|
// Find supervisor for contractors (supervisor in same department)
|
||||||
<TableCell>{user.id}</TableCell>
|
const getSupervisorName = () => {
|
||||||
<TableCell className="text-blue-600">{user.username}</TableCell>
|
if (user.role !== 'Contractor') return null;
|
||||||
<TableCell>{user.name}</TableCell>
|
const supervisor = employees.find(
|
||||||
<TableCell>{user.email}</TableCell>
|
e => e.role === 'Supervisor' && e.department_id === user.department_id
|
||||||
<TableCell>
|
);
|
||||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
return supervisor?.name || null;
|
||||||
user.role === 'SuperAdmin' ? 'bg-purple-100 text-purple-700' :
|
};
|
||||||
user.role === 'Supervisor' ? 'bg-blue-100 text-blue-700' :
|
|
||||||
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
|
// Get reports to info based on role
|
||||||
'bg-gray-100 text-gray-700'
|
const getReportsTo = () => {
|
||||||
}`}>
|
if (user.role === 'Employee') {
|
||||||
{user.role}
|
return user.contractor_name ? (
|
||||||
</span>
|
<span className="text-orange-600">{user.contractor_name}</span>
|
||||||
</TableCell>
|
) : '-';
|
||||||
<TableCell>{user.department_name || '-'}</TableCell>
|
}
|
||||||
<TableCell>
|
if (user.role === 'Contractor') {
|
||||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
const supervisorName = getSupervisorName();
|
||||||
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
return supervisorName ? (
|
||||||
}`}>
|
<span className="text-blue-600">{supervisorName}</span>
|
||||||
{user.is_active ? 'Active' : 'Inactive'}
|
) : '-';
|
||||||
</span>
|
}
|
||||||
</TableCell>
|
return '-';
|
||||||
<TableCell>
|
};
|
||||||
{canManageUsers && (
|
|
||||||
<div className="flex gap-2">
|
return (
|
||||||
<Button
|
<TableRow key={user.id}>
|
||||||
variant="ghost"
|
<TableCell>{user.id}</TableCell>
|
||||||
size="sm"
|
<TableCell className="text-blue-600">{user.username}</TableCell>
|
||||||
onClick={() => handleEditUser(user)}
|
<TableCell>{user.name}</TableCell>
|
||||||
className="text-blue-600 hover:text-blue-800"
|
<TableCell>{user.email}</TableCell>
|
||||||
title="Edit"
|
<TableCell>
|
||||||
>
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
<Edit size={14} />
|
user.role === 'SuperAdmin' ? 'bg-purple-100 text-purple-700' :
|
||||||
</Button>
|
user.role === 'Supervisor' ? 'bg-blue-100 text-blue-700' :
|
||||||
<Button
|
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
|
||||||
variant="ghost"
|
'bg-gray-100 text-gray-700'
|
||||||
size="sm"
|
}`}>
|
||||||
onClick={() => handleDeleteUser(user.id, user.username)}
|
{user.role}
|
||||||
className="text-red-600 hover:text-red-800"
|
</span>
|
||||||
title="Delete"
|
</TableCell>
|
||||||
>
|
<TableCell>{user.department_name || '-'}</TableCell>
|
||||||
<Trash2 size={14} />
|
<TableCell>{getReportsTo()}</TableCell>
|
||||||
</Button>
|
<TableCell>
|
||||||
</div>
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
)}
|
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||||
</TableCell>
|
}`}>
|
||||||
</TableRow>
|
{user.is_active ? 'Active' : 'Inactive'}
|
||||||
))}
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{canManageUsers && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditUser(user)}
|
||||||
|
className="text-blue-600 hover:text-blue-800"
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<Edit size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteUser(user.id, user.username)}
|
||||||
|
className="text-red-600 hover:text-red-800"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : !loading && (
|
) : !loading && (
|
||||||
@@ -410,10 +494,9 @@ export const UsersPage: React.FC = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Input
|
<PasswordInput
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
@@ -425,10 +508,9 @@ export const UsersPage: React.FC = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Input
|
<PasswordInput
|
||||||
label="Confirm Password"
|
label="Confirm Password"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
type="password"
|
|
||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
required
|
required
|
||||||
@@ -476,6 +558,82 @@ export const UsersPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Personal & Bank Details - for Employee and Contractor */}
|
||||||
|
{(formData.role === 'Employee' || formData.role === 'Contractor') && (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">Personal Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
||||||
|
<Input
|
||||||
|
label="Phone Number"
|
||||||
|
name="phoneNumber"
|
||||||
|
value={formData.phoneNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., 9876543210"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Aadhar Card Number"
|
||||||
|
name="aadharNumber"
|
||||||
|
value={formData.aadharNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="12-digit Aadhar number"
|
||||||
|
maxLength={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">Bank Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
||||||
|
<Input
|
||||||
|
label="Bank Account Number"
|
||||||
|
name="bankAccountNumber"
|
||||||
|
value={formData.bankAccountNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Bank Name"
|
||||||
|
name="bankName"
|
||||||
|
value={formData.bankName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="IFSC Code"
|
||||||
|
name="bankIfsc"
|
||||||
|
value={formData.bankIfsc}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., SBIN0001234"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contractor-specific fields */}
|
||||||
|
{formData.role === 'Contractor' && (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">Contractor Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
||||||
|
<Input
|
||||||
|
label="Contractor Agreement Number"
|
||||||
|
name="contractorAgreementNumber"
|
||||||
|
value={formData.contractorAgreementNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="PF Number"
|
||||||
|
name="pfNumber"
|
||||||
|
value={formData.pfNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Provident Fund number"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="ESIC Number"
|
||||||
|
name="esicNumber"
|
||||||
|
value={formData.esicNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="ESIC registration number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -599,6 +757,82 @@ export const UsersPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Personal & Bank Details - for Employee and Contractor */}
|
||||||
|
{(formData.role === 'Employee' || formData.role === 'Contractor') && (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">Personal Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
||||||
|
<Input
|
||||||
|
label="Phone Number"
|
||||||
|
name="phoneNumber"
|
||||||
|
value={formData.phoneNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., 9876543210"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Aadhar Card Number"
|
||||||
|
name="aadharNumber"
|
||||||
|
value={formData.aadharNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="12-digit Aadhar number"
|
||||||
|
maxLength={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">Bank Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
||||||
|
<Input
|
||||||
|
label="Bank Account Number"
|
||||||
|
name="bankAccountNumber"
|
||||||
|
value={formData.bankAccountNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Bank Name"
|
||||||
|
name="bankName"
|
||||||
|
value={formData.bankName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="IFSC Code"
|
||||||
|
name="bankIfsc"
|
||||||
|
value={formData.bankIfsc}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., SBIN0001234"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contractor-specific fields */}
|
||||||
|
{formData.role === 'Contractor' && (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">Contractor Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
||||||
|
<Input
|
||||||
|
label="Contractor Agreement Number"
|
||||||
|
name="contractorAgreementNumber"
|
||||||
|
value={formData.contractorAgreementNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="PF Number"
|
||||||
|
name="pfNumber"
|
||||||
|
value={formData.pfNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Provident Fund number"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="ESIC Number"
|
||||||
|
name="esicNumber"
|
||||||
|
value={formData.esicNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="ESIC registration number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -718,49 +952,78 @@ export const UsersPage: React.FC = () => {
|
|||||||
<TableHead>EMAIL</TableHead>
|
<TableHead>EMAIL</TableHead>
|
||||||
<TableHead>ROLE</TableHead>
|
<TableHead>ROLE</TableHead>
|
||||||
<TableHead>DEPARTMENT</TableHead>
|
<TableHead>DEPARTMENT</TableHead>
|
||||||
|
<TableHead>REPORTS TO</TableHead>
|
||||||
<TableHead>STATUS</TableHead>
|
<TableHead>STATUS</TableHead>
|
||||||
<TableHead>ACTION</TableHead>
|
<TableHead>ACTION</TableHead>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{deletableUsers.map((user) => (
|
{deletableUsers.map((user) => {
|
||||||
<TableRow key={user.id} className="hover:bg-red-50">
|
// Find supervisor for contractors (supervisor in same department)
|
||||||
<TableCell>{user.id}</TableCell>
|
const getSupervisorName = () => {
|
||||||
<TableCell className="text-blue-600">{user.username}</TableCell>
|
if (user.role !== 'Contractor') return null;
|
||||||
<TableCell>{user.name}</TableCell>
|
const supervisor = employees.find(
|
||||||
<TableCell>{user.email}</TableCell>
|
e => e.role === 'Supervisor' && e.department_id === user.department_id
|
||||||
<TableCell>
|
);
|
||||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
return supervisor?.name || null;
|
||||||
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
|
};
|
||||||
'bg-gray-100 text-gray-700'
|
|
||||||
}`}>
|
// Get reports to info based on role
|
||||||
{user.role}
|
const getReportsTo = () => {
|
||||||
</span>
|
if (user.role === 'Employee') {
|
||||||
</TableCell>
|
return user.contractor_name ? (
|
||||||
<TableCell>{user.department_name || '-'}</TableCell>
|
<span className="text-orange-600">{user.contractor_name}</span>
|
||||||
<TableCell>
|
) : '-';
|
||||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
}
|
||||||
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
if (user.role === 'Contractor') {
|
||||||
}`}>
|
const supervisorName = getSupervisorName();
|
||||||
{user.is_active ? 'Active' : 'Inactive'}
|
return supervisorName ? (
|
||||||
</span>
|
<span className="text-blue-600">{supervisorName}</span>
|
||||||
</TableCell>
|
) : '-';
|
||||||
<TableCell>
|
}
|
||||||
<Button
|
return '-';
|
||||||
variant="outline"
|
};
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
return (
|
||||||
if (confirm(`Are you sure you want to permanently delete "${user.name}" (${user.username})?\n\nThis action cannot be undone!`)) {
|
<TableRow key={user.id} className="hover:bg-red-50">
|
||||||
deleteEmployee(user.id);
|
<TableCell>{user.id}</TableCell>
|
||||||
}
|
<TableCell className="text-blue-600">{user.username}</TableCell>
|
||||||
}}
|
<TableCell>{user.name}</TableCell>
|
||||||
className="text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400"
|
<TableCell>{user.email}</TableCell>
|
||||||
>
|
<TableCell>
|
||||||
<UserX size={14} className="mr-1" />
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
Delete
|
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
|
||||||
</Button>
|
'bg-gray-100 text-gray-700'
|
||||||
</TableCell>
|
}`}>
|
||||||
</TableRow>
|
{user.role}
|
||||||
))}
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{user.department_name || '-'}</TableCell>
|
||||||
|
<TableCell>{getReportsTo()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||||
|
}`}>
|
||||||
|
{user.is_active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Are you sure you want to permanently delete "${user.name}" (${user.username})?\n\nThis action cannot be undone!`)) {
|
||||||
|
deleteEmployee(user.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400"
|
||||||
|
>
|
||||||
|
<UserX size={14} className="mr-1" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -166,6 +166,57 @@ class ApiService {
|
|||||||
return this.request<any[]>(`/attendance/summary/stats${query ? `?${query}` : ''}`);
|
return this.request<any[]>(`/attendance/summary/stats${query ? `?${query}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateAttendanceStatus(id: number, status: string, remark?: string) {
|
||||||
|
return this.request<any>(`/attendance/${id}/status`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ status, remark }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAbsent(employeeId: number, workDate: string, remark?: string) {
|
||||||
|
return this.request<any>('/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<any[]>(`/employee-swaps${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmployeeSwap(id: number) {
|
||||||
|
return this.request<any>(`/employee-swaps/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createEmployeeSwap(data: {
|
||||||
|
employeeId: number;
|
||||||
|
targetDepartmentId: number;
|
||||||
|
targetContractorId?: number;
|
||||||
|
swapReason: string;
|
||||||
|
reasonDetails?: string;
|
||||||
|
workCompletionPercentage?: number;
|
||||||
|
swapDate: string;
|
||||||
|
}) {
|
||||||
|
return this.request<any>('/employee-swaps', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeEmployeeSwap(id: number) {
|
||||||
|
return this.request<any>(`/employee-swaps/${id}/complete`, {
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelEmployeeSwap(id: number) {
|
||||||
|
return this.request<any>(`/employee-swaps/${id}/cancel`, {
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Contractor Rates
|
// Contractor Rates
|
||||||
async getContractorRates(params?: { contractorId?: number; subDepartmentId?: number }) {
|
async getContractorRates(params?: { contractorId?: number; subDepartmentId?: number }) {
|
||||||
const query = params ? new URLSearchParams(params as any).toString() : '';
|
const query = params ? new URLSearchParams(params as any).toString() : '';
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ export interface User {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
department_name?: string;
|
department_name?: string;
|
||||||
contractor_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 {
|
export interface Department {
|
||||||
@@ -49,6 +61,8 @@ export interface WorkAllocation {
|
|||||||
department_name?: string;
|
department_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AttendanceStatus = 'CheckedIn' | 'CheckedOut' | 'Absent' | 'HalfDay' | 'Late';
|
||||||
|
|
||||||
export interface Attendance {
|
export interface Attendance {
|
||||||
id: number;
|
id: number;
|
||||||
employee_id: number;
|
employee_id: number;
|
||||||
@@ -56,7 +70,8 @@ export interface Attendance {
|
|||||||
check_in_time: string;
|
check_in_time: string;
|
||||||
check_out_time?: string;
|
check_out_time?: string;
|
||||||
work_date: string;
|
work_date: string;
|
||||||
status: 'CheckedIn' | 'CheckedOut';
|
status: AttendanceStatus;
|
||||||
|
remark?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
employee_name?: string;
|
employee_name?: string;
|
||||||
@@ -66,6 +81,33 @@ export interface Attendance {
|
|||||||
contractor_name?: string;
|
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 {
|
export interface ContractorRate {
|
||||||
id: number;
|
id: number;
|
||||||
contractor_id: number;
|
contractor_id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user