(Feat-Fix): New Reporting system, more seeded data, fixed subdepartments and activity inversion, login page changes, etc etc
This commit is contained in:
@@ -61,7 +61,7 @@ npm run dev
|
||||
### Docker Management
|
||||
|
||||
```bash
|
||||
# Stop MySQL
|
||||
# Stop MySQLclear
|
||||
docker-compose down
|
||||
|
||||
# Stop and remove all data
|
||||
|
||||
82
activities.md
Normal file
82
activities.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Activity Departments and Units of Measurement
|
||||
|
||||
## GROUNDNUT Department
|
||||
|
||||
| # | Activity Name | Sub-Department | Unit of Measurement |
|
||||
|---|---------------|----------------|---------------------|
|
||||
| 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag |
|
||||
| 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag |
|
||||
| 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag |
|
||||
| 4 | Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack) | Loading/Unloading | Per Bag |
|
||||
| 5 | Guthali dhada Pala Tulai Silai Dhaang / Loading (Kernel Heap Weighing Stitching Stack/Loading) | Loading/Unloading | Per Bag |
|
||||
| 6 | Mufali Patthar Bori silai Dhaang (Groundnut Stone Bag Stitching Stack) | Loading/Unloading | Per Bag |
|
||||
| 7 | Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading) | Loading/Unloading | Per Bag |
|
||||
| 8 | Bardana Bandal Loading (Gunny Bundle Loading) | Loading/Unloading | Per Bag |
|
||||
| 9 | Bardana Gatthi Loading/Unloading (Gunny Bale Loading/Unloading) | Loading/Unloading | Per Bag |
|
||||
| 10 | Black Dana Loading/Unloading | Loading/Unloading | Per Bag |
|
||||
| 11 | Pre Cleaner | Pre Cleaning | Fixed Rate-Per Person |
|
||||
| 12 | Destoner | Destoner | Fixed Rate-Per Person |
|
||||
| 13 | Water | Water | Fixed Rate-Per Person |
|
||||
| 14 | Decordicater | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
|
||||
| 15 | Round Chalna (Round Sieving) | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
|
||||
| 16 | Cleaning | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person |
|
||||
| 17 | Round Chalna No.1 (Round Sieving No.1) | Round Chalna No.1 | Fixed Rate-Per Person |
|
||||
| 18 | Dala - Chomu & Jaipur (Branch - Chomu & Jaipur) | Loading/Unloading | Per Bag |
|
||||
|
||||
## DANA Department
|
||||
|
||||
| # | Activity Name | Sub-Department | Unit of Measurement |
|
||||
|---|---------------|----------------|---------------------|
|
||||
| 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag |
|
||||
| 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag |
|
||||
| 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag |
|
||||
| 4 | Tulai Silai Dhaang Ikai No. 2 Machine ke Pass (Weighing Stitching Stack Unit No. 2 Near Machine) | Loading/Unloading | Per Bag |
|
||||
| 5 | Dana Unloading/Dhaang (Grain Unloading/Stack) | Loading/Unloading | Per Bag |
|
||||
| 6 | Dana Aavak Keep Katai (Grain Arrival Hopper Cutting) | Loading/Unloading | Per Bag |
|
||||
| 7 | Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg (Waste Heap Filling Weighing Stitching Load/Stack 70kg) | Loading/Unloading | Per Bag |
|
||||
| 8 | Kachri Dhaang se loading (Waste Loading from Stack) | Loading/Unloading | Per Bag |
|
||||
| 9 | Keep Katai Khulla Katta (Hopper Cutting Open Bag) | Loading/Unloading | Per Bag |
|
||||
| 10 | Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched) | Loading/Unloading | Per Bag |
|
||||
| 11 | Bardana Paltai (Gunny Turning) | Loading/Unloading | Per Bag |
|
||||
| 12 | Ekai No. 2 me Keep Katai Khula Bag (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Open Bag with Dragging into Tank) | Loading/Unloading | Per Bag |
|
||||
| 13 | Ekai No. 2 me Keep Katai Silai Kholkar (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Opening Stitched with Dragging into Tank) | Loading/Unloading | Per Bag |
|
||||
| 14 | Silai Loading Company Gadi Dala Sahit (Stitching Loading Company Vehicle with Branch) | Loading/Unloading | Per Bag |
|
||||
| 15 | Kachri Bharai Silai Dhaang Chatt Par (Waste Filling Stitching Stack on Roof) | Loading/Unloading | Per Bag |
|
||||
| 16 | Bardana Unloading (Gunny Unloading) | Loading/Unloading | Per Bag |
|
||||
| 17 | Grading | Loading/Unloading | Per Bag |
|
||||
| 18 | Destoner | Destoner | Fixed Rate-Per Person |
|
||||
| 19 | Gravity | Gravity | Fixed Rate-Per Person |
|
||||
| 20 | Tank | Tank | Fixed Rate-Per Person |
|
||||
| 21 | Sortex | Sortex | Fixed Rate-Per Person |
|
||||
| 22 | X-Ray | X-Ray | Fixed Rate-Per Person |
|
||||
| 23 | Kachri (Waste) | Kachri | Fixed Rate-Per Person |
|
||||
| 24 | Other Works | Other Works | Fixed Rate-Per Person |
|
||||
|
||||
## TUKDI Department
|
||||
|
||||
| # | Activity Name | Sub-Department | Unit of Measurement |
|
||||
|---|---------------|----------------|---------------------|
|
||||
| 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag |
|
||||
| 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag |
|
||||
| 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |
|
||||
| 4 | Grading Chalne se Maal Bharai Tulai Silai Loading (Grading Running Material Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag |
|
||||
| 5 | Chilka Bharai silai Dhaang (Husk Filling Stitching Stack) | Loading/Unloading | Per Bag |
|
||||
| 6 | Keep katai Bahar Se (Hopper Cutting from Outside) | Loading/Unloading | Per Bag |
|
||||
| 7 | Keep katai Andar Se (Hopper Cutting from Inside) | Loading/Unloading | Per Bag |
|
||||
| 8 | Cartoon Banai Vacume Bharai Tulai Packing and Dhaang (Carton Making Vacuum Filling Weighing Packing and Stack) | Loading/Unloading | Per Bag |
|
||||
| 9 | Cartoon Banai Vacume Bharai Tulai Packing and Loading (Carton Making Vacuum Filling Weighing Packing and Loading) | Loading/Unloading | Per Bag |
|
||||
| 10 | Katta Paltai (Bag Turning) | Loading/Unloading | Per Bag |
|
||||
| 11 | Dhada Pala Bharai Tulai Silai Dhaang (Heap Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |
|
||||
| 12 | Dhada Pala Bharai Tulai Silai Loading (Heap Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag |
|
||||
| 13 | Sike Maal Ki Silai Dhaang Andar (Roasted Material Stitching Stack Inside) | Loading/Unloading | Per Bag |
|
||||
| 14 | Sike Maal Ki Silai Dhaang Bahar (Roasted Material Stitching Stack Outside) | Loading/Unloading | Per Bag |
|
||||
| 15 | Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside) | Loading/Unloading | Per Bag |
|
||||
| 16 | Tank | Tank | Fixed Rate-Per Person |
|
||||
| 17 | Grader (Machine) | Grader (Machine) | Fixed Rate-Per Person |
|
||||
| 18 | Sortex | Sortex | Fixed Rate-Per Person |
|
||||
| 19 | X-Ray | X-Ray | Fixed Rate-Per Person |
|
||||
| 20 | Rejection | Rejection | Fixed Rate-Per Person |
|
||||
| 21 | Store | Store | Fixed Rate-Per Person |
|
||||
| 22 | Roster | Roster | Fixed Rate-Per Person |
|
||||
| 23 | Blancher | Blancher | Fixed Rate-Per Person |
|
||||
| 24 | Other Works | Other Works | Fixed Rate-Per Person |
|
||||
@@ -3,7 +3,7 @@ DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_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
|
||||
|
||||
@@ -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());
|
||||
|
||||
153
backend-deno/routes/activities.ts
Normal file
153
backend-deno/routes/activities.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken } from "../middleware/auth.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
interface Activity {
|
||||
id: number;
|
||||
sub_department_id: number;
|
||||
name: string;
|
||||
unit_of_measurement: string;
|
||||
created_at: string;
|
||||
sub_department_name?: string;
|
||||
department_id?: number;
|
||||
department_name?: string;
|
||||
}
|
||||
|
||||
// Get all activities (with optional filters)
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
const departmentId = params.get("departmentId");
|
||||
|
||||
let query = `
|
||||
SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
|
||||
sd.name as sub_department_name,
|
||||
sd.department_id,
|
||||
d.name as department_name
|
||||
FROM activities a
|
||||
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||
JOIN departments d ON sd.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND a.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND sd.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
query += " ORDER BY d.name, sd.name, a.name";
|
||||
|
||||
const activities = await db.query<Activity[]>(query, queryParams);
|
||||
ctx.response.body = activities;
|
||||
} catch (error) {
|
||||
console.error("Get activities error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get activity by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const activityId = ctx.params.id;
|
||||
|
||||
const activities = await db.query<Activity[]>(
|
||||
`SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
|
||||
sd.name as sub_department_name,
|
||||
sd.department_id,
|
||||
d.name as department_name
|
||||
FROM activities a
|
||||
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||
JOIN departments d ON sd.department_id = d.id
|
||||
WHERE a.id = ?`,
|
||||
[activityId]
|
||||
);
|
||||
|
||||
if (activities.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Activity not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = activities[0];
|
||||
} catch (error) {
|
||||
console.error("Get activity error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Create activity (SuperAdmin only)
|
||||
router.post("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const body = await ctx.request.body.json();
|
||||
const { sub_department_id, name, unit_of_measurement } = body;
|
||||
|
||||
if (!sub_department_id || !name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Sub-department ID and name are required" };
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
|
||||
[sub_department_id, name, unit_of_measurement || "Per Bag"]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = {
|
||||
id: result.lastInsertId,
|
||||
message: "Activity created successfully"
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Create activity error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Update activity
|
||||
router.put("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const activityId = ctx.params.id;
|
||||
const body = await ctx.request.body.json();
|
||||
const { name, unit_of_measurement } = body;
|
||||
|
||||
await db.execute(
|
||||
"UPDATE activities SET name = ?, unit_of_measurement = ? WHERE id = ?",
|
||||
[name, unit_of_measurement, activityId]
|
||||
);
|
||||
|
||||
ctx.response.body = { message: "Activity updated successfully" };
|
||||
} catch (error) {
|
||||
console.error("Update activity error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Delete activity
|
||||
router.delete("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const activityId = ctx.params.id;
|
||||
|
||||
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
|
||||
|
||||
ctx.response.body = { message: "Activity deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete activity error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
183
backend-deno/routes/reports.ts
Normal file
183
backend-deno/routes/reports.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import type { WorkAllocation } from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get completed work allocations for reporting (with optional filters)
|
||||
router.get("/completed-allocations", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
const departmentId = params.get("departmentId");
|
||||
const contractorId = params.get("contractorId");
|
||||
const employeeId = params.get("employeeId");
|
||||
|
||||
let query = `
|
||||
SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
e.phone_number as employee_phone,
|
||||
s.name as supervisor_name,
|
||||
c.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
JOIN users s ON wa.supervisor_id = s.id
|
||||
JOIN users c ON wa.contractor_id = c.id
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.status = 'Completed'
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering - Supervisors can only see their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
if (startDate) {
|
||||
query += " AND wa.completion_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND wa.completion_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
// Department filter (for SuperAdmin)
|
||||
if (departmentId && currentUser.role === "SuperAdmin") {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
// Contractor filter
|
||||
if (contractorId) {
|
||||
query += " AND wa.contractor_id = ?";
|
||||
queryParams.push(contractorId);
|
||||
}
|
||||
|
||||
// Employee filter
|
||||
if (employeeId) {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
query += " ORDER BY wa.completion_date DESC, wa.created_at DESC";
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
|
||||
|
||||
// Calculate summary stats
|
||||
const totalAllocations = allocations.length;
|
||||
const totalAmount = allocations.reduce((sum, a) => sum + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0), 0);
|
||||
const totalUnits = allocations.reduce((sum, a) => sum + (parseFloat(String(a.units)) || 0), 0);
|
||||
|
||||
ctx.response.body = {
|
||||
allocations,
|
||||
summary: {
|
||||
totalAllocations,
|
||||
totalAmount: totalAmount.toFixed(2),
|
||||
totalUnits: totalUnits.toFixed(2),
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get completed allocations report error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get summary statistics for completed work
|
||||
router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
|
||||
let departmentFilter = "";
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
departmentFilter = " AND e.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
let dateFilter = "";
|
||||
if (startDate) {
|
||||
dateFilter += " AND wa.completion_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
dateFilter += " AND wa.completion_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
// Get summary by contractor
|
||||
const byContractor = await db.query<any[]>(`
|
||||
SELECT
|
||||
c.id as contractor_id,
|
||||
c.name as contractor_name,
|
||||
COUNT(*) as total_allocations,
|
||||
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
|
||||
SUM(COALESCE(wa.units, 0)) as total_units
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
JOIN users c ON wa.contractor_id = c.id
|
||||
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||
GROUP BY c.id, c.name
|
||||
ORDER BY total_amount DESC
|
||||
`, queryParams);
|
||||
|
||||
// Get summary by sub-department
|
||||
const bySubDepartment = await db.query<any[]>(`
|
||||
SELECT
|
||||
sd.id as sub_department_id,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
COUNT(*) as total_allocations,
|
||||
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
|
||||
SUM(COALESCE(wa.units, 0)) as total_units
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||
GROUP BY sd.id, sd.name, d.name
|
||||
ORDER BY total_amount DESC
|
||||
`, queryParams);
|
||||
|
||||
// Get summary by activity type
|
||||
const byActivity = await db.query<any[]>(`
|
||||
SELECT
|
||||
COALESCE(wa.activity, 'Standard') as activity,
|
||||
COUNT(*) as total_allocations,
|
||||
SUM(COALESCE(wa.total_amount, wa.rate, 0)) as total_amount,
|
||||
SUM(COALESCE(wa.units, 0)) as total_units
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||
GROUP BY wa.activity
|
||||
ORDER BY total_amount DESC
|
||||
`, queryParams);
|
||||
|
||||
ctx.response.body = {
|
||||
byContractor,
|
||||
bySubDepartment,
|
||||
byActivity,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get report summary error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
479
backend-deno/routes/standard-rates.ts
Normal file
479
backend-deno/routes/standard-rates.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput } from "../middleware/security.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Standard Rate interface
|
||||
interface StandardRate {
|
||||
id: number;
|
||||
sub_department_id: number | null;
|
||||
activity: string | null;
|
||||
rate: number;
|
||||
effective_date: Date;
|
||||
created_by: number;
|
||||
created_at: Date;
|
||||
sub_department_name?: string;
|
||||
department_name?: string;
|
||||
department_id?: number;
|
||||
created_by_name?: string;
|
||||
}
|
||||
|
||||
// Get all standard rates (default rates for comparison)
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const departmentId = params.get("departmentId");
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
const activity = params.get("activity");
|
||||
|
||||
let query = `
|
||||
SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id,
|
||||
u.name as created_by_name
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Supervisors can only see rates for their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND d.id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND d.id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND sr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
if (activity) {
|
||||
query += " AND sr.activity = ?";
|
||||
queryParams.push(activity);
|
||||
}
|
||||
|
||||
query += " ORDER BY sr.effective_date DESC, sr.created_at DESC";
|
||||
|
||||
const rates = await db.query<StandardRate[]>(query, queryParams);
|
||||
ctx.response.body = rates;
|
||||
} catch (error) {
|
||||
console.error("Get standard rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date
|
||||
router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const departmentId = params.get("departmentId");
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
|
||||
// Get contractor rates
|
||||
let contractorQuery = `
|
||||
SELECT
|
||||
cr.id,
|
||||
'contractor' as rate_type,
|
||||
cr.contractor_id,
|
||||
u.name as contractor_name,
|
||||
cr.sub_department_id,
|
||||
sd.name as sub_department_name,
|
||||
d.id as department_id,
|
||||
d.name as department_name,
|
||||
cr.activity,
|
||||
cr.rate,
|
||||
cr.effective_date,
|
||||
cr.created_at,
|
||||
NULL as created_by,
|
||||
NULL as created_by_name
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const contractorParams: unknown[] = [];
|
||||
|
||||
if (departmentId) {
|
||||
contractorQuery += " AND d.id = ?";
|
||||
contractorParams.push(departmentId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
contractorQuery += " AND cr.effective_date >= ?";
|
||||
contractorParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
contractorQuery += " AND cr.effective_date <= ?";
|
||||
contractorParams.push(endDate);
|
||||
}
|
||||
|
||||
// Get standard rates
|
||||
let standardQuery = `
|
||||
SELECT
|
||||
sr.id,
|
||||
'standard' as rate_type,
|
||||
NULL as contractor_id,
|
||||
NULL as contractor_name,
|
||||
sr.sub_department_id,
|
||||
sd.name as sub_department_name,
|
||||
d.id as department_id,
|
||||
d.name as department_name,
|
||||
sr.activity,
|
||||
sr.rate,
|
||||
sr.effective_date,
|
||||
sr.created_at,
|
||||
sr.created_by,
|
||||
u.name as created_by_name
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const standardParams: unknown[] = [];
|
||||
|
||||
if (departmentId) {
|
||||
standardQuery += " AND d.id = ?";
|
||||
standardParams.push(departmentId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
standardQuery += " AND sr.effective_date >= ?";
|
||||
standardParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
standardQuery += " AND sr.effective_date <= ?";
|
||||
standardParams.push(endDate);
|
||||
}
|
||||
|
||||
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
|
||||
const standardRates = await db.query<any[]>(standardQuery, standardParams);
|
||||
|
||||
// Combine and sort by date
|
||||
const allRates = [...contractorRates, ...standardRates].sort((a, b) => {
|
||||
const dateA = new Date(a.effective_date).getTime();
|
||||
const dateB = new Date(b.effective_date).getTime();
|
||||
return dateB - dateA; // Descending order
|
||||
});
|
||||
|
||||
ctx.response.body = {
|
||||
allRates,
|
||||
summary: {
|
||||
totalContractorRates: contractorRates.length,
|
||||
totalStandardRates: standardRates.length,
|
||||
totalRates: allRates.length,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get all rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Compare contractor rates with standard rates
|
||||
router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const contractorId = params.get("contractorId");
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
|
||||
let departmentFilter = "";
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
departmentFilter = " AND d.id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
// Get standard rates
|
||||
let standardQuery = `
|
||||
SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE 1=1 ${departmentFilter}
|
||||
`;
|
||||
|
||||
if (subDepartmentId) {
|
||||
standardQuery += " AND sr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
standardQuery += " ORDER BY sr.effective_date DESC";
|
||||
|
||||
const standardRates = await db.query<StandardRate[]>(standardQuery, queryParams);
|
||||
|
||||
// Get contractor rates for comparison
|
||||
let contractorQuery = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const contractorParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
contractorQuery += " AND d.id = ?";
|
||||
contractorParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
if (contractorId) {
|
||||
contractorQuery += " AND cr.contractor_id = ?";
|
||||
contractorParams.push(contractorId);
|
||||
}
|
||||
|
||||
if (subDepartmentId) {
|
||||
contractorQuery += " AND cr.sub_department_id = ?";
|
||||
contractorParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
contractorQuery += " ORDER BY cr.effective_date DESC";
|
||||
|
||||
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
|
||||
|
||||
// Build comparison data
|
||||
const comparisons = contractorRates.map(cr => {
|
||||
// Find matching standard rate
|
||||
const matchingStandard = standardRates.find(sr =>
|
||||
sr.sub_department_id === cr.sub_department_id &&
|
||||
sr.activity === cr.activity
|
||||
);
|
||||
|
||||
const standardRate = matchingStandard?.rate || 0;
|
||||
const contractorRate = cr.rate || 0;
|
||||
const difference = contractorRate - standardRate;
|
||||
const percentageDiff = standardRate > 0 ? ((difference / standardRate) * 100).toFixed(2) : null;
|
||||
|
||||
return {
|
||||
...cr,
|
||||
standard_rate: standardRate,
|
||||
difference,
|
||||
percentage_difference: percentageDiff,
|
||||
is_above_standard: difference > 0,
|
||||
is_below_standard: difference < 0,
|
||||
};
|
||||
});
|
||||
|
||||
ctx.response.body = {
|
||||
standardRates,
|
||||
contractorRates,
|
||||
comparisons,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Compare rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Create standard rate (Supervisor or SuperAdmin)
|
||||
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as {
|
||||
subDepartmentId?: number;
|
||||
activity?: string;
|
||||
rate: number;
|
||||
effectiveDate: string;
|
||||
};
|
||||
const { subDepartmentId, activity, rate, effectiveDate } = body;
|
||||
|
||||
if (!rate || !effectiveDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields (rate, effectiveDate)" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify sub-department belongs to supervisor's department if supervisor
|
||||
if (subDepartmentId && currentUser.role === "Supervisor") {
|
||||
const subDepts = await db.query<any[]>(
|
||||
"SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?",
|
||||
[subDepartmentId, currentUser.departmentId]
|
||||
);
|
||||
|
||||
if (subDepts.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Sub-department not in your department" };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)",
|
||||
[subDepartmentId || null, sanitizedActivity, rate, effectiveDate, currentUser.id]
|
||||
);
|
||||
|
||||
const newRate = await db.query<StandardRate[]>(
|
||||
`SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
u.name as created_by_name
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
WHERE sr.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRate[0];
|
||||
} catch (error) {
|
||||
console.error("Create standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Update standard rate
|
||||
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const rateId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
|
||||
const { rate, activity, effectiveDate } = body;
|
||||
|
||||
// Verify rate exists and user has access
|
||||
let query = `
|
||||
SELECT sr.*, d.id as department_id
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE sr.id = ?
|
||||
`;
|
||||
const params: unknown[] = [rateId];
|
||||
|
||||
const existing = await db.query<any[]>(query, params);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Standard rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only update rates in their department
|
||||
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Access denied - rate not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const updateParams: unknown[] = [];
|
||||
|
||||
if (rate !== undefined) {
|
||||
updates.push("rate = ?");
|
||||
updateParams.push(rate);
|
||||
}
|
||||
if (activity !== undefined) {
|
||||
updates.push("activity = ?");
|
||||
updateParams.push(sanitizeInput(activity));
|
||||
}
|
||||
if (effectiveDate !== undefined) {
|
||||
updates.push("effective_date = ?");
|
||||
updateParams.push(effectiveDate);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
updateParams.push(rateId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`,
|
||||
updateParams
|
||||
);
|
||||
|
||||
const updatedRate = await db.query<StandardRate[]>(
|
||||
`SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
u.name as created_by_name
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
WHERE sr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRate[0];
|
||||
} catch (error) {
|
||||
console.error("Update standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Delete standard rate
|
||||
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const rateId = ctx.params.id;
|
||||
|
||||
// Verify rate exists and user has access
|
||||
const existing = await db.query<any[]>(
|
||||
`SELECT sr.*, d.id as department_id
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE sr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Standard rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only delete rates in their department
|
||||
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Access denied - rate not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]);
|
||||
ctx.response.body = { message: "Standard rate deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -32,54 +32,187 @@ async function seedDatabase() {
|
||||
console.log(" ℹ️ Departments already exist");
|
||||
}
|
||||
|
||||
// 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]
|
||||
);
|
||||
}
|
||||
console.log(" ✅ Sub-departments created");
|
||||
} else {
|
||||
console.log(" ℹ️ Sub-departments already exist");
|
||||
|
||||
// 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 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,45 +398,220 @@ 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) {
|
||||
// Get contractor IDs for employee assignment
|
||||
const contractorIds: { [key: string]: number } = {};
|
||||
for (const con of contractors) {
|
||||
const result = await db.query<{ id: number }[]>(
|
||||
"SELECT id FROM users WHERE username = ?",
|
||||
[con.username]
|
||||
);
|
||||
if (result.length > 0) {
|
||||
contractorIds[con.username] = result[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
const employees = [
|
||||
// Groundnut Department Employees - Under Ramesh Patel
|
||||
{
|
||||
username: "employee1",
|
||||
name: "Employee One",
|
||||
email: "employee1@workallocate.com",
|
||||
phone: "9876543220",
|
||||
aadhar: "345678901234",
|
||||
bankAccount: "3456789012345678",
|
||||
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: "PUNB0001234"
|
||||
bankIfsc: "PUNB0002222"
|
||||
},
|
||||
{
|
||||
username: "employee2",
|
||||
name: "Employee Two",
|
||||
email: "employee2@workallocate.com",
|
||||
phone: "9876543221",
|
||||
aadhar: "456789012345",
|
||||
bankAccount: "4567890123456789",
|
||||
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: "BARB0001234"
|
||||
bankIfsc: "BARB0004444"
|
||||
},
|
||||
{
|
||||
username: "employee3",
|
||||
name: "Employee Three",
|
||||
email: "employee3@workallocate.com",
|
||||
phone: "9876543222",
|
||||
aadhar: "567890123456",
|
||||
bankAccount: "5678901234567890",
|
||||
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: "ICIC0001234"
|
||||
bankIfsc: "ICIC0005555"
|
||||
},
|
||||
// Dana Department Employees - Under Gopal Sharma
|
||||
{
|
||||
username: "rampal.verma.dn1",
|
||||
name: "Rampal Verma",
|
||||
email: "rampal.verma@workallocate.com",
|
||||
deptId: danaId,
|
||||
contractorUsername: "gopal.sharma.dana",
|
||||
phone: "9876502001",
|
||||
aadhar: "666677778888",
|
||||
bankAccount: "30100666677788",
|
||||
bankName: "State Bank of India",
|
||||
bankIfsc: "SBIN0006666"
|
||||
},
|
||||
{
|
||||
username: "lakhan.singh.dn2",
|
||||
name: "Lakhan Singh",
|
||||
email: "lakhan.singh@workallocate.com",
|
||||
deptId: danaId,
|
||||
contractorUsername: "gopal.sharma.dana",
|
||||
phone: "9876502002",
|
||||
aadhar: "777788889999",
|
||||
bankAccount: "30100777788899",
|
||||
bankName: "Punjab National Bank",
|
||||
bankIfsc: "PUNB0007777"
|
||||
},
|
||||
{
|
||||
username: "bharat.meena.dn3",
|
||||
name: "Bharat Meena",
|
||||
email: "bharat.meena@workallocate.com",
|
||||
deptId: danaId,
|
||||
contractorUsername: "gopal.sharma.dana",
|
||||
phone: "9876502003",
|
||||
aadhar: "888899990000",
|
||||
bankAccount: "30100888899900",
|
||||
bankName: "HDFC Bank",
|
||||
bankIfsc: "HDFC0008888"
|
||||
},
|
||||
// Dana Department Employees - Under Mohan Yadav
|
||||
{
|
||||
username: "kailash.patel.dn4",
|
||||
name: "Kailash Patel",
|
||||
email: "kailash.patel@workallocate.com",
|
||||
deptId: danaId,
|
||||
contractorUsername: "mohan.yadav.dana",
|
||||
phone: "9876502004",
|
||||
aadhar: "999900001111",
|
||||
bankAccount: "30100999900011",
|
||||
bankName: "Bank of Baroda",
|
||||
bankIfsc: "BARB0009999"
|
||||
},
|
||||
{
|
||||
username: "shyam.gupta.dn5",
|
||||
name: "Shyam Gupta",
|
||||
email: "shyam.gupta@workallocate.com",
|
||||
deptId: danaId,
|
||||
contractorUsername: "mohan.yadav.dana",
|
||||
phone: "9876502005",
|
||||
aadhar: "000011112222",
|
||||
bankAccount: "30100000011122",
|
||||
bankName: "ICICI Bank",
|
||||
bankIfsc: "ICIC0000000"
|
||||
},
|
||||
// Tudki Department Employees - Under Suresh Kumar
|
||||
{
|
||||
username: "ganesh.kumar.tk1",
|
||||
name: "Ganesh Kumar",
|
||||
email: "ganesh.kumar@workallocate.com",
|
||||
deptId: tudkiId,
|
||||
contractorUsername: "suresh.kumar.tudki",
|
||||
phone: "9876503001",
|
||||
aadhar: "112233445566",
|
||||
bankAccount: "30100112233445",
|
||||
bankName: "State Bank of India",
|
||||
bankIfsc: "SBIN0001122"
|
||||
},
|
||||
{
|
||||
username: "naresh.yadav.tk2",
|
||||
name: "Naresh Yadav",
|
||||
email: "naresh.yadav@workallocate.com",
|
||||
deptId: tudkiId,
|
||||
contractorUsername: "suresh.kumar.tudki",
|
||||
phone: "9876503002",
|
||||
aadhar: "223344556677",
|
||||
bankAccount: "30100223344556",
|
||||
bankName: "Punjab National Bank",
|
||||
bankIfsc: "PUNB0002233"
|
||||
},
|
||||
{
|
||||
username: "mukesh.sharma.tk3",
|
||||
name: "Mukesh Sharma",
|
||||
email: "mukesh.sharma@workallocate.com",
|
||||
deptId: tudkiId,
|
||||
contractorUsername: "suresh.kumar.tudki",
|
||||
phone: "9876503003",
|
||||
aadhar: "334455667788",
|
||||
bankAccount: "30100334455667",
|
||||
bankName: "HDFC Bank",
|
||||
bankIfsc: "HDFC0003344"
|
||||
},
|
||||
// Tudki Department Employees - Under Dinesh Gupta
|
||||
{
|
||||
username: "pappu.singh.tk4",
|
||||
name: "Pappu Singh",
|
||||
email: "pappu.singh@workallocate.com",
|
||||
deptId: tudkiId,
|
||||
contractorUsername: "dinesh.gupta.tudki",
|
||||
phone: "9876503004",
|
||||
aadhar: "445566778899",
|
||||
bankAccount: "30100445566778",
|
||||
bankName: "Bank of Baroda",
|
||||
bankIfsc: "BARB0004455"
|
||||
},
|
||||
{
|
||||
username: "deepak.verma.tk5",
|
||||
name: "Deepak Verma",
|
||||
email: "deepak.verma@workallocate.com",
|
||||
deptId: tudkiId,
|
||||
contractorUsername: "dinesh.gupta.tudki",
|
||||
phone: "9876503005",
|
||||
aadhar: "556677889900",
|
||||
bankAccount: "30100556677889",
|
||||
bankName: "ICICI Bank",
|
||||
bankIfsc: "ICIC0005566"
|
||||
},
|
||||
{
|
||||
username: "rahul.meena.tk6",
|
||||
name: "Rahul Meena",
|
||||
email: "rahul.meena@workallocate.com",
|
||||
deptId: tudkiId,
|
||||
contractorUsername: "dinesh.gupta.tudki",
|
||||
phone: "9876503006",
|
||||
aadhar: "667788990011",
|
||||
bankAccount: "30100667788990",
|
||||
bankName: "Axis Bank",
|
||||
bankIfsc: "UTIB0006677"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -245,39 +621,76 @@ async function seedDatabase() {
|
||||
[emp.username]
|
||||
);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
// 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");
|
||||
} else {
|
||||
console.log(" ℹ️ Contractor rates already exist");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
23
backend/database/migrations/add_standard_rates.sql
Normal file
23
backend/database/migrations/add_standard_rates.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Migration: Add standard_rates table
|
||||
-- Run this migration to add the standard_rates table for supervisor-managed default rates
|
||||
|
||||
-- Create standard_rates table (default rates for comparison with contractor rates)
|
||||
CREATE TABLE IF NOT EXISTS standard_rates (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
sub_department_id INT,
|
||||
activity VARCHAR(255),
|
||||
rate DECIMAL(10, 2) NOT NULL,
|
||||
effective_date DATE NOT NULL,
|
||||
created_by INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (sub_department_id) REFERENCES sub_departments(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for standard_rates
|
||||
CREATE INDEX idx_standard_rates_subdept ON standard_rates(sub_department_id);
|
||||
CREATE INDEX idx_standard_rates_date ON standard_rates(effective_date);
|
||||
CREATE INDEX idx_standard_rates_created_by ON standard_rates(created_by);
|
||||
|
||||
-- Verify table was created
|
||||
SELECT 'standard_rates table created successfully' AS status;
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: admin123
|
||||
MYSQL_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
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -9,8 +9,11 @@ import { AttendancePage } from './pages/AttendancePage';
|
||||
import { RatesPage } from './pages/RatesPage';
|
||||
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { ReportingPage } from './pages/ReportingPage';
|
||||
import { StandardRatesPage } from './pages/StandardRatesPage';
|
||||
import { AllRatesPage } from './pages/AllRatesPage';
|
||||
|
||||
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps';
|
||||
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates';
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
const [activePage, setActivePage] = useState<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 />;
|
||||
}
|
||||
|
||||
@@ -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
302
src/pages/AllRatesPage.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { RefreshCw, Search, Filter, Eye, Calendar } from 'lucide-react';
|
||||
import { Card, CardContent } from '../components/ui/Card';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input, Select } from '../components/ui/Input';
|
||||
import { api } from '../services/api';
|
||||
import { useDepartments } from '../hooks/useDepartments';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
export const AllRatesPage: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { departments } = useDepartments();
|
||||
const [allRates, setAllRates] = useState<any[]>([]);
|
||||
const [summary, setSummary] = useState<{ totalContractorRates: number; totalStandardRates: number; totalRates: number } | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Filters
|
||||
const [filters, setFilters] = useState({
|
||||
departmentId: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
rateType: '', // 'contractor' | 'standard' | ''
|
||||
});
|
||||
|
||||
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||
|
||||
// Fetch all rates
|
||||
const fetchAllRates = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filters.departmentId) params.departmentId = parseInt(filters.departmentId);
|
||||
if (filters.startDate) params.startDate = filters.startDate;
|
||||
if (filters.endDate) params.endDate = filters.endDate;
|
||||
|
||||
const data = await api.getAllRates(params);
|
||||
setAllRates(data.allRates);
|
||||
setSummary(data.summary);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch rates');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuperAdmin) {
|
||||
fetchAllRates();
|
||||
}
|
||||
}, [isSuperAdmin]);
|
||||
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFilters(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
fetchAllRates();
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
departmentId: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
rateType: '',
|
||||
});
|
||||
setTimeout(fetchAllRates, 0);
|
||||
};
|
||||
|
||||
// Filter rates based on search and rate type
|
||||
const filteredRates = useMemo(() => {
|
||||
let rates = allRates;
|
||||
|
||||
// Filter by rate type
|
||||
if (filters.rateType) {
|
||||
rates = rates.filter(r => r.rate_type === filters.rateType);
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
rates = rates.filter(r =>
|
||||
r.contractor_name?.toLowerCase().includes(query) ||
|
||||
r.sub_department_name?.toLowerCase().includes(query) ||
|
||||
r.department_name?.toLowerCase().includes(query) ||
|
||||
r.activity?.toLowerCase().includes(query) ||
|
||||
r.created_by_name?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
return rates;
|
||||
}, [allRates, searchQuery, filters.rateType]);
|
||||
|
||||
// Access check
|
||||
if (!isSuperAdmin) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="text-center py-12">
|
||||
<Eye size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">Access Restricted</h2>
|
||||
<p className="text-gray-500">
|
||||
This page is only accessible to Super Admin accounts.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card>
|
||||
<div className="border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Eye className="text-purple-600" size={24} />
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-800">All Rates Overview</h2>
|
||||
<p className="text-sm text-gray-500">View all contractor and standard rates across all departments</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent>
|
||||
{/* Filters */}
|
||||
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Filter size={18} className="text-gray-500" />
|
||||
<h3 className="font-medium text-gray-700">Filters</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Select
|
||||
label="Department"
|
||||
name="departmentId"
|
||||
value={filters.departmentId}
|
||||
onChange={handleFilterChange}
|
||||
options={[
|
||||
{ value: '', label: 'All Departments' },
|
||||
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Rate Type"
|
||||
name="rateType"
|
||||
value={filters.rateType}
|
||||
onChange={handleFilterChange}
|
||||
options={[
|
||||
{ value: '', label: 'All Types' },
|
||||
{ value: 'contractor', label: 'Contractor Rates' },
|
||||
{ value: 'standard', label: 'Standard Rates' },
|
||||
]}
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||
<Input
|
||||
type="date"
|
||||
name="startDate"
|
||||
value={filters.startDate}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||
<Input
|
||||
type="date"
|
||||
name="endDate"
|
||||
value={filters.endDate}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button onClick={applyFilters} size="sm">
|
||||
Apply Filters
|
||||
</Button>
|
||||
<Button variant="outline" onClick={clearFilters} size="sm">
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
{summary && (
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="text-sm text-blue-600 font-medium">Total Rates</div>
|
||||
<div className="text-2xl font-bold text-blue-800">{summary.totalRates}</div>
|
||||
</div>
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
||||
<div className="text-sm text-orange-600 font-medium">Contractor Rates</div>
|
||||
<div className="text-2xl font-bold text-orange-800">{summary.totalContractorRates}</div>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="text-sm text-green-600 font-medium">Standard Rates</div>
|
||||
<div className="text-2xl font-bold text-green-800">{summary.totalStandardRates}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Refresh */}
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by contractor, department, activity..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={fetchAllRates}>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<div className="text-center py-8">Loading all rates...</div>
|
||||
) : filteredRates.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Contractor</TableHead>
|
||||
<TableHead>Department</TableHead>
|
||||
<TableHead>Sub-Department</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
<TableHead>Rate (₹)</TableHead>
|
||||
<TableHead>Effective Date</TableHead>
|
||||
<TableHead>Created By</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredRates.map((rate, idx) => (
|
||||
<TableRow key={`${rate.rate_type}-${rate.id}-${idx}`}>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
rate.rate_type === 'contractor'
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{rate.rate_type === 'contractor' ? 'Contractor' : 'Standard'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{rate.contractor_name || '-'}
|
||||
</TableCell>
|
||||
<TableCell>{rate.department_name || '-'}</TableCell>
|
||||
<TableCell>{rate.sub_department_name || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
rate.activity === 'Loading' || rate.activity === 'Unloading'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{rate.activity || 'Standard'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-600 font-semibold">₹{rate.rate}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar size={14} className="text-gray-400" />
|
||||
{new Date(rate.effective_date).toLocaleDateString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-500">
|
||||
{rate.created_by_name || '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No rates found. Adjust your filters or check back later.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { 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,12 +171,14 @@ export const DashboardPage: React.FC = () => {
|
||||
e => e.role === 'Contractor' && e.department_id === supervisor.department_id
|
||||
);
|
||||
|
||||
const supervisorNode: HierarchyNode = {
|
||||
id: supervisor.id,
|
||||
name: supervisor.name,
|
||||
role: 'Supervisor',
|
||||
department: supervisor.department_name || '',
|
||||
children: deptContractors.map(contractor => {
|
||||
// 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
|
||||
);
|
||||
@@ -178,15 +190,15 @@ export const DashboardPage: React.FC = () => {
|
||||
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 || 'Loading',
|
||||
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),
|
||||
@@ -195,7 +207,42 @@ export const DashboardPage: React.FC = () => {
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Add unassigned employees node if there are any
|
||||
if (unassignedEmployees.length > 0) {
|
||||
contractorNodes.push({
|
||||
id: -supervisor.department_id!, // Negative ID to avoid conflicts
|
||||
name: 'Unassigned (Swapped)',
|
||||
role: 'Contractor',
|
||||
department: supervisor.department_name || '',
|
||||
children: unassignedEmployees.map(emp => {
|
||||
const empAttendance = attendance.find(a => a.employee_id === emp.id);
|
||||
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
|
||||
|
||||
return {
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
role: 'Employee',
|
||||
department: emp.department_name || '',
|
||||
subDepartment: empAllocation?.sub_department_name || '-',
|
||||
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
|
||||
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
|
||||
inTime: empAttendance?.check_in_time?.substring(0, 5),
|
||||
outTime: empAttendance?.check_out_time?.substring(0, 5),
|
||||
remark: empAttendance?.remark,
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const supervisorNode: HierarchyNode = {
|
||||
id: supervisor.id,
|
||||
name: supervisor.name,
|
||||
role: 'Supervisor',
|
||||
department: supervisor.department_name || '',
|
||||
children: contractorNodes,
|
||||
};
|
||||
|
||||
return supervisorNode;
|
||||
@@ -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,11 +487,20 @@ 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>
|
||||
<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 */}
|
||||
<div className="relative max-w-md">
|
||||
@@ -647,11 +738,20 @@ export const DashboardPage: React.FC = () => {
|
||||
<h1 className="text-2xl font-bold text-gray-800">{departmentName} Dashboard</h1>
|
||||
<p className="text-gray-500 mt-1">Daily Attendance & Work Overview</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={refreshAllData}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
Refresh
|
||||
</button>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<Calendar size={18} />
|
||||
Date Range
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative max-w-md">
|
||||
|
||||
@@ -202,16 +202,11 @@ export const LoginPage: React.FC = () => {
|
||||
<div className="w-full border-t border-white/10" />
|
||||
</div>
|
||||
<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
346
src/pages/ReportingPage.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Download, RefreshCw, Search, FileSpreadsheet, Filter } from 'lucide-react';
|
||||
import { Card, CardContent } from '../components/ui/Card';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input, Select } from '../components/ui/Input';
|
||||
import { api } from '../services/api';
|
||||
import { useDepartments } from '../hooks/useDepartments';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
export const ReportingPage: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { departments } = useDepartments();
|
||||
const [allocations, setAllocations] = useState<any[]>([]);
|
||||
const [summary, setSummary] = useState<{ totalAllocations: number; totalAmount: string; totalUnits: string } | null>(null);
|
||||
const [contractors, setContractors] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Filters
|
||||
const [filters, setFilters] = useState({
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
departmentId: '',
|
||||
contractorId: '',
|
||||
});
|
||||
|
||||
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||
|
||||
// Fetch contractors
|
||||
useEffect(() => {
|
||||
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Fetch report data
|
||||
const fetchReport = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filters.startDate) params.startDate = filters.startDate;
|
||||
if (filters.endDate) params.endDate = filters.endDate;
|
||||
if (filters.departmentId && isSuperAdmin) params.departmentId = parseInt(filters.departmentId);
|
||||
if (filters.contractorId) params.contractorId = parseInt(filters.contractorId);
|
||||
|
||||
const data = await api.getCompletedAllocationsReport(params);
|
||||
setAllocations(data.allocations);
|
||||
setSummary(data.summary);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch report');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchReport();
|
||||
}, []);
|
||||
|
||||
// Filter allocations based on search
|
||||
const filteredAllocations = useMemo(() => {
|
||||
if (!searchQuery) return allocations;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return allocations.filter(a =>
|
||||
a.employee_name?.toLowerCase().includes(query) ||
|
||||
a.contractor_name?.toLowerCase().includes(query) ||
|
||||
a.sub_department_name?.toLowerCase().includes(query) ||
|
||||
a.activity?.toLowerCase().includes(query) ||
|
||||
a.department_name?.toLowerCase().includes(query)
|
||||
);
|
||||
}, [allocations, searchQuery]);
|
||||
|
||||
// Export to Excel (CSV format)
|
||||
const exportToExcel = () => {
|
||||
if (filteredAllocations.length === 0) {
|
||||
alert('No data to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// Define headers
|
||||
const headers = [
|
||||
'ID',
|
||||
'Employee Name',
|
||||
'Employee Phone',
|
||||
'Contractor',
|
||||
'Department',
|
||||
'Sub-Department',
|
||||
'Activity',
|
||||
'Assigned Date',
|
||||
'Completion Date',
|
||||
'Rate (₹)',
|
||||
'Units',
|
||||
'Total Amount (₹)',
|
||||
'Status',
|
||||
];
|
||||
|
||||
// Map data to rows
|
||||
const rows = filteredAllocations.map(a => [
|
||||
a.id,
|
||||
a.employee_name || '',
|
||||
a.employee_phone || '',
|
||||
a.contractor_name || '',
|
||||
a.department_name || '',
|
||||
a.sub_department_name || '',
|
||||
a.activity || 'Standard',
|
||||
a.assigned_date ? new Date(a.assigned_date).toLocaleDateString() : '',
|
||||
a.completion_date ? new Date(a.completion_date).toLocaleDateString() : '',
|
||||
a.rate || 0,
|
||||
a.units || '',
|
||||
a.total_amount || a.rate || 0,
|
||||
a.status,
|
||||
]);
|
||||
|
||||
// Create CSV content
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
].join('\n');
|
||||
|
||||
// Add summary at the end
|
||||
const summaryRows = [
|
||||
'',
|
||||
'SUMMARY',
|
||||
`Total Allocations,${summary?.totalAllocations || 0}`,
|
||||
`Total Amount (₹),${summary?.totalAmount || 0}`,
|
||||
`Total Units,${summary?.totalUnits || 0}`,
|
||||
];
|
||||
|
||||
const fullContent = csvContent + '\n' + summaryRows.join('\n');
|
||||
|
||||
// Create and download file
|
||||
const blob = new Blob([fullContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `completed_work_allocations_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFilters(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
fetchReport();
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
departmentId: '',
|
||||
contractorId: '',
|
||||
});
|
||||
setTimeout(fetchReport, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card>
|
||||
<div className="border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileSpreadsheet className="text-green-600" size={24} />
|
||||
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2>
|
||||
</div>
|
||||
<Button onClick={exportToExcel} disabled={filteredAllocations.length === 0}>
|
||||
<Download size={16} className="mr-2" />
|
||||
Export to Excel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent>
|
||||
{/* Filters */}
|
||||
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Filter size={18} className="text-gray-500" />
|
||||
<h3 className="font-medium text-gray-700">Filters</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||
<Input
|
||||
type="date"
|
||||
name="startDate"
|
||||
value={filters.startDate}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||
<Input
|
||||
type="date"
|
||||
name="endDate"
|
||||
value={filters.endDate}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
{isSuperAdmin && (
|
||||
<Select
|
||||
label="Department"
|
||||
name="departmentId"
|
||||
value={filters.departmentId}
|
||||
onChange={handleFilterChange}
|
||||
options={[
|
||||
{ value: '', label: 'All Departments' },
|
||||
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
label="Contractor"
|
||||
name="contractorId"
|
||||
value={filters.contractorId}
|
||||
onChange={handleFilterChange}
|
||||
options={[
|
||||
{ value: '', label: 'All Contractors' },
|
||||
...contractors.map(c => ({ value: String(c.id), label: c.name }))
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button onClick={applyFilters} size="sm">
|
||||
Apply Filters
|
||||
</Button>
|
||||
<Button variant="outline" onClick={clearFilters} size="sm">
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
{summary && (
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="text-sm text-blue-600 font-medium">Total Completed</div>
|
||||
<div className="text-2xl font-bold text-blue-800">{summary.totalAllocations}</div>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="text-sm text-green-600 font-medium">Total Amount</div>
|
||||
<div className="text-2xl font-bold text-green-800">₹{parseFloat(summary.totalAmount).toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
||||
<div className="text-sm text-purple-600 font-medium">Total Units</div>
|
||||
<div className="text-2xl font-bold text-purple-800">{parseFloat(summary.totalUnits).toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Refresh */}
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by employee, contractor, department..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={fetchReport}>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<div className="text-center py-8">Loading report data...</div>
|
||||
) : filteredAllocations.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Employee</TableHead>
|
||||
<TableHead>Contractor</TableHead>
|
||||
<TableHead>Department</TableHead>
|
||||
<TableHead>Sub-Department</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
<TableHead>Assigned</TableHead>
|
||||
<TableHead>Completed</TableHead>
|
||||
<TableHead>Rate (₹)</TableHead>
|
||||
<TableHead>Units</TableHead>
|
||||
<TableHead>Total (₹)</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredAllocations.map((allocation) => {
|
||||
const rate = parseFloat(allocation.rate) || 0;
|
||||
const units = parseFloat(allocation.units) || 0;
|
||||
const total = parseFloat(allocation.total_amount) || rate;
|
||||
|
||||
return (
|
||||
<TableRow key={allocation.id}>
|
||||
<TableCell>{allocation.id}</TableCell>
|
||||
<TableCell className="font-medium">{allocation.employee_name || '-'}</TableCell>
|
||||
<TableCell>{allocation.contractor_name || '-'}</TableCell>
|
||||
<TableCell>{allocation.department_name || '-'}</TableCell>
|
||||
<TableCell>{allocation.sub_department_name || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
allocation.activity === 'Loading' || allocation.activity === 'Unloading'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{allocation.activity || 'Standard'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
|
||||
<TableCell>
|
||||
{allocation.completion_date
|
||||
? new Date(allocation.completion_date).toLocaleDateString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>₹{rate.toFixed(2)}</TableCell>
|
||||
<TableCell>{units > 0 ? units : '-'}</TableCell>
|
||||
<TableCell className="font-semibold text-green-600">₹{total.toFixed(2)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No completed work allocations found. Adjust your filters or check back later.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
523
src/pages/StandardRatesPage.tsx
Normal file
523
src/pages/StandardRatesPage.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { RefreshCw, Trash2, Edit, DollarSign, Search, Scale, ArrowUpDown } from 'lucide-react';
|
||||
import { Card, CardContent } from '../components/ui/Card';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input, Select } from '../components/ui/Input';
|
||||
import { api } from '../services/api';
|
||||
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
export const StandardRatesPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'list' | 'add' | 'compare'>('list');
|
||||
const { user } = useAuth();
|
||||
const { departments } = useDepartments();
|
||||
const [standardRates, setStandardRates] = useState<any[]>([]);
|
||||
const [contractors, setContractors] = useState<any[]>([]);
|
||||
const [comparisons, setComparisons] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
subDepartmentId: '',
|
||||
activity: '',
|
||||
rate: '',
|
||||
effectiveDate: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
const [selectedDept, setSelectedDept] = useState('');
|
||||
const { subDepartments } = useSubDepartments(selectedDept);
|
||||
const [formError, setFormError] = useState('');
|
||||
const [formLoading, setFormLoading] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
|
||||
// Compare filters
|
||||
const [compareContractorId, setCompareContractorId] = useState('');
|
||||
|
||||
const isSupervisor = user?.role === 'Supervisor';
|
||||
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
|
||||
|
||||
// Fetch standard rates
|
||||
const fetchStandardRates = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await api.getStandardRates();
|
||||
setStandardRates(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch standard rates');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch contractors
|
||||
const fetchContractors = async () => {
|
||||
try {
|
||||
const data = await api.getUsers({ role: 'Contractor' });
|
||||
setContractors(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch contractors:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch comparison data
|
||||
const fetchComparison = async () => {
|
||||
if (!compareContractorId) {
|
||||
setComparisons([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.compareRates({ contractorId: parseInt(compareContractorId) });
|
||||
setComparisons(data.comparisons);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch comparison');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStandardRates();
|
||||
fetchContractors();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSupervisor && user?.department_id) {
|
||||
setSelectedDept(String(user.department_id));
|
||||
}
|
||||
}, [isSupervisor, user?.department_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'compare' && compareContractorId) {
|
||||
fetchComparison();
|
||||
}
|
||||
}, [activeTab, compareContractorId]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
subDepartmentId: '',
|
||||
activity: '',
|
||||
rate: '',
|
||||
effectiveDate: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
setEditingId(null);
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.rate || !formData.effectiveDate) {
|
||||
setFormError('Rate and effective date are required');
|
||||
return;
|
||||
}
|
||||
|
||||
setFormLoading(true);
|
||||
setFormError('');
|
||||
|
||||
try {
|
||||
if (editingId) {
|
||||
await api.updateStandardRate(editingId, {
|
||||
rate: parseFloat(formData.rate),
|
||||
activity: formData.activity || undefined,
|
||||
effectiveDate: formData.effectiveDate,
|
||||
});
|
||||
} else {
|
||||
await api.createStandardRate({
|
||||
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined,
|
||||
activity: formData.activity || undefined,
|
||||
rate: parseFloat(formData.rate),
|
||||
effectiveDate: formData.effectiveDate,
|
||||
});
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setActiveTab('list');
|
||||
fetchStandardRates();
|
||||
} catch (err: any) {
|
||||
setFormError(err.message || 'Failed to save rate');
|
||||
} finally {
|
||||
setFormLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (rate: any) => {
|
||||
setFormData({
|
||||
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '',
|
||||
activity: rate.activity || '',
|
||||
rate: String(rate.rate),
|
||||
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0],
|
||||
});
|
||||
if (rate.department_id) {
|
||||
setSelectedDept(String(rate.department_id));
|
||||
}
|
||||
setEditingId(rate.id);
|
||||
setActiveTab('add');
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this standard rate?')) return;
|
||||
try {
|
||||
await api.deleteStandardRate(id);
|
||||
fetchStandardRates();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Failed to delete rate');
|
||||
}
|
||||
};
|
||||
|
||||
// Filter rates based on search
|
||||
const filteredRates = useMemo(() => {
|
||||
if (!searchQuery) return standardRates;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return standardRates.filter(rate =>
|
||||
rate.sub_department_name?.toLowerCase().includes(query) ||
|
||||
rate.department_name?.toLowerCase().includes(query) ||
|
||||
rate.activity?.toLowerCase().includes(query) ||
|
||||
rate.created_by_name?.toLowerCase().includes(query)
|
||||
);
|
||||
}, [standardRates, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card>
|
||||
<div className="border-b border-gray-200">
|
||||
<div className="flex space-x-8 px-6">
|
||||
<button
|
||||
onClick={() => { setActiveTab('list'); resetForm(); }}
|
||||
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'list'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Standard Rates
|
||||
</button>
|
||||
{canManageRates && (
|
||||
<button
|
||||
onClick={() => setActiveTab('add')}
|
||||
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'add'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{editingId ? 'Edit Rate' : 'Add Standard Rate'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setActiveTab('compare')}
|
||||
className={`py-4 px-2 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'compare'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Scale size={16} className="inline mr-1" />
|
||||
Compare Rates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent>
|
||||
{activeTab === 'list' && (
|
||||
<div>
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="relative min-w-[300px] flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by sub-department, activity..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={fetchStandardRates}>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Standard Rates</strong> are default rates set by supervisors for sub-departments and activities.
|
||||
These are used as benchmarks to compare against contractor-specific rates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8">Loading standard rates...</div>
|
||||
) : filteredRates.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHead>Department</TableHead>
|
||||
<TableHead>Sub-Department</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
<TableHead>Rate (₹)</TableHead>
|
||||
<TableHead>Effective Date</TableHead>
|
||||
<TableHead>Created By</TableHead>
|
||||
{canManageRates && <TableHead>Actions</TableHead>}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredRates.map((rate) => (
|
||||
<TableRow key={rate.id}>
|
||||
<TableCell>{rate.department_name || '-'}</TableCell>
|
||||
<TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
rate.activity === 'Loading' || rate.activity === 'Unloading'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{rate.activity || 'Standard'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-600 font-semibold">₹{rate.rate}</span>
|
||||
</TableCell>
|
||||
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-gray-500">{rate.created_by_name || '-'}</TableCell>
|
||||
{canManageRates && (
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(rate)}
|
||||
className="text-blue-600"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(rate.id)}
|
||||
className="text-red-600"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No standard rates configured yet. Add one to get started!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'add' && canManageRates && (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
{editingId ? 'Edit Standard Rate' : 'Add New Standard Rate'}
|
||||
</h3>
|
||||
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<h4 className="font-medium text-yellow-800 mb-2">About Standard Rates</h4>
|
||||
<p className="text-sm text-yellow-700">
|
||||
Standard rates serve as default benchmarks for sub-departments and activities.
|
||||
Contractor rates can be compared against these to identify deviations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{formError && (
|
||||
<div className="p-3 bg-red-100 text-red-700 rounded-md">
|
||||
{formError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{isSupervisor ? (
|
||||
<Input
|
||||
label="Department"
|
||||
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
label="Department"
|
||||
value={selectedDept}
|
||||
onChange={(e) => setSelectedDept(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: 'Select Department' },
|
||||
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
label="Sub-Department"
|
||||
name="subDepartmentId"
|
||||
value={formData.subDepartmentId}
|
||||
onChange={handleInputChange}
|
||||
disabled={!!editingId}
|
||||
options={[
|
||||
{ value: '', label: 'All Sub-Departments' },
|
||||
...subDepartments.map(s => ({ value: String(s.id), label: s.name }))
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Activity Type"
|
||||
name="activity"
|
||||
value={formData.activity}
|
||||
onChange={handleInputChange}
|
||||
options={[
|
||||
{ value: '', label: 'Standard (Default)' },
|
||||
{ value: 'Loading', label: 'Loading (per unit)' },
|
||||
{ value: 'Unloading', label: 'Unloading (per unit)' },
|
||||
{ value: 'Other', label: 'Other' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
|
||||
? "Rate per Unit (₹)"
|
||||
: "Standard Rate (₹)"}
|
||||
name="rate"
|
||||
type="number"
|
||||
value={formData.rate}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter rate amount"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Effective Date"
|
||||
name="effectiveDate"
|
||||
type="date"
|
||||
value={formData.effectiveDate}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={formLoading}>
|
||||
{formLoading ? 'Saving...' : (
|
||||
<>
|
||||
<DollarSign size={16} className="mr-2" />
|
||||
{editingId ? 'Update Rate' : 'Add Standard Rate'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'compare' && (
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
<ArrowUpDown size={20} className="inline mr-2" />
|
||||
Compare Contractor Rates vs Standard Rates
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-4 items-end">
|
||||
<div className="w-64">
|
||||
<Select
|
||||
label="Select Contractor"
|
||||
value={compareContractorId}
|
||||
onChange={(e) => setCompareContractorId(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: 'Select Contractor' },
|
||||
...contractors.map(c => ({ value: String(c.id), label: c.name }))
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={fetchComparison} disabled={!compareContractorId}>
|
||||
Compare
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8">Loading comparison...</div>
|
||||
) : comparisons.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHead>Sub-Department</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
<TableHead>Contractor Rate (₹)</TableHead>
|
||||
<TableHead>Standard Rate (₹)</TableHead>
|
||||
<TableHead>Difference (₹)</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{comparisons.map((comp, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
comp.activity === 'Loading' || comp.activity === 'Unloading'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{comp.activity || 'Standard'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="font-semibold">₹{comp.rate}</TableCell>
|
||||
<TableCell className="text-gray-600">₹{comp.standard_rate}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`font-semibold ${
|
||||
comp.difference > 0 ? 'text-red-600' :
|
||||
comp.difference < 0 ? 'text-green-600' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{comp.difference > 0 ? '+' : ''}₹{comp.difference.toFixed(2)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{comp.is_above_standard ? (
|
||||
<span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">
|
||||
Above Standard ({comp.percentage_difference}%)
|
||||
</span>
|
||||
) : comp.is_below_standard ? (
|
||||
<span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">
|
||||
Below Standard ({comp.percentage_difference}%)
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">
|
||||
At Standard
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : compareContractorId ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No rates found for this contractor to compare.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Select a contractor to compare their rates against standard rates.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -253,6 +253,108 @@ class ApiService {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
259
test_employee_swap.sh
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Employee Swap Backend Test Script
|
||||
# Tests the employee swap functionality including create, complete, and cancel operations
|
||||
|
||||
BASE_URL="http://localhost:3000/api"
|
||||
ADMIN_USER="admin"
|
||||
ADMIN_PASS="admin123"
|
||||
|
||||
echo "========================================"
|
||||
echo " Employee Swap Backend Test"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print test results
|
||||
print_result() {
|
||||
if [ $1 -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ PASS${NC}: $2"
|
||||
else
|
||||
echo -e "${RED}✗ FAIL${NC}: $2"
|
||||
fi
|
||||
}
|
||||
|
||||
# Step 1: Login as SuperAdmin
|
||||
echo "Step 1: Logging in as SuperAdmin..."
|
||||
LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$ADMIN_USER\",\"password\":\"$ADMIN_PASS\"}")
|
||||
|
||||
TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.token')
|
||||
|
||||
if [ "$TOKEN" != "null" ] && [ -n "$TOKEN" ]; then
|
||||
print_result 0 "Login successful"
|
||||
echo " Token: ${TOKEN:0:30}..."
|
||||
else
|
||||
print_result 1 "Login failed"
|
||||
echo " Response: $LOGIN_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 2: Get list of employees
|
||||
echo "Step 2: Fetching employees..."
|
||||
EMPLOYEES_RESPONSE=$(curl -s -X GET "$BASE_URL/users?role=Employee" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
EMPLOYEE_COUNT=$(echo $EMPLOYEES_RESPONSE | jq 'length')
|
||||
if [ "$EMPLOYEE_COUNT" -gt 0 ]; then
|
||||
print_result 0 "Found $EMPLOYEE_COUNT employees"
|
||||
FIRST_EMPLOYEE_ID=$(echo $EMPLOYEES_RESPONSE | jq '.[0].id')
|
||||
FIRST_EMPLOYEE_NAME=$(echo $EMPLOYEES_RESPONSE | jq -r '.[0].name')
|
||||
FIRST_EMPLOYEE_DEPT=$(echo $EMPLOYEES_RESPONSE | jq '.[0].department_id')
|
||||
FIRST_EMPLOYEE_CONTRACTOR=$(echo $EMPLOYEES_RESPONSE | jq '.[0].contractor_id')
|
||||
echo " Test employee: $FIRST_EMPLOYEE_NAME (ID: $FIRST_EMPLOYEE_ID)"
|
||||
echo " Current department_id: $FIRST_EMPLOYEE_DEPT"
|
||||
echo " Current contractor_id: $FIRST_EMPLOYEE_CONTRACTOR"
|
||||
else
|
||||
print_result 1 "No employees found"
|
||||
echo " Response: $EMPLOYEES_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 3: Get list of departments
|
||||
echo "Step 3: Fetching departments..."
|
||||
DEPTS_RESPONSE=$(curl -s -X GET "$BASE_URL/departments" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
DEPT_COUNT=$(echo $DEPTS_RESPONSE | jq 'length')
|
||||
if [ "$DEPT_COUNT" -gt 1 ]; then
|
||||
print_result 0 "Found $DEPT_COUNT departments"
|
||||
# Find a different department than the employee's current one
|
||||
TARGET_DEPT_ID=$(echo $DEPTS_RESPONSE | jq --argjson current "$FIRST_EMPLOYEE_DEPT" '[.[] | select(.id != $current)][0].id')
|
||||
TARGET_DEPT_NAME=$(echo $DEPTS_RESPONSE | jq -r --argjson current "$FIRST_EMPLOYEE_DEPT" '[.[] | select(.id != $current)][0].name')
|
||||
echo " Target department: $TARGET_DEPT_NAME (ID: $TARGET_DEPT_ID)"
|
||||
else
|
||||
print_result 1 "Need at least 2 departments for swap test"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 4: Get contractors in target department
|
||||
echo "Step 4: Fetching contractors in target department..."
|
||||
CONTRACTORS_RESPONSE=$(curl -s -X GET "$BASE_URL/users?role=Contractor&departmentId=$TARGET_DEPT_ID" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
CONTRACTOR_COUNT=$(echo $CONTRACTORS_RESPONSE | jq 'length')
|
||||
if [ "$CONTRACTOR_COUNT" -gt 0 ]; then
|
||||
TARGET_CONTRACTOR_ID=$(echo $CONTRACTORS_RESPONSE | jq '.[0].id')
|
||||
TARGET_CONTRACTOR_NAME=$(echo $CONTRACTORS_RESPONSE | jq -r '.[0].name')
|
||||
print_result 0 "Found contractor: $TARGET_CONTRACTOR_NAME (ID: $TARGET_CONTRACTOR_ID)"
|
||||
else
|
||||
TARGET_CONTRACTOR_ID="null"
|
||||
echo -e "${YELLOW}⚠ WARNING${NC}: No contractors in target department, will swap without contractor"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 5: Check for existing active swaps
|
||||
echo "Step 5: Checking for existing active swaps..."
|
||||
EXISTING_SWAPS=$(curl -s -X GET "$BASE_URL/employee-swaps?status=Active" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
ACTIVE_SWAP_FOR_EMPLOYEE=$(echo $EXISTING_SWAPS | jq --argjson empId "$FIRST_EMPLOYEE_ID" '[.[] | select(.employee_id == $empId)][0]')
|
||||
if [ "$ACTIVE_SWAP_FOR_EMPLOYEE" != "null" ]; then
|
||||
EXISTING_SWAP_ID=$(echo $ACTIVE_SWAP_FOR_EMPLOYEE | jq '.id')
|
||||
echo -e "${YELLOW}⚠ WARNING${NC}: Employee already has active swap (ID: $EXISTING_SWAP_ID), cancelling it first..."
|
||||
|
||||
CANCEL_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$EXISTING_SWAP_ID/cancel" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
echo " Cancelled existing swap"
|
||||
fi
|
||||
print_result 0 "No blocking active swaps"
|
||||
echo ""
|
||||
|
||||
# Step 6: Create a new employee swap
|
||||
echo "Step 6: Creating employee swap..."
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
SWAP_DATA="{
|
||||
\"employeeId\": $FIRST_EMPLOYEE_ID,
|
||||
\"targetDepartmentId\": $TARGET_DEPT_ID,
|
||||
\"targetContractorId\": $TARGET_CONTRACTOR_ID,
|
||||
\"swapReason\": \"FinishedEarly\",
|
||||
\"reasonDetails\": \"Test swap from backend test script\",
|
||||
\"workCompletionPercentage\": 75,
|
||||
\"swapDate\": \"$TODAY\"
|
||||
}"
|
||||
|
||||
CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/employee-swaps" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$SWAP_DATA")
|
||||
|
||||
SWAP_ID=$(echo $CREATE_RESPONSE | jq '.id')
|
||||
if [ "$SWAP_ID" != "null" ] && [ -n "$SWAP_ID" ]; then
|
||||
print_result 0 "Swap created successfully (ID: $SWAP_ID)"
|
||||
echo " Employee: $(echo $CREATE_RESPONSE | jq -r '.employee_name')"
|
||||
echo " From: $(echo $CREATE_RESPONSE | jq -r '.original_department_name') → To: $(echo $CREATE_RESPONSE | jq -r '.target_department_name')"
|
||||
echo " Status: $(echo $CREATE_RESPONSE | jq -r '.status')"
|
||||
else
|
||||
print_result 1 "Failed to create swap"
|
||||
echo " Response: $CREATE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 7: Verify employee's department was updated
|
||||
echo "Step 7: Verifying employee's department was updated..."
|
||||
UPDATED_EMPLOYEE=$(curl -s -X GET "$BASE_URL/users/$FIRST_EMPLOYEE_ID" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
NEW_DEPT_ID=$(echo $UPDATED_EMPLOYEE | jq '.department_id')
|
||||
NEW_CONTRACTOR_ID=$(echo $UPDATED_EMPLOYEE | jq '.contractor_id')
|
||||
|
||||
if [ "$NEW_DEPT_ID" == "$TARGET_DEPT_ID" ]; then
|
||||
print_result 0 "Employee department updated correctly"
|
||||
echo " New department_id: $NEW_DEPT_ID (was: $FIRST_EMPLOYEE_DEPT)"
|
||||
echo " New contractor_id: $NEW_CONTRACTOR_ID (was: $FIRST_EMPLOYEE_CONTRACTOR)"
|
||||
else
|
||||
print_result 1 "Employee department NOT updated"
|
||||
echo " Expected: $TARGET_DEPT_ID, Got: $NEW_DEPT_ID"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 8: Get swap by ID
|
||||
echo "Step 8: Fetching swap by ID..."
|
||||
GET_SWAP_RESPONSE=$(curl -s -X GET "$BASE_URL/employee-swaps/$SWAP_ID" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
FETCHED_SWAP_ID=$(echo $GET_SWAP_RESPONSE | jq '.id')
|
||||
if [ "$FETCHED_SWAP_ID" == "$SWAP_ID" ]; then
|
||||
print_result 0 "Swap fetched successfully"
|
||||
else
|
||||
print_result 1 "Failed to fetch swap"
|
||||
echo " Response: $GET_SWAP_RESPONSE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 9: Complete the swap (return employee to original department)
|
||||
echo "Step 9: Completing swap (returning employee to original department)..."
|
||||
COMPLETE_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$SWAP_ID/complete" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
COMPLETED_STATUS=$(echo $COMPLETE_RESPONSE | jq -r '.status')
|
||||
if [ "$COMPLETED_STATUS" == "Completed" ]; then
|
||||
print_result 0 "Swap completed successfully"
|
||||
echo " Status: $COMPLETED_STATUS"
|
||||
else
|
||||
print_result 1 "Failed to complete swap"
|
||||
echo " Response: $COMPLETE_RESPONSE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 10: Verify employee returned to original department
|
||||
echo "Step 10: Verifying employee returned to original department..."
|
||||
FINAL_EMPLOYEE=$(curl -s -X GET "$BASE_URL/users/$FIRST_EMPLOYEE_ID" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
FINAL_DEPT_ID=$(echo $FINAL_EMPLOYEE | jq '.department_id')
|
||||
FINAL_CONTRACTOR_ID=$(echo $FINAL_EMPLOYEE | jq '.contractor_id')
|
||||
|
||||
if [ "$FINAL_DEPT_ID" == "$FIRST_EMPLOYEE_DEPT" ]; then
|
||||
print_result 0 "Employee returned to original department"
|
||||
echo " Final department_id: $FINAL_DEPT_ID"
|
||||
echo " Final contractor_id: $FINAL_CONTRACTOR_ID"
|
||||
else
|
||||
print_result 1 "Employee NOT returned to original department"
|
||||
echo " Expected: $FIRST_EMPLOYEE_DEPT, Got: $FINAL_DEPT_ID"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 11: Test swap cancellation
|
||||
echo "Step 11: Testing swap cancellation..."
|
||||
# Create another swap
|
||||
CREATE_RESPONSE2=$(curl -s -X POST "$BASE_URL/employee-swaps" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$SWAP_DATA")
|
||||
|
||||
SWAP_ID2=$(echo $CREATE_RESPONSE2 | jq '.id')
|
||||
if [ "$SWAP_ID2" != "null" ]; then
|
||||
echo " Created test swap (ID: $SWAP_ID2)"
|
||||
|
||||
# Cancel it
|
||||
CANCEL_RESPONSE=$(curl -s -X PUT "$BASE_URL/employee-swaps/$SWAP_ID2/cancel" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
CANCELLED_STATUS=$(echo $CANCEL_RESPONSE | jq -r '.status')
|
||||
if [ "$CANCELLED_STATUS" == "Cancelled" ]; then
|
||||
print_result 0 "Swap cancelled successfully"
|
||||
else
|
||||
print_result 1 "Failed to cancel swap"
|
||||
echo " Response: $CANCEL_RESPONSE"
|
||||
fi
|
||||
else
|
||||
print_result 1 "Failed to create swap for cancellation test"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "========================================"
|
||||
echo " Test Summary"
|
||||
echo "========================================"
|
||||
echo -e "${GREEN}All employee swap backend tests completed!${NC}"
|
||||
echo ""
|
||||
echo "Tested operations:"
|
||||
echo " ✓ Login as SuperAdmin"
|
||||
echo " ✓ Fetch employees and departments"
|
||||
echo " ✓ Create employee swap"
|
||||
echo " ✓ Verify employee department/contractor update"
|
||||
echo " ✓ Fetch swap by ID"
|
||||
echo " ✓ Complete swap (return to original)"
|
||||
echo " ✓ Cancel swap"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user