(Feat-Fix): New Reporting system, more seeded data, fixed subdepartments and activity inversion, login page changes, etc etc

This commit is contained in:
2025-12-18 08:15:31 +00:00
parent 916ee19677
commit ac29bb2882
24 changed files with 3306 additions and 207 deletions

View File

@@ -61,7 +61,7 @@ npm run dev
### Docker Management
```bash
# Stop MySQL
# Stop MySQLclear
docker-compose down
# Stop and remove all data

82
activities.md Normal file
View File

@@ -0,0 +1,82 @@
# Activity Departments and Units of Measurement
## GROUNDNUT Department
| # | Activity Name | Sub-Department | Unit of Measurement |
|---|---------------|----------------|---------------------|
| 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag |
| 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag |
| 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag |
| 4 | Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack) | Loading/Unloading | Per Bag |
| 5 | Guthali dhada Pala Tulai Silai Dhaang / Loading (Kernel Heap Weighing Stitching Stack/Loading) | Loading/Unloading | Per Bag |
| 6 | Mufali Patthar Bori silai Dhaang (Groundnut Stone Bag Stitching Stack) | Loading/Unloading | Per Bag |
| 7 | Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading) | Loading/Unloading | Per Bag |
| 8 | Bardana Bandal Loading (Gunny Bundle Loading) | Loading/Unloading | Per Bag |
| 9 | Bardana Gatthi Loading/Unloading (Gunny Bale Loading/Unloading) | Loading/Unloading | Per Bag |
| 10 | Black Dana Loading/Unloading | Loading/Unloading | Per Bag |
| 11 | Pre Cleaner | Pre Cleaning | Fixed Rate-Per Person |
| 12 | Destoner | Destoner | Fixed Rate-Per Person |
| 13 | Water | Water | Fixed Rate-Per Person |
| 14 | Decordicater | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
| 15 | Round Chalna (Round Sieving) | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
| 16 | Cleaning | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
| 17 | Round Chalna No.1 (Round Sieving No.1) | Round Chalna No.1 | Fixed Rate-Per Person |
| 18 | Dala - Chomu & Jaipur (Branch - Chomu & Jaipur) | Loading/Unloading | Per Bag |
## DANA Department
| # | Activity Name | Sub-Department | Unit of Measurement |
|---|---------------|----------------|---------------------|
| 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag |
| 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag |
| 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag |
| 4 | Tulai Silai Dhaang Ikai No. 2 Machine ke Pass (Weighing Stitching Stack Unit No. 2 Near Machine) | Loading/Unloading | Per Bag |
| 5 | Dana Unloading/Dhaang (Grain Unloading/Stack) | Loading/Unloading | Per Bag |
| 6 | Dana Aavak Keep Katai (Grain Arrival Hopper Cutting) | Loading/Unloading | Per Bag |
| 7 | Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg (Waste Heap Filling Weighing Stitching Load/Stack 70kg) | Loading/Unloading | Per Bag |
| 8 | Kachri Dhaang se loading (Waste Loading from Stack) | Loading/Unloading | Per Bag |
| 9 | Keep Katai Khulla Katta (Hopper Cutting Open Bag) | Loading/Unloading | Per Bag |
| 10 | Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched) | Loading/Unloading | Per Bag |
| 11 | Bardana Paltai (Gunny Turning) | Loading/Unloading | Per Bag |
| 12 | Ekai No. 2 me Keep Katai Khula Bag (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Open Bag with Dragging into Tank) | Loading/Unloading | Per Bag |
| 13 | Ekai No. 2 me Keep Katai Silai Kholkar (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Opening Stitched with Dragging into Tank) | Loading/Unloading | Per Bag |
| 14 | Silai Loading Company Gadi Dala Sahit (Stitching Loading Company Vehicle with Branch) | Loading/Unloading | Per Bag |
| 15 | Kachri Bharai Silai Dhaang Chatt Par (Waste Filling Stitching Stack on Roof) | Loading/Unloading | Per Bag |
| 16 | Bardana Unloading (Gunny Unloading) | Loading/Unloading | Per Bag |
| 17 | Grading | Loading/Unloading | Per Bag |
| 18 | Destoner | Destoner | Fixed Rate-Per Person |
| 19 | Gravity | Gravity | Fixed Rate-Per Person |
| 20 | Tank | Tank | Fixed Rate-Per Person |
| 21 | Sortex | Sortex | Fixed Rate-Per Person |
| 22 | X-Ray | X-Ray | Fixed Rate-Per Person |
| 23 | Kachri (Waste) | Kachri | Fixed Rate-Per Person |
| 24 | Other Works | Other Works | Fixed Rate-Per Person |
## TUKDI Department
| # | Activity Name | Sub-Department | Unit of Measurement |
|---|---------------|----------------|---------------------|
| 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag |
| 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag |
| 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |
| 4 | Grading Chalne se Maal Bharai Tulai Silai Loading (Grading Running Material Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag |
| 5 | Chilka Bharai silai Dhaang (Husk Filling Stitching Stack) | Loading/Unloading | Per Bag |
| 6 | Keep katai Bahar Se (Hopper Cutting from Outside) | Loading/Unloading | Per Bag |
| 7 | Keep katai Andar Se (Hopper Cutting from Inside) | Loading/Unloading | Per Bag |
| 8 | Cartoon Banai Vacume Bharai Tulai Packing and Dhaang (Carton Making Vacuum Filling Weighing Packing and Stack) | Loading/Unloading | Per Bag |
| 9 | Cartoon Banai Vacume Bharai Tulai Packing and Loading (Carton Making Vacuum Filling Weighing Packing and Loading) | Loading/Unloading | Per Bag |
| 10 | Katta Paltai (Bag Turning) | Loading/Unloading | Per Bag |
| 11 | Dhada Pala Bharai Tulai Silai Dhaang (Heap Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |
| 12 | Dhada Pala Bharai Tulai Silai Loading (Heap Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag |
| 13 | Sike Maal Ki Silai Dhaang Andar (Roasted Material Stitching Stack Inside) | Loading/Unloading | Per Bag |
| 14 | Sike Maal Ki Silai Dhaang Bahar (Roasted Material Stitching Stack Outside) | Loading/Unloading | Per Bag |
| 15 | Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside) | Loading/Unloading | Per Bag |
| 16 | Tank | Tank | Fixed Rate-Per Person |
| 17 | Grader (Machine) | Grader (Machine) | Fixed Rate-Per Person |
| 18 | Sortex | Sortex | Fixed Rate-Per Person |
| 19 | X-Ray | X-Ray | Fixed Rate-Per Person |
| 20 | Rejection | Rejection | Fixed Rate-Per Person |
| 21 | Store | Store | Fixed Rate-Per Person |
| 22 | Roster | Roster | Fixed Rate-Per Person |
| 23 | Blancher | Blancher | Fixed Rate-Per Person |
| 24 | Other Works | Other Works | Fixed Rate-Per Person |

View File

@@ -3,7 +3,7 @@ DB_HOST=localhost
DB_USER=root
DB_PASSWORD=admin123
DB_NAME=work_allocation
DB_PORT=3306
DB_PORT=3307
# JWT Configuration - CHANGE IN PRODUCTION!
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024

View File

@@ -11,6 +11,9 @@ import workAllocationRoutes from "./routes/work-allocations.ts";
import attendanceRoutes from "./routes/attendance.ts";
import contractorRateRoutes from "./routes/contractor-rates.ts";
import employeeSwapRoutes from "./routes/employee-swaps.ts";
import reportRoutes from "./routes/reports.ts";
import standardRateRoutes from "./routes/standard-rates.ts";
import activityRoutes from "./routes/activities.ts";
// Initialize database connection
await db.connect();
@@ -63,6 +66,9 @@ router.use("/api/work-allocations", workAllocationRoutes.routes(), workAllocatio
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods());
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods());
router.use("/api/employee-swaps", employeeSwapRoutes.routes(), employeeSwapRoutes.allowedMethods());
router.use("/api/reports", reportRoutes.routes(), reportRoutes.allowedMethods());
router.use("/api/standard-rates", standardRateRoutes.routes(), standardRateRoutes.allowedMethods());
router.use("/api/activities", activityRoutes.routes(), activityRoutes.allowedMethods());
// Apply routes
app.use(router.routes());

View File

@@ -0,0 +1,153 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken } from "../middleware/auth.ts";
const router = new Router();
interface Activity {
id: number;
sub_department_id: number;
name: string;
unit_of_measurement: string;
created_at: string;
sub_department_name?: string;
department_id?: number;
department_name?: string;
}
// Get all activities (with optional filters)
router.get("/", authenticateToken, async (ctx) => {
try {
const params = ctx.request.url.searchParams;
const subDepartmentId = params.get("subDepartmentId");
const departmentId = params.get("departmentId");
let query = `
SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
sd.name as sub_department_name,
sd.department_id,
d.name as department_name
FROM activities a
JOIN sub_departments sd ON a.sub_department_id = sd.id
JOIN departments d ON sd.department_id = d.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
if (subDepartmentId) {
query += " AND a.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
if (departmentId) {
query += " AND sd.department_id = ?";
queryParams.push(departmentId);
}
query += " ORDER BY d.name, sd.name, a.name";
const activities = await db.query<Activity[]>(query, queryParams);
ctx.response.body = activities;
} catch (error) {
console.error("Get activities error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get activity by ID
router.get("/:id", authenticateToken, async (ctx) => {
try {
const activityId = ctx.params.id;
const activities = await db.query<Activity[]>(
`SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
sd.name as sub_department_name,
sd.department_id,
d.name as department_name
FROM activities a
JOIN sub_departments sd ON a.sub_department_id = sd.id
JOIN departments d ON sd.department_id = d.id
WHERE a.id = ?`,
[activityId]
);
if (activities.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Activity not found" };
return;
}
ctx.response.body = activities[0];
} catch (error) {
console.error("Get activity error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Create activity (SuperAdmin only)
router.post("/", authenticateToken, async (ctx) => {
try {
const body = await ctx.request.body.json();
const { sub_department_id, name, unit_of_measurement } = body;
if (!sub_department_id || !name) {
ctx.response.status = 400;
ctx.response.body = { error: "Sub-department ID and name are required" };
return;
}
const result = await db.execute(
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
[sub_department_id, name, unit_of_measurement || "Per Bag"]
);
ctx.response.status = 201;
ctx.response.body = {
id: result.lastInsertId,
message: "Activity created successfully"
};
} catch (error) {
console.error("Create activity error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Update activity
router.put("/:id", authenticateToken, async (ctx) => {
try {
const activityId = ctx.params.id;
const body = await ctx.request.body.json();
const { name, unit_of_measurement } = body;
await db.execute(
"UPDATE activities SET name = ?, unit_of_measurement = ? WHERE id = ?",
[name, unit_of_measurement, activityId]
);
ctx.response.body = { message: "Activity updated successfully" };
} catch (error) {
console.error("Update activity error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Delete activity
router.delete("/:id", authenticateToken, async (ctx) => {
try {
const activityId = ctx.params.id;
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
ctx.response.body = { message: "Activity deleted successfully" };
} catch (error) {
console.error("Delete activity error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,183 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import type { WorkAllocation } from "../types/index.ts";
const router = new Router();
// Get completed work allocations for reporting (with optional filters)
router.get("/completed-allocations", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const startDate = params.get("startDate");
const endDate = params.get("endDate");
const departmentId = params.get("departmentId");
const contractorId = params.get("contractorId");
const employeeId = params.get("employeeId");
let query = `
SELECT wa.*,
e.name as employee_name, e.username as employee_username,
e.phone_number as employee_phone,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name,
d.id as department_id
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.status = 'Completed'
`;
const queryParams: unknown[] = [];
// Role-based filtering - Supervisors can only see their department
if (currentUser.role === "Supervisor") {
query += " AND e.department_id = ?";
queryParams.push(currentUser.departmentId);
}
// Date range filter
if (startDate) {
query += " AND wa.completion_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND wa.completion_date <= ?";
queryParams.push(endDate);
}
// Department filter (for SuperAdmin)
if (departmentId && currentUser.role === "SuperAdmin") {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
// Contractor filter
if (contractorId) {
query += " AND wa.contractor_id = ?";
queryParams.push(contractorId);
}
// Employee filter
if (employeeId) {
query += " AND wa.employee_id = ?";
queryParams.push(employeeId);
}
query += " ORDER BY wa.completion_date DESC, wa.created_at DESC";
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
// Calculate summary stats
const totalAllocations = allocations.length;
const totalAmount = allocations.reduce((sum, a) => sum + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0), 0);
const totalUnits = allocations.reduce((sum, a) => sum + (parseFloat(String(a.units)) || 0), 0);
ctx.response.body = {
allocations,
summary: {
totalAllocations,
totalAmount: totalAmount.toFixed(2),
totalUnits: totalUnits.toFixed(2),
}
};
} catch (error) {
console.error("Get completed allocations report error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get summary statistics for completed work
router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const startDate = params.get("startDate");
const endDate = params.get("endDate");
let departmentFilter = "";
const queryParams: unknown[] = [];
if (currentUser.role === "Supervisor") {
departmentFilter = " AND e.department_id = ?";
queryParams.push(currentUser.departmentId);
}
let dateFilter = "";
if (startDate) {
dateFilter += " AND wa.completion_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
dateFilter += " AND wa.completion_date <= ?";
queryParams.push(endDate);
}
// Get summary by contractor
const byContractor = await db.query<any[]>(`
SELECT
c.id as contractor_id,
c.name as contractor_name,
COUNT(*) as total_allocations,
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
SUM(COALESCE(wa.units, 0)) as total_units
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users c ON wa.contractor_id = c.id
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
GROUP BY c.id, c.name
ORDER BY total_amount DESC
`, queryParams);
// Get summary by sub-department
const bySubDepartment = await db.query<any[]>(`
SELECT
sd.id as sub_department_id,
sd.name as sub_department_name,
d.name as department_name,
COUNT(*) as total_allocations,
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
SUM(COALESCE(wa.units, 0)) as total_units
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
GROUP BY sd.id, sd.name, d.name
ORDER BY total_amount DESC
`, queryParams);
// Get summary by activity type
const byActivity = await db.query<any[]>(`
SELECT
COALESCE(wa.activity, 'Standard') as activity,
COUNT(*) as total_allocations,
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
SUM(COALESCE(wa.units, 0)) as total_units
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
GROUP BY wa.activity
ORDER BY total_amount DESC
`, queryParams);
ctx.response.body = {
byContractor,
bySubDepartment,
byActivity,
};
} catch (error) {
console.error("Get report summary error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,479 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
const router = new Router();
// Standard Rate interface
interface StandardRate {
id: number;
sub_department_id: number | null;
activity: string | null;
rate: number;
effective_date: Date;
created_by: number;
created_at: Date;
sub_department_name?: string;
department_name?: string;
department_id?: number;
created_by_name?: string;
}
// Get all standard rates (default rates for comparison)
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const departmentId = params.get("departmentId");
const subDepartmentId = params.get("subDepartmentId");
const activity = params.get("activity");
let query = `
SELECT sr.*,
sd.name as sub_department_name,
d.name as department_name,
d.id as department_id,
u.name as created_by_name
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Supervisors can only see rates for their department
if (currentUser.role === "Supervisor") {
query += " AND d.id = ?";
queryParams.push(currentUser.departmentId);
}
if (departmentId) {
query += " AND d.id = ?";
queryParams.push(departmentId);
}
if (subDepartmentId) {
query += " AND sr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
if (activity) {
query += " AND sr.activity = ?";
queryParams.push(activity);
}
query += " ORDER BY sr.effective_date DESC, sr.created_at DESC";
const rates = await db.query<StandardRate[]>(query, queryParams);
ctx.response.body = rates;
} catch (error) {
console.error("Get standard rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date
router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const params = ctx.request.url.searchParams;
const departmentId = params.get("departmentId");
const startDate = params.get("startDate");
const endDate = params.get("endDate");
// Get contractor rates
let contractorQuery = `
SELECT
cr.id,
'contractor' as rate_type,
cr.contractor_id,
u.name as contractor_name,
cr.sub_department_id,
sd.name as sub_department_name,
d.id as department_id,
d.name as department_name,
cr.activity,
cr.rate,
cr.effective_date,
cr.created_at,
NULL as created_by,
NULL as created_by_name
FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE 1=1
`;
const contractorParams: unknown[] = [];
if (departmentId) {
contractorQuery += " AND d.id = ?";
contractorParams.push(departmentId);
}
if (startDate) {
contractorQuery += " AND cr.effective_date >= ?";
contractorParams.push(startDate);
}
if (endDate) {
contractorQuery += " AND cr.effective_date <= ?";
contractorParams.push(endDate);
}
// Get standard rates
let standardQuery = `
SELECT
sr.id,
'standard' as rate_type,
NULL as contractor_id,
NULL as contractor_name,
sr.sub_department_id,
sd.name as sub_department_name,
d.id as department_id,
d.name as department_name,
sr.activity,
sr.rate,
sr.effective_date,
sr.created_at,
sr.created_by,
u.name as created_by_name
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.id
WHERE 1=1
`;
const standardParams: unknown[] = [];
if (departmentId) {
standardQuery += " AND d.id = ?";
standardParams.push(departmentId);
}
if (startDate) {
standardQuery += " AND sr.effective_date >= ?";
standardParams.push(startDate);
}
if (endDate) {
standardQuery += " AND sr.effective_date <= ?";
standardParams.push(endDate);
}
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
const standardRates = await db.query<any[]>(standardQuery, standardParams);
// Combine and sort by date
const allRates = [...contractorRates, ...standardRates].sort((a, b) => {
const dateA = new Date(a.effective_date).getTime();
const dateB = new Date(b.effective_date).getTime();
return dateB - dateA; // Descending order
});
ctx.response.body = {
allRates,
summary: {
totalContractorRates: contractorRates.length,
totalStandardRates: standardRates.length,
totalRates: allRates.length,
}
};
} catch (error) {
console.error("Get all rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Compare contractor rates with standard rates
router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const contractorId = params.get("contractorId");
const subDepartmentId = params.get("subDepartmentId");
let departmentFilter = "";
const queryParams: unknown[] = [];
if (currentUser.role === "Supervisor") {
departmentFilter = " AND d.id = ?";
queryParams.push(currentUser.departmentId);
}
// Get standard rates
let standardQuery = `
SELECT sr.*,
sd.name as sub_department_name,
d.name as department_name,
d.id as department_id
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE 1=1 ${departmentFilter}
`;
if (subDepartmentId) {
standardQuery += " AND sr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
standardQuery += " ORDER BY sr.effective_date DESC";
const standardRates = await db.query<StandardRate[]>(standardQuery, queryParams);
// Get contractor rates for comparison
let contractorQuery = `
SELECT cr.*,
u.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name,
d.id as department_id
FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE 1=1
`;
const contractorParams: unknown[] = [];
if (currentUser.role === "Supervisor") {
contractorQuery += " AND d.id = ?";
contractorParams.push(currentUser.departmentId);
}
if (contractorId) {
contractorQuery += " AND cr.contractor_id = ?";
contractorParams.push(contractorId);
}
if (subDepartmentId) {
contractorQuery += " AND cr.sub_department_id = ?";
contractorParams.push(subDepartmentId);
}
contractorQuery += " ORDER BY cr.effective_date DESC";
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
// Build comparison data
const comparisons = contractorRates.map(cr => {
// Find matching standard rate
const matchingStandard = standardRates.find(sr =>
sr.sub_department_id === cr.sub_department_id &&
sr.activity === cr.activity
);
const standardRate = matchingStandard?.rate || 0;
const contractorRate = cr.rate || 0;
const difference = contractorRate - standardRate;
const percentageDiff = standardRate > 0 ? ((difference / standardRate) * 100).toFixed(2) : null;
return {
...cr,
standard_rate: standardRate,
difference,
percentage_difference: percentageDiff,
is_above_standard: difference > 0,
is_below_standard: difference < 0,
};
});
ctx.response.body = {
standardRates,
contractorRates,
comparisons,
};
} catch (error) {
console.error("Compare rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Create standard rate (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as {
subDepartmentId?: number;
activity?: string;
rate: number;
effectiveDate: string;
};
const { subDepartmentId, activity, rate, effectiveDate } = body;
if (!rate || !effectiveDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields (rate, effectiveDate)" };
return;
}
// Verify sub-department belongs to supervisor's department if supervisor
if (subDepartmentId && currentUser.role === "Supervisor") {
const subDepts = await db.query<any[]>(
"SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?",
[subDepartmentId, currentUser.departmentId]
);
if (subDepts.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Sub-department not in your department" };
return;
}
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const result = await db.execute(
"INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)",
[subDepartmentId || null, sanitizedActivity, rate, effectiveDate, currentUser.id]
);
const newRate = await db.query<StandardRate[]>(
`SELECT sr.*,
sd.name as sub_department_name,
d.name as department_name,
u.name as created_by_name
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.id
WHERE sr.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newRate[0];
} catch (error) {
console.error("Create standard rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Update standard rate
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const rateId = ctx.params.id;
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
const { rate, activity, effectiveDate } = body;
// Verify rate exists and user has access
let query = `
SELECT sr.*, d.id as department_id
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE sr.id = ?
`;
const params: unknown[] = [rateId];
const existing = await db.query<any[]>(query, params);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Standard rate not found" };
return;
}
// Supervisors can only update rates in their department
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Access denied - rate not in your department" };
return;
}
const updates: string[] = [];
const updateParams: unknown[] = [];
if (rate !== undefined) {
updates.push("rate = ?");
updateParams.push(rate);
}
if (activity !== undefined) {
updates.push("activity = ?");
updateParams.push(sanitizeInput(activity));
}
if (effectiveDate !== undefined) {
updates.push("effective_date = ?");
updateParams.push(effectiveDate);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
updateParams.push(rateId);
await db.execute(
`UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`,
updateParams
);
const updatedRate = await db.query<StandardRate[]>(
`SELECT sr.*,
sd.name as sub_department_name,
d.name as department_name,
u.name as created_by_name
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.id
WHERE sr.id = ?`,
[rateId]
);
ctx.response.body = updatedRate[0];
} catch (error) {
console.error("Update standard rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Delete standard rate
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const rateId = ctx.params.id;
// Verify rate exists and user has access
const existing = await db.query<any[]>(
`SELECT sr.*, d.id as department_id
FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE sr.id = ?`,
[rateId]
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Standard rate not found" };
return;
}
// Supervisors can only delete rates in their department
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Access denied - rate not in your department" };
return;
}
await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]);
ctx.response.body = { message: "Standard rate deleted successfully" };
} catch (error) {
console.error("Delete standard rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -32,54 +32,187 @@ async function seedDatabase() {
console.log(" Departments already exist");
}
// 2. Seed Sub-departments for Groundnut
console.log("📂 Seeding sub-departments...");
const groundnutDept = await db.query<{ id: number }[]>(
// 2. Seed Sub-departments and Activities for all departments
console.log("📂 Seeding sub-departments and activities...");
// Get department IDs
const tudkiDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Tudki"]
);
const danaDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Dana"]
);
const groundnutDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Groundnut"]
);
let groundnutId: number | null = null;
const tudkiId = tudkiDeptResult[0]?.id;
const danaId = danaDeptResult[0]?.id;
const groundnutId = groundnutDeptResult[0]?.id;
if (groundnutDept.length > 0) {
groundnutId = groundnutDept[0].id;
// Define sub-departments and activities per department based on activities.md
const departmentData: { [key: number]: { subDept: string; activities: { name: string; unit: string }[] }[] } = {};
if (groundnutId) {
departmentData[groundnutId] = [
{
subDept: "Loading/Unloading",
activities: [
{ name: "Mufali Aavak Katai (Groundnut Arrival Cutting)", unit: "Per Bag" },
{ name: "Mufali Aavak Dhaang (Groundnut Arrival Stacking)", unit: "Per Bag" },
{ name: "Dhaang Se Katai (Cutting from Stack)", unit: "Per Bag" },
{ name: "Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack)", unit: "Per Bag" },
{ name: "Guthali dhada Pala Tulai Silai Dhaang / Loading", unit: "Per Bag" },
{ name: "Mufali Patthar Bori silai Dhaang", unit: "Per Bag" },
{ name: "Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading)", unit: "Per Bag" },
{ name: "Bardana Bandal Loading (Gunny Bundle Loading)", unit: "Per Bag" },
{ name: "Bardana Gatthi Loading/Unloading", unit: "Per Bag" },
{ name: "Black Dana Loading/Unloading", unit: "Per Bag" },
{ name: "Dala - Chomu & Jaipur (Branch)", unit: "Per Bag" },
]
},
{ subDept: "Pre Cleaning", activities: [{ name: "Pre Cleaner", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Water", activities: [{ name: "Water", unit: "Fixed Rate-Per Person" }] },
{
subDept: "Decordicater & Cleaning and Round Chalna",
activities: [
{ name: "Decordicater", unit: "Fixed Rate-Per Person" },
{ name: "Round Chalna (Round Sieving)", unit: "Fixed Rate-Per Person" },
{ name: "Cleaning", unit: "Fixed Rate-Per Person" },
]
},
{ subDept: "Round Chalna No.1", activities: [{ name: "Round Chalna No.1 (Round Sieving No.1)", unit: "Fixed Rate-Per Person" }] },
];
}
if (danaId) {
departmentData[danaId] = [
{
subDept: "Loading/Unloading",
activities: [
{ name: "Tulai Silai Loading (Weighing Stitching Loading)", unit: "Per Bag" },
{ name: "Dhaang se Loading (Loading from Stack)", unit: "Per Bag" },
{ name: "Silai Dhaang (Stitching Stack)", unit: "Per Bag" },
{ name: "Tulai Silai Dhaang Ikai No. 2 Machine ke Pass", unit: "Per Bag" },
{ name: "Dana Unloading/Dhaang (Grain Unloading/Stack)", unit: "Per Bag" },
{ name: "Dana Aavak Keep Katai (Grain Arrival Hopper Cutting)", unit: "Per Bag" },
{ name: "Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg", unit: "Per Bag" },
{ name: "Kachri Dhaang se loading (Waste Loading from Stack)", unit: "Per Bag" },
{ name: "Keep Katai Khulla Katta (Hopper Cutting Open Bag)", unit: "Per Bag" },
{ name: "Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched)", unit: "Per Bag" },
{ name: "Bardana Paltai (Gunny Turning)", unit: "Per Bag" },
{ name: "Ekai No. 2 me Keep Katai Khula Bag", unit: "Per Bag" },
{ name: "Ekai No. 2 me Keep Katai Silai Kholkar", unit: "Per Bag" },
{ name: "Silai Loading Company Gadi Dala Sahit", unit: "Per Bag" },
{ name: "Kachri Bharai Silai Dhaang Chatt Par", unit: "Per Bag" },
{ name: "Bardana Unloading (Gunny Unloading)", unit: "Per Bag" },
{ name: "Grading", unit: "Per Bag" },
]
},
{ subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Gravity", activities: [{ name: "Gravity", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] },
{ subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Kachri", activities: [{ name: "Kachri (Waste)", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] },
];
}
if (tudkiId) {
departmentData[tudkiId] = [
{
subDept: "Loading/Unloading",
activities: [
{ name: "Dana Loading/Unloading (Grain Loading/Unloading)", unit: "Per Bag" },
{ name: "Loading/Unloading 40 Kg", unit: "Per Bag" },
{ name: "Grading Chalne se Maal Bharai Tulai Silai Dhaang", unit: "Per Bag" },
{ name: "Grading Chalne se Maal Bharai Tulai Silai Loading", unit: "Per Bag" },
{ name: "Chilka Bharai silai Dhaang (Husk Filling Stitching Stack)", unit: "Per Bag" },
{ name: "Keep katai Bahar Se (Hopper Cutting from Outside)", unit: "Per Bag" },
{ name: "Keep katai Andar Se (Hopper Cutting from Inside)", unit: "Per Bag" },
{ name: "Cartoon Banai Vacume Bharai Tulai Packing and Dhaang", unit: "Per Bag" },
{ name: "Cartoon Banai Vacume Bharai Tulai Packing and Loading", unit: "Per Bag" },
{ name: "Katta Paltai (Bag Turning)", unit: "Per Bag" },
{ name: "Dhada Pala Bharai Tulai Silai Dhaang", unit: "Per Bag" },
{ name: "Dhada Pala Bharai Tulai Silai Loading", unit: "Per Bag" },
{ name: "Sike Maal Ki Silai Dhaang Andar", unit: "Per Bag" },
{ name: "Sike Maal Ki Silai Dhaang Bahar", unit: "Per Bag" },
{ name: "Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside)", unit: "Per Bag" },
]
},
{ subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Grader (Machine)", activities: [{ name: "Grader (Machine)", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] },
{ subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Rejection", activities: [{ name: "Rejection", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Store", activities: [{ name: "Store", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Roster", activities: [{ name: "Roster", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Blancher", activities: [{ name: "Blancher", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] },
];
}
// Check if activities table exists, if not create it
try {
await db.execute(`
CREATE TABLE IF NOT EXISTS activities (
id INT AUTO_INCREMENT PRIMARY KEY,
sub_department_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
unit_of_measurement ENUM('Per Bag', 'Fixed Rate-Per Person') NOT NULL DEFAULT 'Per Bag',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE CASCADE,
UNIQUE KEY unique_activity (sub_department_id, name)
)
`);
} catch (_e) {
// Table might already exist
}
// Seed sub-departments and activities for each department
for (const [deptId, subDepts] of Object.entries(departmentData)) {
const existingSubDepts = await db.query<{ count: number }[]>(
"SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?",
[groundnutId]
[deptId]
);
if (existingSubDepts[0].count === 0) {
const subDepts = [
"Mufali Aavak Katai",
"Mufali Aavak Dhang",
"Dhang Se Katai",
"Guthli Bori Silai Dhang",
"Guthali dada Pala Tulai Silai Dhang",
"Mufali Patthar Bori silai dhang",
"Mufali Patthar Bori Utrai",
"Bardana Bandal Loading Unloading",
"Bardana Gatthi Loading",
"Black Dana Loading/Unloading",
"Pre Cleaning",
"Destoner",
"Water",
"Decordicater",
"Round Chalna",
"Cleaning",
"Round Chalna No.1"
];
for (const name of subDepts) {
for (const { subDept, activities } of subDepts) {
// Insert sub-department
await db.execute(
"INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)",
[groundnutId, name, "Loading/Unloading"]
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[deptId, subDept]
);
// Get the sub-department ID
const subDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM sub_departments WHERE department_id = ? AND name = ?",
[deptId, subDept]
);
if (subDeptResult.length > 0) {
const subDeptId = subDeptResult[0].id;
// Insert activities for this sub-department
for (const activity of activities) {
try {
await db.execute(
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
[subDeptId, activity.name, activity.unit]
);
} catch (_e) {
// Activity might already exist
}
}
}
}
console.log(" ✅ Sub-departments created");
} else {
console.log(" Sub-departments already exist");
}
}
console.log(" ✅ Sub-departments and activities created");
// 3. Seed SuperAdmin
console.log("👤 Seeding SuperAdmin user...");
@@ -104,23 +237,32 @@ async function seedDatabase() {
console.log(" ✅ SuperAdmin created");
}
// 4. Seed Sample Supervisors
console.log("👥 Seeding sample supervisors...");
const tudkiDept = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Tudki"]
);
const danaDept = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Dana"]
);
// 4. Seed Supervisors for all departments
console.log("👥 Seeding supervisors...");
const supervisorPassword = await hashPassword("supervisor123");
const supervisors = [
{ username: "supervisor_tudki", name: "Tudki Supervisor", email: "supervisor.tudki@workallocate.com", deptId: tudkiDept[0]?.id },
{ username: "supervisor_dana", name: "Dana Supervisor", email: "supervisor.dana@workallocate.com", deptId: danaDept[0]?.id },
{ username: "supervisor_groundnut", name: "Groundnut Supervisor", email: "supervisor.groundnut@workallocate.com", deptId: groundnutId }
{
username: "rajesh.sharma.tudki",
name: "Rajesh Sharma",
email: "rajesh.sharma@workallocate.com",
deptId: tudkiId,
phone: "9414567890"
},
{
username: "sunil.verma.dana",
name: "Sunil Verma",
email: "sunil.verma@workallocate.com",
deptId: danaId,
phone: "9414567891"
},
{
username: "mahesh.agarwal.groundnut",
name: "Mahesh Agarwal",
email: "mahesh.agarwal@workallocate.com",
deptId: groundnutId,
phone: "9414567892"
}
];
for (const sup of supervisors) {
@@ -131,8 +273,8 @@ async function seedDatabase() {
);
if (existing.length === 0) {
await db.execute(
"INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
[sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true]
"INSERT INTO users (username, name, email, password, role, department_id, is_active, phone_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true, sup.phone]
);
console.log(`${sup.name} created`);
} else {
@@ -141,38 +283,97 @@ async function seedDatabase() {
}
}
// 5. Seed Sample Contractors
console.log("🏗️ Seeding sample contractors...");
// 5. Seed Contractors for all departments
console.log("🏗️ Seeding contractors...");
const contractorPassword = await hashPassword("contractor123");
const contractors = [
// Groundnut Department Contractors
{
username: "contractor1",
name: "Contractor One",
email: "contractor1@workallocate.com",
username: "ramesh.patel.gn",
name: "Ramesh Patel",
email: "ramesh.patel@workallocate.com",
deptId: groundnutId,
phone: "9876543210",
aadhar: "123456789012",
bankAccount: "1234567890123456",
bankName: "State Bank of India",
bankIfsc: "SBIN0001234",
agreementNo: "AGR-2024-001",
pfNo: "PF/GJ/12345/67890",
esicNo: "12-34-567890-123-0001"
},
{
username: "contractor2",
name: "Contractor Two",
email: "contractor2@workallocate.com",
deptId: groundnutId,
phone: "9876543211",
aadhar: "234567890123",
bankAccount: "2345678901234567",
phone: "9829012345",
aadhar: "234567891234",
bankAccount: "50100123456789",
bankName: "HDFC Bank",
bankIfsc: "HDFC0001234",
agreementNo: "AGR-2024-002",
pfNo: "PF/GJ/12345/67891",
esicNo: "12-34-567890-123-0002"
agreementNo: "AGR-GN-2024-001",
pfNo: "RJ/JPR/12345/001",
esicNo: "12-34-567890-001-0001"
},
{
username: "kishan.meena.gn",
name: "Kishan Meena",
email: "kishan.meena@workallocate.com",
deptId: groundnutId,
phone: "9829012346",
aadhar: "345678912345",
bankAccount: "50100123456790",
bankName: "State Bank of India",
bankIfsc: "SBIN0005678",
agreementNo: "AGR-GN-2024-002",
pfNo: "RJ/JPR/12345/002",
esicNo: "12-34-567890-001-0002"
},
// Dana Department Contractors
{
username: "gopal.sharma.dana",
name: "Gopal Sharma",
email: "gopal.sharma@workallocate.com",
deptId: danaId,
phone: "9829012347",
aadhar: "456789123456",
bankAccount: "50100123456791",
bankName: "Punjab National Bank",
bankIfsc: "PUNB0009876",
agreementNo: "AGR-DN-2024-001",
pfNo: "RJ/JPR/12345/003",
esicNo: "12-34-567890-002-0001"
},
{
username: "mohan.yadav.dana",
name: "Mohan Yadav",
email: "mohan.yadav@workallocate.com",
deptId: danaId,
phone: "9829012348",
aadhar: "567891234567",
bankAccount: "50100123456792",
bankName: "Bank of Baroda",
bankIfsc: "BARB0004567",
agreementNo: "AGR-DN-2024-002",
pfNo: "RJ/JPR/12345/004",
esicNo: "12-34-567890-002-0002"
},
// Tudki Department Contractors
{
username: "suresh.kumar.tudki",
name: "Suresh Kumar",
email: "suresh.kumar@workallocate.com",
deptId: tudkiId,
phone: "9829012349",
aadhar: "678912345678",
bankAccount: "50100123456793",
bankName: "ICICI Bank",
bankIfsc: "ICIC0003456",
agreementNo: "AGR-TK-2024-001",
pfNo: "RJ/JPR/12345/005",
esicNo: "12-34-567890-003-0001"
},
{
username: "dinesh.gupta.tudki",
name: "Dinesh Gupta",
email: "dinesh.gupta@workallocate.com",
deptId: tudkiId,
phone: "9829012350",
aadhar: "789123456789",
bankAccount: "50100123456794",
bankName: "Axis Bank",
bankIfsc: "UTIB0002345",
agreementNo: "AGR-TK-2024-002",
pfNo: "RJ/JPR/12345/006",
esicNo: "12-34-567890-003-0002"
}
];
@@ -197,87 +398,299 @@ async function seedDatabase() {
}
}
// 6. Seed Sample Employees
console.log("👷 Seeding sample employees...");
const contractor1 = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
["contractor1"]
);
// 6. Seed Employees for all departments
console.log("👷 Seeding employees...");
const employeePassword = await hashPassword("employee123");
if (contractor1.length > 0) {
const employees = [
{
username: "employee1",
name: "Employee One",
email: "employee1@workallocate.com",
phone: "9876543220",
aadhar: "345678901234",
bankAccount: "3456789012345678",
bankName: "Punjab National Bank",
bankIfsc: "PUNB0001234"
},
{
username: "employee2",
name: "Employee Two",
email: "employee2@workallocate.com",
phone: "9876543221",
aadhar: "456789012345",
bankAccount: "4567890123456789",
bankName: "Bank of Baroda",
bankIfsc: "BARB0001234"
},
{
username: "employee3",
name: "Employee Three",
email: "employee3@workallocate.com",
phone: "9876543222",
aadhar: "567890123456",
bankAccount: "5678901234567890",
bankName: "ICICI Bank",
bankIfsc: "ICIC0001234"
}
];
// Get contractor IDs for employee assignment
const contractorIds: { [key: string]: number } = {};
for (const con of contractors) {
const result = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
[con.username]
);
if (result.length > 0) {
contractorIds[con.username] = result[0].id;
}
}
for (const emp of employees) {
const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
[emp.username]
);
if (existing.length === 0) {
const employees = [
// Groundnut Department Employees - Under Ramesh Patel
{
username: "ravi.singh.gn1",
name: "Ravi Singh",
email: "ravi.singh@workallocate.com",
deptId: groundnutId,
contractorUsername: "ramesh.patel.gn",
phone: "9876501001",
aadhar: "111122223333",
bankAccount: "30100111122233",
bankName: "State Bank of India",
bankIfsc: "SBIN0001111"
},
{
username: "amit.kumar.gn2",
name: "Amit Kumar",
email: "amit.kumar@workallocate.com",
deptId: groundnutId,
contractorUsername: "ramesh.patel.gn",
phone: "9876501002",
aadhar: "222233334444",
bankAccount: "30100222233344",
bankName: "Punjab National Bank",
bankIfsc: "PUNB0002222"
},
{
username: "vijay.meena.gn3",
name: "Vijay Meena",
email: "vijay.meena@workallocate.com",
deptId: groundnutId,
contractorUsername: "ramesh.patel.gn",
phone: "9876501003",
aadhar: "333344445555",
bankAccount: "30100333344455",
bankName: "HDFC Bank",
bankIfsc: "HDFC0003333"
},
// Groundnut Department Employees - Under Kishan Meena
{
username: "sanjay.yadav.gn4",
name: "Sanjay Yadav",
email: "sanjay.yadav@workallocate.com",
deptId: groundnutId,
contractorUsername: "kishan.meena.gn",
phone: "9876501004",
aadhar: "444455556666",
bankAccount: "30100444455566",
bankName: "Bank of Baroda",
bankIfsc: "BARB0004444"
},
{
username: "prakash.sharma.gn5",
name: "Prakash Sharma",
email: "prakash.sharma@workallocate.com",
deptId: groundnutId,
contractorUsername: "kishan.meena.gn",
phone: "9876501005",
aadhar: "555566667777",
bankAccount: "30100555566677",
bankName: "ICICI Bank",
bankIfsc: "ICIC0005555"
},
// Dana Department Employees - Under Gopal Sharma
{
username: "rampal.verma.dn1",
name: "Rampal Verma",
email: "rampal.verma@workallocate.com",
deptId: danaId,
contractorUsername: "gopal.sharma.dana",
phone: "9876502001",
aadhar: "666677778888",
bankAccount: "30100666677788",
bankName: "State Bank of India",
bankIfsc: "SBIN0006666"
},
{
username: "lakhan.singh.dn2",
name: "Lakhan Singh",
email: "lakhan.singh@workallocate.com",
deptId: danaId,
contractorUsername: "gopal.sharma.dana",
phone: "9876502002",
aadhar: "777788889999",
bankAccount: "30100777788899",
bankName: "Punjab National Bank",
bankIfsc: "PUNB0007777"
},
{
username: "bharat.meena.dn3",
name: "Bharat Meena",
email: "bharat.meena@workallocate.com",
deptId: danaId,
contractorUsername: "gopal.sharma.dana",
phone: "9876502003",
aadhar: "888899990000",
bankAccount: "30100888899900",
bankName: "HDFC Bank",
bankIfsc: "HDFC0008888"
},
// Dana Department Employees - Under Mohan Yadav
{
username: "kailash.patel.dn4",
name: "Kailash Patel",
email: "kailash.patel@workallocate.com",
deptId: danaId,
contractorUsername: "mohan.yadav.dana",
phone: "9876502004",
aadhar: "999900001111",
bankAccount: "30100999900011",
bankName: "Bank of Baroda",
bankIfsc: "BARB0009999"
},
{
username: "shyam.gupta.dn5",
name: "Shyam Gupta",
email: "shyam.gupta@workallocate.com",
deptId: danaId,
contractorUsername: "mohan.yadav.dana",
phone: "9876502005",
aadhar: "000011112222",
bankAccount: "30100000011122",
bankName: "ICICI Bank",
bankIfsc: "ICIC0000000"
},
// Tudki Department Employees - Under Suresh Kumar
{
username: "ganesh.kumar.tk1",
name: "Ganesh Kumar",
email: "ganesh.kumar@workallocate.com",
deptId: tudkiId,
contractorUsername: "suresh.kumar.tudki",
phone: "9876503001",
aadhar: "112233445566",
bankAccount: "30100112233445",
bankName: "State Bank of India",
bankIfsc: "SBIN0001122"
},
{
username: "naresh.yadav.tk2",
name: "Naresh Yadav",
email: "naresh.yadav@workallocate.com",
deptId: tudkiId,
contractorUsername: "suresh.kumar.tudki",
phone: "9876503002",
aadhar: "223344556677",
bankAccount: "30100223344556",
bankName: "Punjab National Bank",
bankIfsc: "PUNB0002233"
},
{
username: "mukesh.sharma.tk3",
name: "Mukesh Sharma",
email: "mukesh.sharma@workallocate.com",
deptId: tudkiId,
contractorUsername: "suresh.kumar.tudki",
phone: "9876503003",
aadhar: "334455667788",
bankAccount: "30100334455667",
bankName: "HDFC Bank",
bankIfsc: "HDFC0003344"
},
// Tudki Department Employees - Under Dinesh Gupta
{
username: "pappu.singh.tk4",
name: "Pappu Singh",
email: "pappu.singh@workallocate.com",
deptId: tudkiId,
contractorUsername: "dinesh.gupta.tudki",
phone: "9876503004",
aadhar: "445566778899",
bankAccount: "30100445566778",
bankName: "Bank of Baroda",
bankIfsc: "BARB0004455"
},
{
username: "deepak.verma.tk5",
name: "Deepak Verma",
email: "deepak.verma@workallocate.com",
deptId: tudkiId,
contractorUsername: "dinesh.gupta.tudki",
phone: "9876503005",
aadhar: "556677889900",
bankAccount: "30100556677889",
bankName: "ICICI Bank",
bankIfsc: "ICIC0005566"
},
{
username: "rahul.meena.tk6",
name: "Rahul Meena",
email: "rahul.meena@workallocate.com",
deptId: tudkiId,
contractorUsername: "dinesh.gupta.tudki",
phone: "9876503006",
aadhar: "667788990011",
bankAccount: "30100667788990",
bankName: "Axis Bank",
bankIfsc: "UTIB0006677"
}
];
for (const emp of employees) {
const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
[emp.username]
);
if (existing.length === 0) {
const contractorId = contractorIds[emp.contractorUsername];
if (contractorId) {
await db.execute(
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active,
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[emp.username, emp.name, emp.email, employeePassword, "Employee", groundnutId, contractor1[0].id, true,
[emp.username, emp.name, emp.email, employeePassword, "Employee", emp.deptId, contractorId, true,
emp.phone, emp.aadhar, emp.bankAccount, emp.bankName, emp.bankIfsc]
);
console.log(`${emp.name} created`);
} else {
console.log(` ${emp.name} already exists`);
}
} else {
console.log(` ${emp.name} already exists`);
}
}
// 7. Seed Contractor Rates
// 7. Seed Contractor Rates for all contractors
console.log("💰 Seeding contractor rates...");
if (contractor1.length > 0) {
const today = new Date().toISOString().split("T")[0];
// Get all sub-departments for rate assignment
const allSubDepts = await db.query<{ id: number; name: string; department_id: number }[]>(
"SELECT id, name, department_id FROM sub_departments"
);
// Create rates for each contractor based on their department
for (const [username, contractorId] of Object.entries(contractorIds)) {
const existingRate = await db.query<{ id: number }[]>(
"SELECT id FROM contractor_rates WHERE contractor_id = ?",
[contractor1[0].id]
[contractorId]
);
if (existingRate.length === 0) {
const today = new Date().toISOString().split("T")[0];
await db.execute(
"INSERT INTO contractor_rates (contractor_id, rate, effective_date) VALUES (?, ?, ?)",
[contractor1[0].id, 500.00, today]
);
console.log(" ✅ Contractor rates created");
} else {
console.log(" Contractor rates already exist");
// Find the contractor's department
const contractor = contractors.find(c => c.username === username);
if (contractor) {
// Get sub-departments for this contractor's department
const deptSubDepts = allSubDepts.filter(sd => sd.department_id === contractor.deptId);
// Create rates for Loading/Unloading sub-department (Per Bag rates)
const loadingSubDept = deptSubDepts.find(sd => sd.name === "Loading/Unloading");
if (loadingSubDept) {
// Get activities for this sub-department
const activities = await db.query<{ id: number; name: string }[]>(
"SELECT id, name FROM activities WHERE sub_department_id = ? LIMIT 3",
[loadingSubDept.id]
);
for (const activity of activities) {
const rate = 5 + Math.random() * 3; // Random rate between 5-8 per bag
await db.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
[contractorId, loadingSubDept.id, activity.name, rate.toFixed(2), today]
);
}
}
// Create fixed rates for other sub-departments
const fixedSubDepts = deptSubDepts.filter(sd => sd.name !== "Loading/Unloading");
for (const subDept of fixedSubDepts.slice(0, 2)) { // Limit to 2 fixed rate sub-depts per contractor
const rate = 300 + Math.random() * 200; // Random rate between 300-500 per person
await db.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, rate, effective_date) VALUES (?, ?, ?, ?)",
[contractorId, subDept.id, rate.toFixed(2), today]
);
}
}
}
}
console.log(" ✅ Contractor rates created");
console.log(`
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -289,17 +702,18 @@ async function seedDatabase() {
Username: admin
Password: admin123
Supervisor (Groundnut):
Username: supervisor_groundnut
Password: supervisor123
Supervisors (password: supervisor123):
- Tudki: rajesh.sharma.tudki
- Dana: sunil.verma.dana
- Groundnut: mahesh.agarwal.groundnut
Contractor:
Username: contractor1
Password: contractor123
Contractors (password: contractor123):
- Groundnut: ramesh.patel.gn, kishan.meena.gn
- Dana: gopal.sharma.dana, mohan.yadav.dana
- Tudki: suresh.kumar.tudki, dinesh.gupta.tudki
Employee:
Username: employee1
Password: employee123
Employees (password: employee123):
- Use any employee username like ravi.singh.gn1, rampal.verma.dn1, ganesh.kumar.tk1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);

View File

@@ -242,3 +242,25 @@ export interface CreateContractorRateRequest {
rate: number;
effectiveDate: string;
}
// Standard rate types
export interface StandardRate {
id: number;
sub_department_id: number | null;
activity: string | null;
rate: number;
effective_date: Date;
created_by: number;
created_at: Date;
sub_department_name?: string;
department_name?: string;
department_id?: number;
created_by_name?: string;
}
export interface CreateStandardRateRequest {
subDepartmentId?: number | null;
activity?: string | null;
rate: number;
effectiveDate: string;
}

View File

@@ -13,12 +13,22 @@ CREATE TABLE IF NOT EXISTS sub_departments (
id INT AUTO_INCREMENT PRIMARY KEY,
department_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
primary_activity VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE,
UNIQUE KEY unique_subdept (department_id, name)
);
-- Create activities table (activities belong to sub-departments)
CREATE TABLE IF NOT EXISTS activities (
id INT AUTO_INCREMENT PRIMARY KEY,
sub_department_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
unit_of_measurement ENUM('Per Bag', 'Fixed Rate-Per Person') NOT NULL DEFAULT 'Per Bag',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE CASCADE,
UNIQUE KEY unique_activity (sub_department_id, name)
);
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -120,6 +130,19 @@ CREATE TABLE IF NOT EXISTS contractor_rates (
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL
);
-- Create standard_rates table (default rates for comparison with contractor rates)
CREATE TABLE IF NOT EXISTS standard_rates (
id INT AUTO_INCREMENT PRIMARY KEY,
sub_department_id INT,
activity VARCHAR(255),
rate DECIMAL(10, 2) NOT NULL,
effective_date DATE NOT NULL,
created_by INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
);
-- Create indexes for better query performance
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_department ON users(department_id);
@@ -136,3 +159,6 @@ CREATE INDEX idx_attendance_date ON attendance(work_date);
CREATE INDEX idx_attendance_status ON attendance(status);
CREATE INDEX idx_contractor_rates_contractor ON contractor_rates(contractor_id);
CREATE INDEX idx_contractor_rates_date ON contractor_rates(effective_date);
CREATE INDEX idx_standard_rates_subdept ON standard_rates(sub_department_id);
CREATE INDEX idx_standard_rates_date ON standard_rates(effective_date);
CREATE INDEX idx_standard_rates_created_by ON standard_rates(created_by);

View File

@@ -0,0 +1,23 @@
-- Migration: Add standard_rates table
-- Run this migration to add the standard_rates table for supervisor-managed default rates
-- Create standard_rates table (default rates for comparison with contractor rates)
CREATE TABLE IF NOT EXISTS standard_rates (
id INT AUTO_INCREMENT PRIMARY KEY,
sub_department_id INT,
activity VARCHAR(255),
rate DECIMAL(10, 2) NOT NULL,
effective_date DATE NOT NULL,
created_by INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
);
-- Create indexes for standard_rates
CREATE INDEX idx_standard_rates_subdept ON standard_rates(sub_department_id);
CREATE INDEX idx_standard_rates_date ON standard_rates(effective_date);
CREATE INDEX idx_standard_rates_created_by ON standard_rates(created_by);
-- Verify table was created
SELECT 'standard_rates table created successfully' AS status;

View File

@@ -7,7 +7,7 @@ services:
MYSQL_ROOT_PASSWORD: admin123
MYSQL_DATABASE: work_allocation
ports:
- "3306:3306"
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro

View File

@@ -9,8 +9,11 @@ import { AttendancePage } from './pages/AttendancePage';
import { RatesPage } from './pages/RatesPage';
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
import { LoginPage } from './pages/LoginPage';
import { ReportingPage } from './pages/ReportingPage';
import { StandardRatesPage } from './pages/StandardRatesPage';
import { AllRatesPage } from './pages/AllRatesPage';
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps';
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates';
const AppContent: React.FC = () => {
const [activePage, setActivePage] = useState<PageType>('dashboard');
@@ -30,6 +33,12 @@ const AppContent: React.FC = () => {
return <RatesPage />;
case 'swaps':
return <EmployeeSwapPage />;
case 'reports':
return <ReportingPage />;
case 'standard-rates':
return <StandardRatesPage />;
case 'all-rates':
return <AllRatesPage />;
default:
return <DashboardPage />;
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft } from 'lucide-react';
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
interface SidebarItemProps {
@@ -112,6 +112,36 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
onClick={() => onNavigate('swaps')}
/>
)}
{/* Reports - SuperAdmin and Supervisor */}
{canManageRates && (
<SidebarItem
icon={FileSpreadsheet}
label="Reports"
active={activePage === 'reports'}
onClick={() => onNavigate('reports')}
/>
)}
{/* Standard Rates - SuperAdmin and Supervisor */}
{canManageRates && (
<SidebarItem
icon={Scale}
label="Standard Rates"
active={activePage === 'standard-rates'}
onClick={() => onNavigate('standard-rates')}
/>
)}
{/* All Rates View - SuperAdmin only */}
{isSuperAdmin && (
<SidebarItem
icon={Eye}
label="All Rates"
active={activePage === 'all-rates'}
onClick={() => onNavigate('all-rates')}
/>
)}
</nav>
{/* Role indicator at bottom */}

302
src/pages/AllRatesPage.tsx Normal file
View File

@@ -0,0 +1,302 @@
import React, { useState, useEffect, useMemo } from 'react';
import { RefreshCw, Search, Filter, Eye, Calendar } from 'lucide-react';
import { Card, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
export const AllRatesPage: React.FC = () => {
const { user } = useAuth();
const { departments } = useDepartments();
const [allRates, setAllRates] = useState<any[]>([]);
const [summary, setSummary] = useState<{ totalContractorRates: number; totalStandardRates: number; totalRates: number } | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Filters
const [filters, setFilters] = useState({
departmentId: '',
startDate: '',
endDate: '',
rateType: '', // 'contractor' | 'standard' | ''
});
const isSuperAdmin = user?.role === 'SuperAdmin';
// Fetch all rates
const fetchAllRates = async () => {
setLoading(true);
setError('');
try {
const params: any = {};
if (filters.departmentId) params.departmentId = parseInt(filters.departmentId);
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
const data = await api.getAllRates(params);
setAllRates(data.allRates);
setSummary(data.summary);
} catch (err: any) {
setError(err.message || 'Failed to fetch rates');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isSuperAdmin) {
fetchAllRates();
}
}, [isSuperAdmin]);
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value }));
};
const applyFilters = () => {
fetchAllRates();
};
const clearFilters = () => {
setFilters({
departmentId: '',
startDate: '',
endDate: '',
rateType: '',
});
setTimeout(fetchAllRates, 0);
};
// Filter rates based on search and rate type
const filteredRates = useMemo(() => {
let rates = allRates;
// Filter by rate type
if (filters.rateType) {
rates = rates.filter(r => r.rate_type === filters.rateType);
}
// Filter by search query
if (searchQuery) {
const query = searchQuery.toLowerCase();
rates = rates.filter(r =>
r.contractor_name?.toLowerCase().includes(query) ||
r.sub_department_name?.toLowerCase().includes(query) ||
r.department_name?.toLowerCase().includes(query) ||
r.activity?.toLowerCase().includes(query) ||
r.created_by_name?.toLowerCase().includes(query)
);
}
return rates;
}, [allRates, searchQuery, filters.rateType]);
// Access check
if (!isSuperAdmin) {
return (
<div className="p-6">
<Card>
<CardContent>
<div className="text-center py-12">
<Eye size={48} className="mx-auto text-gray-400 mb-4" />
<h2 className="text-xl font-semibold text-gray-700 mb-2">Access Restricted</h2>
<p className="text-gray-500">
This page is only accessible to Super Admin accounts.
</p>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Eye className="text-purple-600" size={24} />
<div>
<h2 className="text-xl font-semibold text-gray-800">All Rates Overview</h2>
<p className="text-sm text-gray-500">View all contractor and standard rates across all departments</p>
</div>
</div>
</div>
</div>
<CardContent>
{/* Filters */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 mb-4">
<Filter size={18} className="text-gray-500" />
<h3 className="font-medium text-gray-700">Filters</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Select
label="Department"
name="departmentId"
value={filters.departmentId}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Departments' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
<Select
label="Rate Type"
name="rateType"
value={filters.rateType}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Types' },
{ value: 'contractor', label: 'Contractor Rates' },
{ value: 'standard', label: 'Standard Rates' },
]}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
<Input
type="date"
name="startDate"
value={filters.startDate}
onChange={handleFilterChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
<Input
type="date"
name="endDate"
value={filters.endDate}
onChange={handleFilterChange}
/>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button onClick={applyFilters} size="sm">
Apply Filters
</Button>
<Button variant="outline" onClick={clearFilters} size="sm">
Clear
</Button>
</div>
</div>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">Total Rates</div>
<div className="text-2xl font-bold text-blue-800">{summary.totalRates}</div>
</div>
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="text-sm text-orange-600 font-medium">Contractor Rates</div>
<div className="text-2xl font-bold text-orange-800">{summary.totalContractorRates}</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-sm text-green-600 font-medium">Standard Rates</div>
<div className="text-2xl font-bold text-green-800">{summary.totalStandardRates}</div>
</div>
</div>
)}
{/* Search and Refresh */}
<div className="flex gap-4 mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search by contractor, department, activity..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchAllRates}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
{/* Error */}
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{/* Table */}
{loading ? (
<div className="text-center py-8">Loading all rates...</div>
) : filteredRates.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableHead>Type</TableHead>
<TableHead>Contractor</TableHead>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Effective Date</TableHead>
<TableHead>Created By</TableHead>
</TableHeader>
<TableBody>
{filteredRates.map((rate, idx) => (
<TableRow key={`${rate.rate_type}-${rate.id}-${idx}`}>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.rate_type === 'contractor'
? 'bg-orange-100 text-orange-700'
: 'bg-green-100 text-green-700'
}`}>
{rate.rate_type === 'contractor' ? 'Contractor' : 'Standard'}
</span>
</TableCell>
<TableCell className="font-medium">
{rate.contractor_name || '-'}
</TableCell>
<TableCell>{rate.department_name || '-'}</TableCell>
<TableCell>{rate.sub_department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.activity === 'Loading' || rate.activity === 'Unloading'
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-700'
}`}>
{rate.activity || 'Standard'}
</span>
</TableCell>
<TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Calendar size={14} className="text-gray-400" />
{new Date(rate.effective_date).toLocaleDateString()}
</div>
</TableCell>
<TableCell className="text-gray-500">
{rate.created_by_name || '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
No rates found. Adjust your filters or check back later.
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink, RefreshCw } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts';
import { Card, CardHeader, CardContent } from '../components/ui/Card';
import { useEmployees } from '../hooks/useEmployees';
@@ -43,15 +43,25 @@ interface HierarchyNode {
}
export const DashboardPage: React.FC = () => {
const { employees, loading: employeesLoading } = useEmployees();
const { employees, loading: employeesLoading, refresh: refreshEmployees } = useEmployees();
const { departments, loading: deptLoading } = useDepartments();
const { allocations, loading: allocLoading } = useWorkAllocations();
const { allocations, loading: allocLoading, refresh: refreshAllocations } = useWorkAllocations();
const { user } = useAuth();
const [attendance, setAttendance] = useState<AttendanceRecord[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
const [contractorRates, setContractorRates] = useState<Record<number, number>>({});
// Refresh all data function
const refreshAllData = () => {
refreshEmployees();
refreshAllocations();
const today = new Date().toISOString().split('T')[0];
api.getAttendance({ startDate: today, endDate: today })
.then(setAttendance)
.catch(console.error);
};
const isSuperAdmin = user?.role === 'SuperAdmin';
const isSupervisor = user?.role === 'Supervisor';
const isContractor = user?.role === 'Contractor';
@@ -161,41 +171,78 @@ export const DashboardPage: React.FC = () => {
e => e.role === 'Contractor' && e.department_id === supervisor.department_id
);
// Get employees without a contractor but in this department (e.g., swapped employees)
const unassignedEmployees = employees.filter(
e => e.role === 'Employee' &&
e.department_id === supervisor.department_id &&
!e.contractor_id
);
const contractorNodes = deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
return {
id: contractor.id,
name: contractor.name,
role: 'Contractor',
department: contractor.department_name || '',
children: contractorEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || '-',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
};
});
// Add unassigned employees node if there are any
if (unassignedEmployees.length > 0) {
contractorNodes.push({
id: -supervisor.department_id!, // Negative ID to avoid conflicts
name: 'Unassigned (Swapped)',
role: 'Contractor',
department: supervisor.department_name || '',
children: unassignedEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
});
}
const supervisorNode: HierarchyNode = {
id: supervisor.id,
name: supervisor.name,
role: 'Supervisor',
department: supervisor.department_name || '',
children: deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
return {
id: contractor.id,
name: contractor.name,
role: 'Contractor',
department: contractor.department_name || '',
children: contractorEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id);
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: emp.sub_department_name,
activity: empAllocation?.description || 'Loading',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
};
}),
children: contractorNodes,
};
return supervisorNode;
@@ -211,27 +258,34 @@ export const DashboardPage: React.FC = () => {
e => e.role === 'Contractor' && e.department_id === user.department_id
);
return deptContractors.map(contractor => {
// Get employees without a contractor but in this department (e.g., swapped employees)
const unassignedEmployees = employees.filter(
e => e.role === 'Employee' &&
e.department_id === user.department_id &&
!e.contractor_id
);
const contractorNodes: HierarchyNode[] = deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
const contractorNode: HierarchyNode = {
return {
id: contractor.id,
name: contractor.name,
role: 'Contractor',
department: contractor.department_name || '',
children: contractorEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: emp.sub_department_name,
activity: empAllocation?.description || '-',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || '-',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
@@ -240,10 +294,38 @@ export const DashboardPage: React.FC = () => {
};
}),
};
return contractorNode;
});
}, [isSupervisor, user, employees, attendance, allocations]);
// Add unassigned employees node if there are any
if (unassignedEmployees.length > 0) {
contractorNodes.push({
id: -user.department_id, // Negative ID to avoid conflicts
name: 'Unassigned (Swapped)',
role: 'Contractor',
department: filteredDepartments[0]?.name || '',
children: unassignedEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
});
}
return contractorNodes;
}, [isSupervisor, user, employees, attendance, allocations, filteredDepartments]);
// Department presence data for bar chart
const departmentPresenceData = useMemo(() => {
@@ -405,10 +487,19 @@ export const DashboardPage: React.FC = () => {
{/* Daily Attendance Report Header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">Daily Attendance Report</h1>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
<div className="flex items-center gap-2">
<button
onClick={refreshAllData}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw size={18} />
Refresh
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
</div>
</div>
{/* Search Bar */}
@@ -647,10 +738,19 @@ export const DashboardPage: React.FC = () => {
<h1 className="text-2xl font-bold text-gray-800">{departmentName} Dashboard</h1>
<p className="text-gray-500 mt-1">Daily Attendance & Work Overview</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
<div className="flex items-center gap-2">
<button
onClick={refreshAllData}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw size={18} />
Refresh
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
</div>
</div>
{/* Search Bar */}

View File

@@ -202,16 +202,11 @@ export const LoginPage: React.FC = () => {
<div className="w-full border-t border-white/10" />
</div>
<div className="relative flex justify-center text-xs">
<span className="px-4 bg-transparent text-blue-200/50">Work Allocation System</span>
<span className="px-4 bg-transparent text-blue-200/50"></span>
</div>
</div>
{/* Footer Info */}
<div className="text-center">
<p className="text-blue-200/40 text-xs">
Secure login powered by JWT authentication
</p>
</div>
</div>
{/* Version badge */}

346
src/pages/ReportingPage.tsx Normal file
View File

@@ -0,0 +1,346 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Download, RefreshCw, Search, FileSpreadsheet, Filter } from 'lucide-react';
import { Card, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
export const ReportingPage: React.FC = () => {
const { user } = useAuth();
const { departments } = useDepartments();
const [allocations, setAllocations] = useState<any[]>([]);
const [summary, setSummary] = useState<{ totalAllocations: number; totalAmount: string; totalUnits: string } | null>(null);
const [contractors, setContractors] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Filters
const [filters, setFilters] = useState({
startDate: '',
endDate: '',
departmentId: '',
contractorId: '',
});
const isSuperAdmin = user?.role === 'SuperAdmin';
// Fetch contractors
useEffect(() => {
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error);
}, []);
// Fetch report data
const fetchReport = async () => {
setLoading(true);
setError('');
try {
const params: any = {};
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
if (filters.departmentId && isSuperAdmin) params.departmentId = parseInt(filters.departmentId);
if (filters.contractorId) params.contractorId = parseInt(filters.contractorId);
const data = await api.getCompletedAllocationsReport(params);
setAllocations(data.allocations);
setSummary(data.summary);
} catch (err: any) {
setError(err.message || 'Failed to fetch report');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchReport();
}, []);
// Filter allocations based on search
const filteredAllocations = useMemo(() => {
if (!searchQuery) return allocations;
const query = searchQuery.toLowerCase();
return allocations.filter(a =>
a.employee_name?.toLowerCase().includes(query) ||
a.contractor_name?.toLowerCase().includes(query) ||
a.sub_department_name?.toLowerCase().includes(query) ||
a.activity?.toLowerCase().includes(query) ||
a.department_name?.toLowerCase().includes(query)
);
}, [allocations, searchQuery]);
// Export to Excel (CSV format)
const exportToExcel = () => {
if (filteredAllocations.length === 0) {
alert('No data to export');
return;
}
// Define headers
const headers = [
'ID',
'Employee Name',
'Employee Phone',
'Contractor',
'Department',
'Sub-Department',
'Activity',
'Assigned Date',
'Completion Date',
'Rate (₹)',
'Units',
'Total Amount (₹)',
'Status',
];
// Map data to rows
const rows = filteredAllocations.map(a => [
a.id,
a.employee_name || '',
a.employee_phone || '',
a.contractor_name || '',
a.department_name || '',
a.sub_department_name || '',
a.activity || 'Standard',
a.assigned_date ? new Date(a.assigned_date).toLocaleDateString() : '',
a.completion_date ? new Date(a.completion_date).toLocaleDateString() : '',
a.rate || 0,
a.units || '',
a.total_amount || a.rate || 0,
a.status,
]);
// Create CSV content
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
].join('\n');
// Add summary at the end
const summaryRows = [
'',
'SUMMARY',
`Total Allocations,${summary?.totalAllocations || 0}`,
`Total Amount (₹),${summary?.totalAmount || 0}`,
`Total Units,${summary?.totalUnits || 0}`,
];
const fullContent = csvContent + '\n' + summaryRows.join('\n');
// Create and download file
const blob = new Blob([fullContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `completed_work_allocations_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value }));
};
const applyFilters = () => {
fetchReport();
};
const clearFilters = () => {
setFilters({
startDate: '',
endDate: '',
departmentId: '',
contractorId: '',
});
setTimeout(fetchReport, 0);
};
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileSpreadsheet className="text-green-600" size={24} />
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2>
</div>
<Button onClick={exportToExcel} disabled={filteredAllocations.length === 0}>
<Download size={16} className="mr-2" />
Export to Excel
</Button>
</div>
</div>
<CardContent>
{/* Filters */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 mb-4">
<Filter size={18} className="text-gray-500" />
<h3 className="font-medium text-gray-700">Filters</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
<Input
type="date"
name="startDate"
value={filters.startDate}
onChange={handleFilterChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
<Input
type="date"
name="endDate"
value={filters.endDate}
onChange={handleFilterChange}
/>
</div>
{isSuperAdmin && (
<Select
label="Department"
name="departmentId"
value={filters.departmentId}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Departments' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
)}
<Select
label="Contractor"
name="contractorId"
value={filters.contractorId}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Contractors' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
]}
/>
</div>
<div className="flex gap-2 mt-4">
<Button onClick={applyFilters} size="sm">
Apply Filters
</Button>
<Button variant="outline" onClick={clearFilters} size="sm">
Clear
</Button>
</div>
</div>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">Total Completed</div>
<div className="text-2xl font-bold text-blue-800">{summary.totalAllocations}</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-sm text-green-600 font-medium">Total Amount</div>
<div className="text-2xl font-bold text-green-800">{parseFloat(summary.totalAmount).toLocaleString()}</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-sm text-purple-600 font-medium">Total Units</div>
<div className="text-2xl font-bold text-purple-800">{parseFloat(summary.totalUnits).toLocaleString()}</div>
</div>
</div>
)}
{/* Search and Refresh */}
<div className="flex gap-4 mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search by employee, contractor, department..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchReport}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
{/* Error */}
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{/* Table */}
{loading ? (
<div className="text-center py-8">Loading report data...</div>
) : filteredAllocations.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableHead>ID</TableHead>
<TableHead>Employee</TableHead>
<TableHead>Contractor</TableHead>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Assigned</TableHead>
<TableHead>Completed</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Units</TableHead>
<TableHead>Total ()</TableHead>
</TableHeader>
<TableBody>
{filteredAllocations.map((allocation) => {
const rate = parseFloat(allocation.rate) || 0;
const units = parseFloat(allocation.units) || 0;
const total = parseFloat(allocation.total_amount) || rate;
return (
<TableRow key={allocation.id}>
<TableCell>{allocation.id}</TableCell>
<TableCell className="font-medium">{allocation.employee_name || '-'}</TableCell>
<TableCell>{allocation.contractor_name || '-'}</TableCell>
<TableCell>{allocation.department_name || '-'}</TableCell>
<TableCell>{allocation.sub_department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === 'Loading' || allocation.activity === 'Unloading'
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-700'
}`}>
{allocation.activity || 'Standard'}
</span>
</TableCell>
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
<TableCell>
{allocation.completion_date
? new Date(allocation.completion_date).toLocaleDateString()
: '-'}
</TableCell>
<TableCell>{rate.toFixed(2)}</TableCell>
<TableCell>{units > 0 ? units : '-'}</TableCell>
<TableCell className="font-semibold text-green-600">{total.toFixed(2)}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
No completed work allocations found. Adjust your filters or check back later.
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -0,0 +1,523 @@
import React, { useState, useEffect, useMemo } from 'react';
import { RefreshCw, Trash2, Edit, DollarSign, Search, Scale, ArrowUpDown } from 'lucide-react';
import { Card, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
export const StandardRatesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'add' | 'compare'>('list');
const { user } = useAuth();
const { departments } = useDepartments();
const [standardRates, setStandardRates] = useState<any[]>([]);
const [contractors, setContractors] = useState<any[]>([]);
const [comparisons, setComparisons] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Form state
const [formData, setFormData] = useState({
subDepartmentId: '',
activity: '',
rate: '',
effectiveDate: new Date().toISOString().split('T')[0],
});
const [selectedDept, setSelectedDept] = useState('');
const { subDepartments } = useSubDepartments(selectedDept);
const [formError, setFormError] = useState('');
const [formLoading, setFormLoading] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
// Compare filters
const [compareContractorId, setCompareContractorId] = useState('');
const isSupervisor = user?.role === 'Supervisor';
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
// Fetch standard rates
const fetchStandardRates = async () => {
setLoading(true);
setError('');
try {
const data = await api.getStandardRates();
setStandardRates(data);
} catch (err: any) {
setError(err.message || 'Failed to fetch standard rates');
} finally {
setLoading(false);
}
};
// Fetch contractors
const fetchContractors = async () => {
try {
const data = await api.getUsers({ role: 'Contractor' });
setContractors(data);
} catch (err) {
console.error('Failed to fetch contractors:', err);
}
};
// Fetch comparison data
const fetchComparison = async () => {
if (!compareContractorId) {
setComparisons([]);
return;
}
setLoading(true);
try {
const data = await api.compareRates({ contractorId: parseInt(compareContractorId) });
setComparisons(data.comparisons);
} catch (err: any) {
setError(err.message || 'Failed to fetch comparison');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStandardRates();
fetchContractors();
}, []);
useEffect(() => {
if (isSupervisor && user?.department_id) {
setSelectedDept(String(user.department_id));
}
}, [isSupervisor, user?.department_id]);
useEffect(() => {
if (activeTab === 'compare' && compareContractorId) {
fetchComparison();
}
}, [activeTab, compareContractorId]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
setFormError('');
};
const resetForm = () => {
setFormData({
subDepartmentId: '',
activity: '',
rate: '',
effectiveDate: new Date().toISOString().split('T')[0],
});
setEditingId(null);
setFormError('');
};
const handleSubmit = async () => {
if (!formData.rate || !formData.effectiveDate) {
setFormError('Rate and effective date are required');
return;
}
setFormLoading(true);
setFormError('');
try {
if (editingId) {
await api.updateStandardRate(editingId, {
rate: parseFloat(formData.rate),
activity: formData.activity || undefined,
effectiveDate: formData.effectiveDate,
});
} else {
await api.createStandardRate({
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined,
activity: formData.activity || undefined,
rate: parseFloat(formData.rate),
effectiveDate: formData.effectiveDate,
});
}
resetForm();
setActiveTab('list');
fetchStandardRates();
} catch (err: any) {
setFormError(err.message || 'Failed to save rate');
} finally {
setFormLoading(false);
}
};
const handleEdit = (rate: any) => {
setFormData({
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '',
activity: rate.activity || '',
rate: String(rate.rate),
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0],
});
if (rate.department_id) {
setSelectedDept(String(rate.department_id));
}
setEditingId(rate.id);
setActiveTab('add');
};
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this standard rate?')) return;
try {
await api.deleteStandardRate(id);
fetchStandardRates();
} catch (err: any) {
alert(err.message || 'Failed to delete rate');
}
};
// Filter rates based on search
const filteredRates = useMemo(() => {
if (!searchQuery) return standardRates;
const query = searchQuery.toLowerCase();
return standardRates.filter(rate =>
rate.sub_department_name?.toLowerCase().includes(query) ||
rate.department_name?.toLowerCase().includes(query) ||
rate.activity?.toLowerCase().includes(query) ||
rate.created_by_name?.toLowerCase().includes(query)
);
}, [standardRates, searchQuery]);
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200">
<div className="flex space-x-8 px-6">
<button
onClick={() => { setActiveTab('list'); resetForm(); }}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Standard Rates
</button>
{canManageRates && (
<button
onClick={() => setActiveTab('add')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'add'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{editingId ? 'Edit Rate' : 'Add Standard Rate'}
</button>
)}
<button
onClick={() => setActiveTab('compare')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'compare'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<Scale size={16} className="inline mr-1" />
Compare Rates
</button>
</div>
</div>
<CardContent>
{activeTab === 'list' && (
<div>
<div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search by sub-department, activity..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchStandardRates}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-700">
<strong>Standard Rates</strong> are default rates set by supervisors for sub-departments and activities.
These are used as benchmarks to compare against contractor-specific rates.
</p>
</div>
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{loading ? (
<div className="text-center py-8">Loading standard rates...</div>
) : filteredRates.length > 0 ? (
<Table>
<TableHeader>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Effective Date</TableHead>
<TableHead>Created By</TableHead>
{canManageRates && <TableHead>Actions</TableHead>}
</TableHeader>
<TableBody>
{filteredRates.map((rate) => (
<TableRow key={rate.id}>
<TableCell>{rate.department_name || '-'}</TableCell>
<TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.activity === 'Loading' || rate.activity === 'Unloading'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}>
{rate.activity || 'Standard'}
</span>
</TableCell>
<TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span>
</TableCell>
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
<TableCell className="text-gray-500">{rate.created_by_name || '-'}</TableCell>
{canManageRates && (
<TableCell>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rate)}
className="text-blue-600"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(rate.id)}
className="text-red-600"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-gray-500">
No standard rates configured yet. Add one to get started!
</div>
)}
</div>
)}
{activeTab === 'add' && canManageRates && (
<div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800">
{editingId ? 'Edit Standard Rate' : 'Add New Standard Rate'}
</h3>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<h4 className="font-medium text-yellow-800 mb-2">About Standard Rates</h4>
<p className="text-sm text-yellow-700">
Standard rates serve as default benchmarks for sub-departments and activities.
Contractor rates can be compared against these to identify deviations.
</p>
</div>
{formError && (
<div className="p-3 bg-red-100 text-red-700 rounded-md">
{formError}
</div>
)}
<div className="grid grid-cols-2 gap-6">
{isSupervisor ? (
<Input
label="Department"
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'}
disabled
/>
) : (
<Select
label="Department"
value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)}
options={[
{ value: '', label: 'Select Department' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
)}
<Select
label="Sub-Department"
name="subDepartmentId"
value={formData.subDepartmentId}
onChange={handleInputChange}
disabled={!!editingId}
options={[
{ value: '', label: 'All Sub-Departments' },
...subDepartments.map(s => ({ value: String(s.id), label: s.name }))
]}
/>
<Select
label="Activity Type"
name="activity"
value={formData.activity}
onChange={handleInputChange}
options={[
{ value: '', label: 'Standard (Default)' },
{ value: 'Loading', label: 'Loading (per unit)' },
{ value: 'Unloading', label: 'Unloading (per unit)' },
{ value: 'Other', label: 'Other' },
]}
/>
<Input
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
? "Rate per Unit (₹)"
: "Standard Rate (₹)"}
name="rate"
type="number"
value={formData.rate}
onChange={handleInputChange}
placeholder="Enter rate amount"
required
/>
<Input
label="Effective Date"
name="effectiveDate"
type="date"
value={formData.effectiveDate}
onChange={handleInputChange}
required
/>
</div>
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={formLoading}>
{formLoading ? 'Saving...' : (
<>
<DollarSign size={16} className="mr-2" />
{editingId ? 'Update Rate' : 'Add Standard Rate'}
</>
)}
</Button>
</div>
</div>
)}
{activeTab === 'compare' && (
<div>
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<ArrowUpDown size={20} className="inline mr-2" />
Compare Contractor Rates vs Standard Rates
</h3>
<div className="flex gap-4 items-end">
<div className="w-64">
<Select
label="Select Contractor"
value={compareContractorId}
onChange={(e) => setCompareContractorId(e.target.value)}
options={[
{ value: '', label: 'Select Contractor' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
]}
/>
</div>
<Button onClick={fetchComparison} disabled={!compareContractorId}>
Compare
</Button>
</div>
</div>
{loading ? (
<div className="text-center py-8">Loading comparison...</div>
) : comparisons.length > 0 ? (
<Table>
<TableHeader>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Contractor Rate ()</TableHead>
<TableHead>Standard Rate ()</TableHead>
<TableHead>Difference ()</TableHead>
<TableHead>Status</TableHead>
</TableHeader>
<TableBody>
{comparisons.map((comp, idx) => (
<TableRow key={idx}>
<TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
comp.activity === 'Loading' || comp.activity === 'Unloading'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}>
{comp.activity || 'Standard'}
</span>
</TableCell>
<TableCell className="font-semibold">{comp.rate}</TableCell>
<TableCell className="text-gray-600">{comp.standard_rate}</TableCell>
<TableCell>
<span className={`font-semibold ${
comp.difference > 0 ? 'text-red-600' :
comp.difference < 0 ? 'text-green-600' :
'text-gray-600'
}`}>
{comp.difference > 0 ? '+' : ''}{comp.difference.toFixed(2)}
</span>
</TableCell>
<TableCell>
{comp.is_above_standard ? (
<span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">
Above Standard ({comp.percentage_difference}%)
</span>
) : comp.is_below_standard ? (
<span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">
Below Standard ({comp.percentage_difference}%)
</span>
) : (
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">
At Standard
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : compareContractorId ? (
<div className="text-center py-8 text-gray-500">
No rates found for this contractor to compare.
</div>
) : (
<div className="text-center py-8 text-gray-500">
Select a contractor to compare their rates against standard rates.
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -253,6 +253,108 @@ class ApiService {
method: 'DELETE',
});
}
// Reports
async getCompletedAllocationsReport(params?: {
startDate?: string;
endDate?: string;
departmentId?: number;
contractorId?: number;
employeeId?: number;
}) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
allocations: any[];
summary: { totalAllocations: number; totalAmount: string; totalUnits: string }
}>(`/reports/completed-allocations${query ? `?${query}` : ''}`);
}
async getReportSummary(params?: { startDate?: string; endDate?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
byContractor: any[];
bySubDepartment: any[];
byActivity: any[];
}>(`/reports/summary${query ? `?${query}` : ''}`);
}
// Standard Rates
async getStandardRates(params?: { departmentId?: number; subDepartmentId?: number; activity?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<any[]>(`/standard-rates${query ? `?${query}` : ''}`);
}
async getAllRates(params?: { departmentId?: number; startDate?: string; endDate?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
allRates: any[];
summary: { totalContractorRates: number; totalStandardRates: number; totalRates: number };
}>(`/standard-rates/all-rates${query ? `?${query}` : ''}`);
}
async compareRates(params?: { contractorId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
standardRates: any[];
contractorRates: any[];
comparisons: any[];
}>(`/standard-rates/compare${query ? `?${query}` : ''}`);
}
async createStandardRate(data: {
subDepartmentId?: number;
activity?: string;
rate: number;
effectiveDate: string
}) {
return this.request<any>('/standard-rates', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateStandardRate(id: number, data: { rate?: number; activity?: string; effectiveDate?: string }) {
return this.request<any>(`/standard-rates/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteStandardRate(id: number) {
return this.request<{ message: string }>(`/standard-rates/${id}`, {
method: 'DELETE',
});
}
// Activities
async getActivities(params?: { departmentId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<any[]>(`/activities${query ? `?${query}` : ''}`);
}
async getActivity(id: number) {
return this.request<any>(`/activities/${id}`);
}
async createActivity(data: { sub_department_id: number; name: string; unit_of_measurement?: string }) {
return this.request<any>('/activities', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateActivity(id: number, data: { name?: string; unit_of_measurement?: string }) {
return this.request<any>(`/activities/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteActivity(id: number) {
return this.request<{ message: string }>(`/activities/${id}`, {
method: 'DELETE',
});
}
}
export const api = new ApiService(API_BASE_URL);

View File

@@ -12,6 +12,7 @@ export interface User {
contractor_name?: string;
sub_department_id?: number;
sub_department_name?: string;
primary_activity?: string;
// Common fields for Employee and Contractor
phone_number?: string;
aadhar_number?: string;
@@ -35,9 +36,20 @@ export interface SubDepartment {
id: number;
department_id: number;
name: string;
primary_activity: string;
created_at: string;
updated_at: string;
department_name?: string;
}
export interface Activity {
id: number;
sub_department_id: number;
name: string;
unit_of_measurement: 'Per Bag' | 'Fixed Rate-Per Person';
created_at: string;
sub_department_name?: string;
department_id?: number;
department_name?: string;
}
export interface WorkAllocation {
@@ -111,10 +123,43 @@ export interface EmployeeSwap {
export interface ContractorRate {
id: number;
contractor_id: number;
sub_department_id?: number;
activity?: string;
rate: number;
effective_date: string;
created_at: string;
updated_at: string;
contractor_name?: string;
contractor_username?: string;
sub_department_name?: string;
department_name?: string;
}
export interface StandardRate {
id: number;
sub_department_id?: number;
activity?: string;
rate: number;
effective_date: string;
created_by: number;
created_at: string;
sub_department_name?: string;
department_name?: string;
department_id?: number;
created_by_name?: string;
}
export interface RateComparison {
id: number;
contractor_id: number;
contractor_name: string;
sub_department_id?: number;
sub_department_name?: string;
activity?: string;
rate: number;
standard_rate: number;
difference: number;
percentage_difference: string | null;
is_above_standard: boolean;
is_below_standard: boolean;
}

View File

@@ -74,11 +74,11 @@ check_db_connection() {
docker exec work_allocation_db mysql -u root -padmin123 -e "SELECT 1" &> /dev/null
return $?
elif command -v mysql &> /dev/null; then
mysql -h localhost -P 3306 -u root -padmin123 -e "SELECT 1" &> /dev/null
mysql -h localhost -P 3307 -u root -padmin123 -e "SELECT 1" &> /dev/null
return $?
else
# Try using nc to check if port is open
nc -z localhost 3306 &> /dev/null
nc -z localhost 3307 &> /dev/null
return $?
fi
}

259
test_employee_swap.sh Executable file
View File

@@ -0,0 +1,259 @@
#!/bin/bash
# Employee Swap Backend Test Script
# Tests the employee swap functionality including create, complete, and cancel operations
BASE_URL="http://localhost:3000/api"
ADMIN_USER="admin"
ADMIN_PASS="admin123"
echo "========================================"
echo " Employee Swap Backend Test"
echo "========================================"
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print test results
print_result() {
if [ $1 -eq 0 ]; then
echo -e "${GREEN}✓ PASS${NC}: $2"
else
echo -e "${RED}✗ FAIL${NC}: $2"
fi
}
# Step 1: Login as SuperAdmin
echo "Step 1: Logging in as SuperAdmin..."
LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$ADMIN_USER\",\"password\":\"$ADMIN_PASS\"}")
TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.token')
if [ "$TOKEN" != "null" ] && [ -n "$TOKEN" ]; then
print_result 0 "Login successful"
echo " Token: ${TOKEN:0:30}..."
else
print_result 1 "Login failed"
echo " Response: $LOGIN_RESPONSE"
exit 1
fi
echo ""
# Step 2: Get list of employees
echo "Step 2: Fetching employees..."
EMPLOYEES_RESPONSE=$(curl -s -X GET "$BASE_URL/users?role=Employee" \
-H "Authorization: Bearer $TOKEN")
EMPLOYEE_COUNT=$(echo $EMPLOYEES_RESPONSE | jq 'length')
if [ "$EMPLOYEE_COUNT" -gt 0 ]; then
print_result 0 "Found $EMPLOYEE_COUNT employees"
FIRST_EMPLOYEE_ID=$(echo $EMPLOYEES_RESPONSE | jq '.[0].id')
FIRST_EMPLOYEE_NAME=$(echo $EMPLOYEES_RESPONSE | jq -r '.[0].name')
FIRST_EMPLOYEE_DEPT=$(echo $EMPLOYEES_RESPONSE | jq '.[0].department_id')
FIRST_EMPLOYEE_CONTRACTOR=$(echo $EMPLOYEES_RESPONSE | jq '.[0].contractor_id')
echo " Test employee: $FIRST_EMPLOYEE_NAME (ID: $FIRST_EMPLOYEE_ID)"
echo " Current department_id: $FIRST_EMPLOYEE_DEPT"
echo " Current contractor_id: $FIRST_EMPLOYEE_CONTRACTOR"
else
print_result 1 "No employees found"
echo " Response: $EMPLOYEES_RESPONSE"
exit 1
fi
echo ""
# Step 3: Get list of departments
echo "Step 3: Fetching departments..."
DEPTS_RESPONSE=$(curl -s -X GET "$BASE_URL/departments" \
-H "Authorization: Bearer $TOKEN")
DEPT_COUNT=$(echo $DEPTS_RESPONSE | jq 'length')
if [ "$DEPT_COUNT" -gt 1 ]; then
print_result 0 "Found $DEPT_COUNT departments"
# Find a different department than the employee's current one
TARGET_DEPT_ID=$(echo $DEPTS_RESPONSE | jq --argjson current "$FIRST_EMPLOYEE_DEPT" '[.[] | select(.id != $current)][0].id')
TARGET_DEPT_NAME=$(echo $DEPTS_RESPONSE | jq -r --argjson current "$FIRST_EMPLOYEE_DEPT" '[.[] | select(.id != $current)][0].name')
echo " Target department: $TARGET_DEPT_NAME (ID: $TARGET_DEPT_ID)"
else
print_result 1 "Need at least 2 departments for swap test"
exit 1
fi
echo ""
# Step 4: Get contractors in target department
echo "Step 4: Fetching contractors in target department..."
CONTRACTORS_RESPONSE=$(curl -s -X GET "$BASE_URL/users?role=Contractor&departmentId=$TARGET_DEPT_ID" \
-H "Authorization: Bearer $TOKEN")
CONTRACTOR_COUNT=$(echo $CONTRACTORS_RESPONSE | jq 'length')
if [ "$CONTRACTOR_COUNT" -gt 0 ]; then
TARGET_CONTRACTOR_ID=$(echo $CONTRACTORS_RESPONSE | jq '.[0].id')
TARGET_CONTRACTOR_NAME=$(echo $CONTRACTORS_RESPONSE | jq -r '.[0].name')
print_result 0 "Found contractor: $TARGET_CONTRACTOR_NAME (ID: $TARGET_CONTRACTOR_ID)"
else
TARGET_CONTRACTOR_ID="null"
echo -e "${YELLOW}⚠ WARNING${NC}: No contractors in target department, will swap without contractor"
fi
echo ""
# Step 5: Check for existing active swaps
echo "Step 5: Checking for existing active swaps..."
EXISTING_SWAPS=$(curl -s -X GET "$BASE_URL/employee-swaps?status=Active" \
-H "Authorization: Bearer $TOKEN")
ACTIVE_SWAP_FOR_EMPLOYEE=$(echo $EXISTING_SWAPS | jq --argjson empId "$FIRST_EMPLOYEE_ID" '[.[] | select(.employee_id == $empId)][0]')
if [ "$ACTIVE_SWAP_FOR_EMPLOYEE" != "null" ]; then
EXISTING_SWAP_ID=$(echo $ACTIVE_SWAP_FOR_EMPLOYEE | jq '.id')
echo -e "${YELLOW}⚠ WARNING${NC}: Employee already has active swap (ID: $EXISTING_SWAP_ID), cancelling it first..."
CANCEL_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$EXISTING_SWAP_ID/cancel" \
-H "Authorization: Bearer $TOKEN")
echo " Cancelled existing swap"
fi
print_result 0 "No blocking active swaps"
echo ""
# Step 6: Create a new employee swap
echo "Step 6: Creating employee swap..."
TODAY=$(date +%Y-%m-%d)
SWAP_DATA="{
\"employeeId\": $FIRST_EMPLOYEE_ID,
\"targetDepartmentId\": $TARGET_DEPT_ID,
\"targetContractorId\": $TARGET_CONTRACTOR_ID,
\"swapReason\": \"FinishedEarly\",
\"reasonDetails\": \"Test swap from backend test script\",
\"workCompletionPercentage\": 75,
\"swapDate\": \"$TODAY\"
}"
CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/employee-swaps" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$SWAP_DATA")
SWAP_ID=$(echo $CREATE_RESPONSE | jq '.id')
if [ "$SWAP_ID" != "null" ] && [ -n "$SWAP_ID" ]; then
print_result 0 "Swap created successfully (ID: $SWAP_ID)"
echo " Employee: $(echo $CREATE_RESPONSE | jq -r '.employee_name')"
echo " From: $(echo $CREATE_RESPONSE | jq -r '.original_department_name') → To: $(echo $CREATE_RESPONSE | jq -r '.target_department_name')"
echo " Status: $(echo $CREATE_RESPONSE | jq -r '.status')"
else
print_result 1 "Failed to create swap"
echo " Response: $CREATE_RESPONSE"
exit 1
fi
echo ""
# Step 7: Verify employee's department was updated
echo "Step 7: Verifying employee's department was updated..."
UPDATED_EMPLOYEE=$(curl -s -X GET "$BASE_URL/users/$FIRST_EMPLOYEE_ID" \
-H "Authorization: Bearer $TOKEN")
NEW_DEPT_ID=$(echo $UPDATED_EMPLOYEE | jq '.department_id')
NEW_CONTRACTOR_ID=$(echo $UPDATED_EMPLOYEE | jq '.contractor_id')
if [ "$NEW_DEPT_ID" == "$TARGET_DEPT_ID" ]; then
print_result 0 "Employee department updated correctly"
echo " New department_id: $NEW_DEPT_ID (was: $FIRST_EMPLOYEE_DEPT)"
echo " New contractor_id: $NEW_CONTRACTOR_ID (was: $FIRST_EMPLOYEE_CONTRACTOR)"
else
print_result 1 "Employee department NOT updated"
echo " Expected: $TARGET_DEPT_ID, Got: $NEW_DEPT_ID"
fi
echo ""
# Step 8: Get swap by ID
echo "Step 8: Fetching swap by ID..."
GET_SWAP_RESPONSE=$(curl -s -X GET "$BASE_URL/employee-swaps/$SWAP_ID" \
-H "Authorization: Bearer $TOKEN")
FETCHED_SWAP_ID=$(echo $GET_SWAP_RESPONSE | jq '.id')
if [ "$FETCHED_SWAP_ID" == "$SWAP_ID" ]; then
print_result 0 "Swap fetched successfully"
else
print_result 1 "Failed to fetch swap"
echo " Response: $GET_SWAP_RESPONSE"
fi
echo ""
# Step 9: Complete the swap (return employee to original department)
echo "Step 9: Completing swap (returning employee to original department)..."
COMPLETE_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$SWAP_ID/complete" \
-H "Authorization: Bearer $TOKEN")
COMPLETED_STATUS=$(echo $COMPLETE_RESPONSE | jq -r '.status')
if [ "$COMPLETED_STATUS" == "Completed" ]; then
print_result 0 "Swap completed successfully"
echo " Status: $COMPLETED_STATUS"
else
print_result 1 "Failed to complete swap"
echo " Response: $COMPLETE_RESPONSE"
fi
echo ""
# Step 10: Verify employee returned to original department
echo "Step 10: Verifying employee returned to original department..."
FINAL_EMPLOYEE=$(curl -s -X GET "$BASE_URL/users/$FIRST_EMPLOYEE_ID" \
-H "Authorization: Bearer $TOKEN")
FINAL_DEPT_ID=$(echo $FINAL_EMPLOYEE | jq '.department_id')
FINAL_CONTRACTOR_ID=$(echo $FINAL_EMPLOYEE | jq '.contractor_id')
if [ "$FINAL_DEPT_ID" == "$FIRST_EMPLOYEE_DEPT" ]; then
print_result 0 "Employee returned to original department"
echo " Final department_id: $FINAL_DEPT_ID"
echo " Final contractor_id: $FINAL_CONTRACTOR_ID"
else
print_result 1 "Employee NOT returned to original department"
echo " Expected: $FIRST_EMPLOYEE_DEPT, Got: $FINAL_DEPT_ID"
fi
echo ""
# Step 11: Test swap cancellation
echo "Step 11: Testing swap cancellation..."
# Create another swap
CREATE_RESPONSE2=$(curl -s -X POST "$BASE_URL/employee-swaps" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$SWAP_DATA")
SWAP_ID2=$(echo $CREATE_RESPONSE2 | jq '.id')
if [ "$SWAP_ID2" != "null" ]; then
echo " Created test swap (ID: $SWAP_ID2)"
# Cancel it
CANCEL_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$SWAP_ID2/cancel" \
-H "Authorization: Bearer $TOKEN")
CANCELLED_STATUS=$(echo $CANCEL_RESPONSE | jq -r '.status')
if [ "$CANCELLED_STATUS" == "Cancelled" ]; then
print_result 0 "Swap cancelled successfully"
else
print_result 1 "Failed to cancel swap"
echo " Response: $CANCEL_RESPONSE"
fi
else
print_result 1 "Failed to create swap for cancellation test"
fi
echo ""
# Summary
echo "========================================"
echo " Test Summary"
echo "========================================"
echo -e "${GREEN}All employee swap backend tests completed!${NC}"
echo ""
echo "Tested operations:"
echo " ✓ Login as SuperAdmin"
echo " ✓ Fetch employees and departments"
echo " ✓ Create employee swap"
echo " ✓ Verify employee department/contractor update"
echo " ✓ Fetch swap by ID"
echo " ✓ Complete swap (return to original)"
echo " ✓ Cancel swap"
echo ""