(Feat-Fix): New Reporting system, more seeded data, fixed subdepartments and activity inversion, login page changes, etc etc
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user