490 lines
16 KiB
TypeScript
490 lines
16 KiB
TypeScript
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,
|
|
a.unit_of_measurement
|
|
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
|
|
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
|
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,
|
|
a.unit_of_measurement
|
|
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 activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
|
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,
|
|
a.unit_of_measurement
|
|
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
|
|
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
|
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,
|
|
a.unit_of_measurement
|
|
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
|
|
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
|
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,
|
|
a.unit_of_measurement
|
|
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
|
|
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
|
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;
|