(Feat-Fix): Lots of fixes done, reporting system fixed, stricter types

This commit is contained in:
2025-12-19 18:48:05 +00:00
parent 01400ad4e1
commit 865e0bf00e
61 changed files with 10072 additions and 6645 deletions

View File

@@ -21,7 +21,7 @@ router.get("/", authenticateToken, async (ctx) => {
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,
@@ -33,19 +33,19 @@ router.get("/", authenticateToken, async (ctx) => {
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) {
@@ -59,7 +59,7 @@ router.get("/", authenticateToken, async (ctx) => {
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,
@@ -69,15 +69,15 @@ router.get("/:id", authenticateToken, async (ctx) => {
JOIN sub_departments sd ON a.sub_department_id = sd.id
JOIN departments d ON sd.department_id = d.id
WHERE a.id = ?`,
[activityId]
[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);
@@ -92,55 +92,61 @@ router.post("/", authenticateToken, async (ctx) => {
const user = getCurrentUser(ctx);
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;
}
// Get the sub-department to check department ownership
const subDepts = await db.query<{ department_id: number }[]>(
"SELECT department_id FROM sub_departments WHERE id = ?",
[sub_department_id]
[sub_department_id],
);
if (subDepts.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Sub-department not found" };
return;
}
const subDeptDepartmentId = subDepts[0].department_id;
// Check authorization
if (user.role === 'Supervisor' && user.departmentId !== subDeptDepartmentId) {
if (
user.role === "Supervisor" && user.departmentId !== subDeptDepartmentId
) {
ctx.response.status = 403;
ctx.response.body = { error: "You can only create activities for your own department" };
ctx.response.body = {
error: "You can only create activities for your own department",
};
return;
}
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" };
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"]
[sub_department_id, name, unit_of_measurement || "Per Bag"],
);
ctx.response.status = 201;
ctx.response.body = {
id: result.lastInsertId,
message: "Activity created successfully"
ctx.response.body = {
id: result.insertId,
message: "Activity created successfully",
};
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Activity already exists in this sub-department" };
ctx.response.body = {
error: "Activity already exists in this sub-department",
};
return;
}
console.error("Create activity error:", error);
@@ -155,12 +161,12 @@ router.put("/:id", authenticateToken, async (ctx) => {
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]
[name, unit_of_measurement, activityId],
);
ctx.response.body = { message: "Activity updated successfully" };
} catch (error) {
console.error("Update activity error:", error);
@@ -174,39 +180,43 @@ router.delete("/:id", authenticateToken, async (ctx) => {
try {
const user = getCurrentUser(ctx);
const activityId = ctx.params.id;
// Get the activity and its sub-department to check department ownership
const activities = await db.query<Activity[]>(
`SELECT a.*, sd.department_id
FROM activities a
JOIN sub_departments sd ON a.sub_department_id = sd.id
WHERE a.id = ?`,
[activityId]
[activityId],
);
if (activities.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Activity not found" };
return;
}
const activity = activities[0] as Activity & { department_id: number };
// Check authorization
if (user.role === 'Supervisor' && user.departmentId !== activity.department_id) {
if (
user.role === "Supervisor" && user.departmentId !== activity.department_id
) {
ctx.response.status = 403;
ctx.response.body = { error: "You can only delete activities from your own department" };
ctx.response.body = {
error: "You can only delete activities from your own department",
};
return;
}
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" };
return;
}
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
ctx.response.body = { message: "Activity deleted successfully" };
} catch (error) {
console.error("Delete activity error:", error);

View File

@@ -1,21 +1,37 @@
import { Router } from "@oak/oak";
import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import type { Attendance, CheckInOutRequest, User, UpdateAttendanceStatusRequest, AttendanceStatus } from "../types/index.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import type {
Attendance,
AttendanceStatus,
CheckInOutRequest,
JWTPayload,
UpdateAttendanceStatusRequest,
User,
} from "../types/index.ts";
const router = new Router();
// Get all attendance records
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const employeeId = params.get("employeeId");
const startDate = params.get("startDate");
const endDate = params.get("endDate");
const status = params.get("status");
let query = `
router.get(
"/",
authenticateToken,
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try {
const currentUser: JWTPayload = getCurrentUser(ctx);
const params: URLSearchParams = ctx.request.url.searchParams;
const employeeId: string | null = params.get("employeeId");
const startDate: string | null = params.get("startDate");
const endDate: string | null = params.get("endDate");
const status: string | null = params.get("status");
let query = `
SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
@@ -28,53 +44,54 @@ router.get("/", authenticateToken, async (ctx) => {
LEFT JOIN users c ON e.contractor_id = c.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Role-based filtering
if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Employee") {
query += " AND a.employee_id = ?";
queryParams.push(currentUser.id);
const queryParams: unknown[] = [];
// Role-based filtering
if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Employee") {
query += " AND a.employee_id = ?";
queryParams.push(currentUser.id);
}
if (employeeId) {
query += " AND a.employee_id = ?";
queryParams.push(employeeId);
}
if (startDate) {
query += " AND a.work_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND a.work_date <= ?";
queryParams.push(endDate);
}
if (status) {
query += " AND a.status = ?";
queryParams.push(status);
}
query += " ORDER BY a.work_date DESC, a.check_in_time DESC";
const records = await db.query<Attendance[]>(query, queryParams);
ctx.response.body = records;
} catch (error) {
console.error("Get attendance error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
if (employeeId) {
query += " AND a.employee_id = ?";
queryParams.push(employeeId);
}
if (startDate) {
query += " AND a.work_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND a.work_date <= ?";
queryParams.push(endDate);
}
if (status) {
query += " AND a.status = ?";
queryParams.push(status);
}
query += " ORDER BY a.work_date DESC, a.check_in_time DESC";
const records = await db.query<Attendance[]>(query, queryParams);
ctx.response.body = records;
} catch (error) {
console.error("Get attendance error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Get attendance by ID
router.get("/:id", authenticateToken, async (ctx) => {
try {
const attendanceId = ctx.params.id;
const records = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
@@ -87,15 +104,15 @@ router.get("/:id", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[attendanceId]
[attendanceId],
);
if (records.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Attendance record not found" };
return;
}
ctx.response.body = records[0];
} catch (error) {
console.error("Get attendance error:", error);
@@ -105,245 +122,260 @@ router.get("/:id", authenticateToken, async (ctx) => {
});
// Check in employee (Supervisor or SuperAdmin)
router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest;
const { employeeId, workDate } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Verify employee exists
let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?";
const employeeParams: unknown[] = [employeeId, "Employee"];
if (currentUser.role === "Supervisor") {
employeeQuery += " AND department_id = ?";
employeeParams.push(currentUser.departmentId);
}
const employees = await db.query<User[]>(employeeQuery, employeeParams);
if (employees.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Employee not found or not in your department" };
return;
}
// Check if already checked in today
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?",
[employeeId, workDate, "CheckedIn"]
);
if (existing.length > 0) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee already checked in today" };
return;
}
const checkInTime = new Date().toISOString().slice(0, 19).replace("T", " ");
const result = await db.execute(
"INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)",
[employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"]
);
const newRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newRecord[0];
} catch (error) {
console.error("Check in error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
router.post(
"/check-in",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest;
const { employeeId, workDate } = body;
// Check out employee (Supervisor or SuperAdmin)
router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest;
const { employeeId, workDate } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Find the check-in record
let query = "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?";
const params: unknown[] = [employeeId, workDate, "CheckedIn"];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const records = await db.query<Attendance[]>(query, params);
if (records.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "No check-in record found for today" };
return;
}
const checkOutTime = new Date().toISOString().slice(0, 19).replace("T", " ");
await db.execute(
"UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?",
[checkOutTime, "CheckedOut", records[0].id]
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[records[0].id]
);
ctx.response.body = updatedRecord[0];
} catch (error) {
console.error("Check out error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Update attendance status (mark as Absent, HalfDay, Late)
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const attendanceId = ctx.params.id;
const body = await ctx.request.body.json() as UpdateAttendanceStatusRequest;
const { status, remark } = body;
// Validate status
const validStatuses: AttendanceStatus[] = ["CheckedIn", "CheckedOut", "Absent", "HalfDay", "Late"];
if (!validStatuses.includes(status)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late" };
return;
}
// Check if record exists
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE id = ?",
[attendanceId]
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Attendance record not found" };
return;
}
// Update the status
await db.execute(
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
[status, remark || null, attendanceId]
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[attendanceId]
);
ctx.response.body = updatedRecord[0];
} catch (error) {
console.error("Update attendance status error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Verify employee exists
let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?";
const employeeParams: unknown[] = [employeeId, "Employee"];
// Mark employee as absent (create absent record)
router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json();
const { employeeId, workDate, remark } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Check if record already exists for this date
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?",
[employeeId, workDate]
);
if (existing.length > 0) {
// Update existing record to Absent
await db.execute(
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
["Absent", remark || "Marked absent", existing[0].id]
if (currentUser.role === "Supervisor") {
employeeQuery += " AND department_id = ?";
employeeParams.push(currentUser.departmentId);
}
const employees = await db.query<User[]>(employeeQuery, employeeParams);
if (employees.length === 0) {
ctx.response.status = 403;
ctx.response.body = {
error: "Employee not found or not in your department",
};
return;
}
// Check if already checked in today
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?",
[employeeId, workDate, "CheckedIn"],
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[existing[0].id]
if (existing.length > 0) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee already checked in today" };
return;
}
const checkInTime = new Date().toISOString().slice(0, 19).replace(
"T",
" ",
);
ctx.response.body = updatedRecord[0];
} else {
// Create new absent record
const result = await db.execute(
"INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)",
[employeeId, currentUser.id, workDate, "Absent", remark || "Marked absent"]
"INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)",
[employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"],
);
const newRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newRecord[0];
} catch (error) {
console.error("Check in error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Check out employee (Supervisor or SuperAdmin)
router.post(
"/check-out",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest;
const { employeeId, workDate } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Find the check-in record
let query =
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?";
const params: unknown[] = [employeeId, workDate, "CheckedIn"];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const records = await db.query<Attendance[]>(query, params);
if (records.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "No check-in record found for today" };
return;
}
const checkOutTime = new Date().toISOString().slice(0, 19).replace(
"T",
" ",
);
await db.execute(
"UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?",
[checkOutTime, "CheckedOut", records[0].id],
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[records[0].id],
);
ctx.response.body = updatedRecord[0];
} catch (error) {
console.error("Check out error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Update attendance status (mark as Absent, HalfDay, Late)
router.put(
"/:id/status",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const attendanceId = ctx.params.id;
const body = await ctx.request.body
.json() as UpdateAttendanceStatusRequest;
const { status, remark } = body;
// Validate status
const validStatuses: AttendanceStatus[] = [
"CheckedIn",
"CheckedOut",
"Absent",
"HalfDay",
"Late",
];
if (!validStatuses.includes(status)) {
ctx.response.status = 400;
ctx.response.body = {
error:
"Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late",
};
return;
}
// Check if record exists
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE id = ?",
[attendanceId],
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Attendance record not found" };
return;
}
// Update the status
await db.execute(
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
[status, remark || null, attendanceId],
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[attendanceId],
);
ctx.response.body = updatedRecord[0];
} catch (error) {
console.error("Update attendance status error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Mark employee as absent (create absent record)
router.post(
"/mark-absent",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json();
const { employeeId, workDate, remark } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Check if record already exists for this date
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?",
[employeeId, workDate],
);
if (existing.length > 0) {
// Update existing record to Absent
await db.execute(
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
["Absent", remark || "Marked absent", existing[0].id],
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
@@ -354,29 +386,68 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newRecord[0];
[existing[0].id],
);
ctx.response.body = updatedRecord[0];
} else {
// Create new absent record
const result = await db.execute(
"INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)",
[
employeeId,
currentUser.id,
workDate,
"Absent",
remark || "Marked absent",
],
);
const newRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newRecord[0];
}
} catch (error) {
console.error("Mark absent error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
} catch (error) {
console.error("Mark absent error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Get attendance summary
router.get("/summary/stats", authenticateToken, 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");
let query = `
router.get(
"/summary/stats",
authenticateToken,
async (
ctx: RouterContext<
"/summary/stats",
Record<string | number, string | undefined>,
State
>,
) => {
try {
const currentUser: JWTPayload = getCurrentUser(ctx);
const params: URLSearchParams = ctx.request.url.searchParams;
const startDate: string | null = params.get("startDate");
const endDate: string | null = params.get("endDate");
const departmentId: string | null = params.get("departmentId");
let query: string = `
SELECT
COUNT(DISTINCT a.employee_id) as total_employees,
COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in,
@@ -387,37 +458,38 @@ router.get("/summary/stats", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON e.department_id = d.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?";
queryParams.push(currentUser.id);
const queryParams: (number | string)[] = [];
if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?";
queryParams.push(currentUser.id);
}
if (startDate) {
query += " AND a.work_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND a.work_date <= ?";
queryParams.push(endDate);
}
if (departmentId) {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
query += " GROUP BY d.id, d.name";
const summary = await db.query(query, queryParams);
ctx.response.body = summary;
} catch (error) {
console.error("Get attendance summary error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
if (startDate) {
query += " AND a.work_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND a.work_date <= ?";
queryParams.push(endDate);
}
if (departmentId) {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
query += " GROUP BY d.id, d.name";
const summary = await db.query(query, queryParams);
ctx.response.body = summary;
} catch (error) {
console.error("Get attendance summary error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;

View File

@@ -1,16 +1,23 @@
import { Router } from "@oak/oak";
import { hash, compare, genSalt } from "bcrypt";
import { compare, genSalt, hash } from "bcrypt";
import { db } from "../config/database.ts";
import { config } from "../config/env.ts";
// Helper function to hash password with proper salt generation
async function hashPassword(password: string): Promise<string> {
const salt = await genSalt(config.BCRYPT_ROUNDS);
return await hash(password, salt);
}
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts";
import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.ts";
import {
authenticateToken,
generateToken,
getCurrentUser,
} from "../middleware/auth.ts";
import { isStrongPassword, sanitizeInput } from "../middleware/security.ts";
import type {
ChangePasswordRequest,
LoginRequest,
User,
} from "../types/index.ts";
const router = new Router();
@@ -19,41 +26,41 @@ router.post("/login", async (ctx) => {
try {
const body = await ctx.request.body.json() as LoginRequest;
const { username, password } = body;
// Input validation
if (!username || !password) {
ctx.response.status = 400;
ctx.response.body = { error: "Username and password required" };
return;
}
// Sanitize input
const sanitizedUsername = sanitizeInput(username);
// Query user
const users = await db.query<User[]>(
"SELECT * FROM users WHERE username = ? AND is_active = TRUE",
[sanitizedUsername]
[sanitizedUsername],
);
if (users.length === 0) {
// Use generic message to prevent user enumeration
ctx.response.status = 401;
ctx.response.body = { error: "Invalid credentials" };
return;
}
const user = users[0];
// Verify password
const validPassword = await compare(password, user.password!);
if (!validPassword) {
ctx.response.status = 401;
ctx.response.body = { error: "Invalid credentials" };
return;
}
// Generate JWT token
const token = await generateToken({
id: user.id,
@@ -61,10 +68,10 @@ router.post("/login", async (ctx) => {
role: user.role,
departmentId: user.department_id,
});
// Return user data without password
const { password: _, ...userWithoutPassword } = user;
ctx.response.body = {
token,
user: userWithoutPassword,
@@ -80,18 +87,18 @@ router.post("/login", async (ctx) => {
router.get("/me", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const users = await db.query<User[]>(
"SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?",
[currentUser.id]
[currentUser.id],
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
ctx.response.body = users[0];
} catch (error) {
console.error("Get user error:", error);
@@ -106,14 +113,14 @@ router.post("/change-password", authenticateToken, async (ctx) => {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as ChangePasswordRequest;
const { currentPassword, newPassword } = body;
// Input validation
if (!currentPassword || !newPassword) {
ctx.response.status = 400;
ctx.response.body = { error: "Current and new password required" };
return;
}
// Validate new password strength (only enforce in production or if explicitly enabled)
if (config.isProduction()) {
const passwordCheck = isStrongPassword(newPassword);
@@ -127,37 +134,37 @@ router.post("/change-password", authenticateToken, async (ctx) => {
ctx.response.body = { error: "Password must be at least 6 characters" };
return;
}
// Get current password hash
const users = await db.query<User[]>(
"SELECT password FROM users WHERE id = ?",
[currentUser.id]
[currentUser.id],
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Verify current password
const validPassword = await compare(currentPassword, users[0].password!);
if (!validPassword) {
ctx.response.status = 401;
ctx.response.body = { error: "Current password is incorrect" };
return;
}
// Hash new password with configured rounds
const hashedPassword = await hashPassword(newPassword);
// Update password
await db.execute(
"UPDATE users SET password = ? WHERE id = ?",
[hashedPassword, currentUser.id]
[hashedPassword, currentUser.id],
);
ctx.response.body = { message: "Password changed successfully" };
} catch (error) {
console.error("Change password error:", error);

View File

@@ -1,19 +1,32 @@
import { Router } from "@oak/oak";
import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { ContractorRate, CreateContractorRateRequest, User } from "../types/index.ts";
import type {
ContractorRate,
CreateContractorRateRequest,
User,
} from "../types/index.ts";
const router = new Router();
// Get contractor rates
router.get("/", authenticateToken, async (ctx) => {
try {
const params = ctx.request.url.searchParams;
const contractorId = params.get("contractorId");
const subDepartmentId = params.get("subDepartmentId");
let query = `
router.get(
"/",
authenticateToken,
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try {
const params: URLSearchParams = ctx.request.url.searchParams;
const contractorId: string | null = params.get("contractorId");
const subDepartmentId: string | null = params.get("subDepartmentId");
let query: string = `
SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name,
@@ -26,37 +39,47 @@ router.get("/", authenticateToken, async (ctx) => {
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
WHERE 1=1
`;
const queryParams: unknown[] = [];
if (contractorId) {
query += " AND cr.contractor_id = ?";
queryParams.push(contractorId);
const queryParams: unknown[] = [];
if (contractorId) {
query += " AND cr.contractor_id = ?";
queryParams.push(contractorId);
}
if (subDepartmentId) {
query += " AND cr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
query += " ORDER BY cr.effective_date DESC, cr.created_at DESC";
const rates = await db.query<ContractorRate[]>(query, queryParams);
ctx.response.body = rates;
} catch (error) {
console.error("Get contractor rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
if (subDepartmentId) {
query += " AND cr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
query += " ORDER BY cr.effective_date DESC, cr.created_at DESC";
const rates = await db.query<ContractorRate[]>(query, queryParams);
ctx.response.body = rates;
} catch (error) {
console.error("Get contractor rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Get current rate for a contractor + sub-department combination
router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) => {
try {
const contractorId = ctx.params.contractorId;
const params = ctx.request.url.searchParams;
const subDepartmentId = params.get("subDepartmentId");
let query = `
router.get(
"/contractor/:contractorId/current",
authenticateToken,
async (
ctx: RouterContext<
"/contractor/:contractorId/current",
{ contractorId: string } & Record<string | number, string | undefined>,
State
>,
) => {
try {
const contractorId = ctx.params.contractorId;
const params = ctx.request.url.searchParams;
const subDepartmentId = params.get("subDepartmentId");
let query: string = `
SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name,
@@ -67,72 +90,92 @@ router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) =
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
WHERE cr.contractor_id = ?
`;
const queryParams: unknown[] = [contractorId];
if (subDepartmentId) {
query += " AND cr.sub_department_id = ?";
queryParams.push(subDepartmentId);
const queryParams: unknown[] = [contractorId];
if (subDepartmentId) {
query += " AND cr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
query += " ORDER BY cr.effective_date DESC LIMIT 1";
const rates = await db.query<ContractorRate[]>(query, queryParams);
if (rates.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "No rate found for contractor" };
return;
}
ctx.response.body = rates[0];
} catch (error) {
console.error("Get current rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
query += " ORDER BY cr.effective_date DESC LIMIT 1";
const rates = await db.query<ContractorRate[]>(query, queryParams);
if (rates.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "No rate found for contractor" };
return;
}
ctx.response.body = rates[0];
} catch (error) {
console.error("Get current rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Set contractor 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 CreateContractorRateRequest;
const { contractorId, subDepartmentId, activity, rate, effectiveDate } = body;
if (!contractorId || !rate || !effectiveDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields (contractorId, rate, effectiveDate)" };
return;
}
// Verify contractor exists
const contractors = await db.query<User[]>(
"SELECT * FROM users WHERE id = ? AND role = ?",
[contractorId, "Contractor"]
);
if (contractors.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Contractor not found" };
return;
}
// Supervisors can only set rates for contractors in their department
if (currentUser.role === "Supervisor" && contractors[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Contractor not in your department" };
return;
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const result = await db.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
[contractorId, subDepartmentId || null, sanitizedActivity, rate, effectiveDate]
);
const newRate = await db.query<ContractorRate[]>(
`SELECT cr.*,
router.post(
"/",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateContractorRateRequest;
const { contractorId, subDepartmentId, activity, rate, effectiveDate } =
body;
if (!contractorId || !rate || !effectiveDate) {
ctx.response.status = 400;
ctx.response.body = {
error: "Missing required fields (contractorId, rate, effectiveDate)",
};
return;
}
// Verify contractor exists
const contractors = await db.query<User[]>(
"SELECT * FROM users WHERE id = ? AND role = ?",
[contractorId, "Contractor"],
);
if (contractors.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Contractor not found" };
return;
}
// Supervisors can only set rates for contractors in their department
if (
currentUser.role === "Supervisor" &&
contractors[0].department_id !== currentUser.departmentId
) {
ctx.response.status = 403;
ctx.response.body = { error: "Contractor not in your department" };
return;
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const result: { insertId: number; affectedRows: number } = await db
.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
[
contractorId,
subDepartmentId || null,
sanitizedActivity,
rate,
effectiveDate,
],
);
const newRate: ContractorRate[] = await db.query<ContractorRate[]>(
`SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name,
a.unit_of_measurement
@@ -141,67 +184,82 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
WHERE cr.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newRate[0];
} catch (error) {
console.error("Set contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newRate[0];
} catch (error) {
console.error("Set contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Update contractor rate
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const rateId = ctx.params.id;
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
const { rate, activity, effectiveDate } = body;
const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?",
[rateId]
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Rate not found" };
return;
}
const updates: string[] = [];
const params: unknown[] = [];
if (rate !== undefined) {
updates.push("rate = ?");
params.push(rate);
}
if (activity !== undefined) {
updates.push("activity = ?");
params.push(sanitizeInput(activity));
}
if (effectiveDate !== undefined) {
updates.push("effective_date = ?");
params.push(effectiveDate);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
params.push(rateId);
await db.execute(
`UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`,
params
);
const updatedRate = await db.query<ContractorRate[]>(
`SELECT cr.*,
router.put(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<
"/:id",
{ id: string } & Record<string | number, string | undefined>,
State
>,
) => {
try {
const rateId = ctx.params.id;
const body = await ctx.request.body.json() as {
rate?: number;
activity?: string;
effectiveDate?: string;
};
const { rate, activity, effectiveDate } = body;
const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?",
[rateId],
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Rate not found" };
return;
}
const updates: string[] = [];
const params: unknown[] = [];
if (rate !== undefined) {
updates.push("rate = ?");
params.push(rate);
}
if (activity !== undefined) {
updates.push("activity = ?");
params.push(sanitizeInput(activity));
}
if (effectiveDate !== undefined) {
updates.push("effective_date = ?");
params.push(effectiveDate);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
params.push(rateId);
await db.execute(
`UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`,
params,
);
const updatedRate = await db.query<ContractorRate[]>(
`SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name,
a.unit_of_measurement
@@ -210,40 +268,52 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
WHERE cr.id = ?`,
[rateId]
);
ctx.response.body = updatedRate[0];
} catch (error) {
console.error("Update contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
[rateId],
);
ctx.response.body = updatedRate[0];
} catch (error) {
console.error("Update contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Delete contractor rate
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const rateId = ctx.params.id;
const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?",
[rateId]
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Rate not found" };
return;
router.delete(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<
"/:id",
{ id: string } & Record<string | number, string | undefined>,
State
>,
) => {
try {
const rateId = ctx.params.id;
const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?",
[rateId],
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Rate not found" };
return;
}
await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]);
ctx.response.body = { message: "Rate deleted successfully" };
} catch (error) {
console.error("Delete contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]);
ctx.response.body = { message: "Rate deleted successfully" };
} catch (error) {
console.error("Delete contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;

View File

@@ -1,16 +1,20 @@
import { Router } from "@oak/oak";
import { type Context, Router, type RouterContext } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { Department, SubDepartment } from "../types/index.ts";
const router = new Router();
// Get all departments
router.get("/", authenticateToken, async (ctx) => {
router.get("/", authenticateToken, async (ctx: Context) => {
try {
const departments = await db.query<Department[]>(
"SELECT * FROM departments ORDER BY name"
"SELECT * FROM departments ORDER BY name",
);
ctx.response.body = departments;
} catch (error) {
@@ -21,21 +25,21 @@ router.get("/", authenticateToken, async (ctx) => {
});
// Get department by ID
router.get("/:id", authenticateToken, async (ctx) => {
router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => {
try {
const deptId = ctx.params.id;
const departments = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?",
[deptId]
[deptId],
);
if (departments.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Department not found" };
return;
}
ctx.response.body = departments[0];
} catch (error) {
console.error("Get department error:", error);
@@ -44,70 +48,101 @@ router.get("/:id", authenticateToken, async (ctx) => {
}
});
// Get all sub-departments (for reporting/filtering)
router.get(
"/sub-departments/all",
authenticateToken,
async (ctx: Context) => {
try {
const subDepartments = await db.query<SubDepartment[]>(
"SELECT sd.*, d.name as department_name FROM sub_departments sd LEFT JOIN departments d ON sd.department_id = d.id ORDER BY d.name, sd.name",
);
ctx.response.body = subDepartments;
} catch (error) {
console.error("Get all sub-departments error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Get sub-departments by department ID
router.get("/:id/sub-departments", authenticateToken, async (ctx) => {
try {
const deptId = ctx.params.id;
const subDepartments = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name",
[deptId]
);
ctx.response.body = subDepartments;
} catch (error) {
console.error("Get sub-departments error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
router.get(
"/:id/sub-departments",
authenticateToken,
async (ctx: RouterContext<"/:id/sub-departments">) => {
try {
const deptId = ctx.params.id;
const subDepartments = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name",
[deptId],
);
ctx.response.body = subDepartments;
} catch (error) {
console.error("Get sub-departments error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Create department (SuperAdmin only)
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const body = await ctx.request.body.json() as { name: string };
const { name } = body;
if (!name) {
ctx.response.status = 400;
ctx.response.body = { error: "Department name required" };
return;
router.post(
"/",
authenticateToken,
authorize("SuperAdmin"),
async (ctx: Context) => {
try {
const body = await ctx.request.body.json() as { name: string };
const { name } = body;
if (!name) {
ctx.response.status = 400;
ctx.response.body = { error: "Department name required" };
return;
}
const sanitizedName = sanitizeInput(name);
const result = await db.execute(
"INSERT INTO departments (name) VALUES (?)",
[sanitizedName],
);
const newDepartment = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?",
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newDepartment[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Department already exists" };
return;
}
console.error("Create department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
const sanitizedName = sanitizeInput(name);
const result = await db.execute(
"INSERT INTO departments (name) VALUES (?)",
[sanitizedName]
);
const newDepartment = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?",
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newDepartment[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Department already exists" };
return;
}
console.error("Create department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Create sub-department (SuperAdmin or Supervisor for their own department)
router.post("/sub-departments", authenticateToken, async (ctx) => {
router.post("/sub-departments", authenticateToken, async (ctx: Context) => {
try {
const user = getCurrentUser(ctx);
const body = await ctx.request.body.json() as { department_id: number; name: string };
const body = await ctx.request.body.json() as {
department_id: number;
name: string;
};
const { department_id, name } = body;
if (!name || !department_id) {
ctx.response.status = 400;
ctx.response.body = { error: "Department ID and name are required" };
@@ -115,37 +150,41 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
}
// Check authorization
if (user.role === 'Supervisor' && user.departmentId !== department_id) {
if (user.role === "Supervisor" && user.departmentId !== department_id) {
ctx.response.status = 403;
ctx.response.body = { error: "You can only create sub-departments for your own department" };
ctx.response.body = {
error: "You can only create sub-departments for your own department",
};
return;
}
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" };
return;
}
const sanitizedName = sanitizeInput(name);
const result = await db.execute(
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[department_id, sanitizedName]
[department_id, sanitizedName],
);
const newSubDepartment = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?",
[result.lastInsertId]
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newSubDepartment[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Sub-department already exists in this department" };
ctx.response.body = {
error: "Sub-department already exists in this department",
};
return;
}
console.error("Create sub-department error:", error);
@@ -155,90 +194,108 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
});
// Delete sub-department (SuperAdmin or Supervisor for their own department)
router.delete("/sub-departments/:id", authenticateToken, async (ctx) => {
try {
const user = getCurrentUser(ctx);
const subDeptId = ctx.params.id;
// Get the sub-department to check department ownership
const subDepts = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?",
[subDeptId]
);
if (subDepts.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Sub-department not found" };
return;
router.delete(
"/sub-departments/:id",
authenticateToken,
async (ctx: RouterContext<"/sub-departments/:id">) => {
try {
const user = getCurrentUser(ctx);
const subDeptId = ctx.params.id;
// Get the sub-department to check department ownership
const subDepts = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?",
[subDeptId],
);
if (subDepts.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Sub-department not found" };
return;
}
const subDept = subDepts[0];
// Check authorization
if (
user.role === "Supervisor" &&
user.departmentId !== subDept.department_id
) {
ctx.response.status = 403;
ctx.response.body = {
error: "You can only delete sub-departments from your own department",
};
return;
}
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" };
return;
}
// Delete associated activities first (cascade should handle this, but being explicit)
await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [
subDeptId,
]);
// Delete the sub-department
await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]);
ctx.response.body = { message: "Sub-department deleted successfully" };
} catch (error) {
console.error("Delete sub-department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
const subDept = subDepts[0];
// Check authorization
if (user.role === 'Supervisor' && user.departmentId !== subDept.department_id) {
ctx.response.status = 403;
ctx.response.body = { error: "You can only delete sub-departments from your own department" };
return;
}
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" };
return;
}
// Delete associated activities first (cascade should handle this, but being explicit)
await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [subDeptId]);
// Delete the sub-department
await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]);
ctx.response.body = { message: "Sub-department deleted successfully" };
} catch (error) {
console.error("Delete sub-department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Legacy route for creating sub-department under specific department (SuperAdmin only)
router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const deptId = ctx.params.id;
const body = await ctx.request.body.json() as { name: string };
const { name } = body;
if (!name) {
ctx.response.status = 400;
ctx.response.body = { error: "Name is required" };
return;
router.post(
"/:id/sub-departments",
authenticateToken,
authorize("SuperAdmin"),
async (ctx: RouterContext<"/:id/sub-departments">) => {
try {
const deptId: string | number = ctx.params.id;
const body = await ctx.request.body.json() as { name: string };
const { name } = body;
if (!name) {
ctx.response.status = 400;
ctx.response.body = { error: "Name is required" };
return;
}
const sanitizedName = sanitizeInput(name);
const result = await db.execute(
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[deptId, sanitizedName],
);
const newSubDepartment = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?",
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newSubDepartment[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = {
error: "Sub-department already exists in this department",
};
return;
}
console.error("Create sub-department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
const sanitizedName = sanitizeInput(name);
const result = await db.execute(
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[deptId, sanitizedName]
);
const newSubDepartment = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?",
[result.lastInsertId]
);
ctx.response.status = 201;
ctx.response.body = newSubDepartment[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Sub-department already exists in this department" };
return;
}
console.error("Create sub-department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;

View File

@@ -1,20 +1,30 @@
import { Router } from "@oak/oak";
import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import type { EmployeeSwap, CreateSwapRequest, User } from "../types/index.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import type { CreateSwapRequest, EmployeeSwap, User } from "../types/index.ts";
const router = new Router();
// Get all employee swaps (SuperAdmin only)
router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const params = ctx.request.url.searchParams;
const status = params.get("status");
const employeeId = params.get("employeeId");
const startDate = params.get("startDate");
const endDate = params.get("endDate");
let query = `
router.get(
"/",
authenticateToken,
authorize("SuperAdmin"),
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try {
const params: URLSearchParams = ctx.request.url.searchParams;
const status: string | null = params.get("status");
const employeeId: string | null = params.get("employeeId");
const startDate: string | null = params.get("startDate");
const endDate: string | null = params.get("endDate");
let query = `
SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
@@ -31,46 +41,57 @@ router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
JOIN users sb ON es.swapped_by = sb.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
if (status) {
query += " AND es.status = ?";
queryParams.push(status);
const queryParams: unknown[] = [];
if (status) {
query += " AND es.status = ?";
queryParams.push(status);
}
if (employeeId) {
query += " AND es.employee_id = ?";
queryParams.push(employeeId);
}
if (startDate) {
query += " AND es.swap_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND es.swap_date <= ?";
queryParams.push(endDate);
}
query += " ORDER BY es.created_at DESC";
const swaps = await db.query<EmployeeSwap[]>(query, queryParams);
ctx.response.body = swaps;
} catch (error) {
console.error("Get employee swaps error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
if (employeeId) {
query += " AND es.employee_id = ?";
queryParams.push(employeeId);
}
if (startDate) {
query += " AND es.swap_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND es.swap_date <= ?";
queryParams.push(endDate);
}
query += " ORDER BY es.created_at DESC";
const swaps = await db.query<EmployeeSwap[]>(query, queryParams);
ctx.response.body = swaps;
} catch (error) {
console.error("Get employee swaps error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Get swap by ID
router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const swapId = ctx.params.id;
const swaps = await db.query<EmployeeSwap[]>(
`SELECT es.*,
router.get(
"/:id",
authenticateToken,
authorize("SuperAdmin"),
async (
ctx: RouterContext<
"/:id",
{ id: string } & Record<string | number, string | undefined>,
State
>,
) => {
try {
const swapId = ctx.params.id;
const swaps = await db.query<EmployeeSwap[]>(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
@@ -85,45 +106,49 @@ router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[swapId]
);
if (swaps.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Swap record not found" };
return;
[swapId],
);
if (swaps.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Swap record not found" };
return;
}
ctx.response.body = swaps[0];
} catch (error) {
console.error("Get swap error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
ctx.response.body = swaps[0];
} catch (error) {
console.error("Get swap error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Create new employee swap (SuperAdmin only)
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateSwapRequest;
const {
employeeId,
targetDepartmentId,
targetContractorId,
swapReason,
reasonDetails,
const {
employeeId,
targetDepartmentId,
targetContractorId,
swapReason,
reasonDetails,
workCompletionPercentage,
swapDate
swapDate,
} = body;
// Validate required fields
if (!employeeId || !targetDepartmentId || !swapReason || !swapDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID, target department, swap reason, and swap date are required" };
ctx.response.body = {
error:
"Employee ID, target department, swap reason, and swap date are required",
};
return;
}
// Validate swap reason
const validReasons = ["LeftWork", "Sick", "FinishedEarly", "Other"];
if (!validReasons.includes(swapReason)) {
@@ -131,87 +156,108 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
ctx.response.body = { error: "Invalid swap reason" };
return;
}
// Get employee's current department and contractor
const employees = await db.query<User[]>(
"SELECT * FROM users WHERE id = ? AND role = 'Employee'",
[employeeId]
[employeeId],
);
if (employees.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Employee not found" };
return;
}
const employee = employees[0];
if (!employee.department_id) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee has no current department" };
return;
}
// Check if there's already an active swap for this employee
const activeSwaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE employee_id = ? AND status = 'Active'",
[employeeId]
[employeeId],
);
if (activeSwaps.length > 0) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee already has an active swap. Complete or cancel it first." };
ctx.response.body = {
error:
"Employee already has an active swap. Complete or cancel it first.",
};
return;
}
// Create the swap record
const result = await db.execute(
`INSERT INTO employee_swaps
(employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id,
swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`,
[
employeeId,
employee.department_id,
targetDepartmentId,
employee.contractor_id || null,
targetContractorId || null,
swapReason,
reasonDetails || null,
workCompletionPercentage || 0,
swapDate,
currentUser.id
]
);
// Update the employee's department and contractor
await db.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[targetDepartmentId, targetContractorId || null, employeeId]
);
// Fetch the created swap
const newSwap = await db.query<EmployeeSwap[]>(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
oc.name as original_contractor_name,
tc.name as target_contractor_name,
sb.name as swapped_by_name
FROM employee_swaps es
JOIN users e ON es.employee_id = e.id
JOIN departments od ON es.original_department_id = od.id
JOIN departments td ON es.target_department_id = td.id
LEFT JOIN users oc ON es.original_contractor_id = oc.id
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[result.insertId]
);
// Use transaction to ensure both operations succeed or fail together
const newSwap = await db.transaction(async (connection) => {
// Create the swap record
const [insertResult] = await connection.execute(
`INSERT INTO employee_swaps
(employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id,
swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`,
[
employeeId,
employee.department_id,
targetDepartmentId,
employee.contractor_id || null,
targetContractorId || null,
swapReason,
reasonDetails || null,
workCompletionPercentage || 0,
swapDate,
currentUser.id,
],
);
const swapInsertId = (insertResult as { insertId: number }).insertId;
if (!swapInsertId) {
throw new Error("Failed to create swap record");
}
// Update the employee's department and contractor
const [updateResult] = await connection.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[targetDepartmentId, targetContractorId || null, employeeId],
);
const affectedRows =
(updateResult as { affectedRows: number }).affectedRows;
if (affectedRows === 0) {
throw new Error("Failed to update employee department");
}
// Fetch the created swap
const [swapRows] = await connection.query(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
oc.name as original_contractor_name,
tc.name as target_contractor_name,
sb.name as swapped_by_name
FROM employee_swaps es
JOIN users e ON es.employee_id = e.id
JOIN departments od ON es.original_department_id = od.id
JOIN departments td ON es.target_department_id = td.id
LEFT JOIN users oc ON es.original_contractor_id = oc.id
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[swapInsertId],
);
return (swapRows as EmployeeSwap[])[0];
});
ctx.response.status = 201;
ctx.response.body = newSwap[0];
ctx.response.body = newSwap;
} catch (error) {
console.error("Create swap error:", error);
ctx.response.status = 500;
@@ -220,121 +266,149 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
});
// Complete a swap (return employee to original department)
router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const swapId = ctx.params.id;
// Get the swap record
const swaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
[swapId]
);
if (swaps.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Active swap not found" };
return;
router.put(
"/:id/complete",
authenticateToken,
authorize("SuperAdmin"),
async (ctx) => {
try {
const swapId = ctx.params.id;
// Get the swap record
const swaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
[swapId],
);
if (swaps.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Active swap not found" };
return;
}
const swap = swaps[0];
// Use transaction to ensure both operations succeed or fail together
const updatedSwap = await db.transaction(async (connection) => {
// Return employee to original department and contractor
await connection.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[
swap.original_department_id,
swap.original_contractor_id,
swap.employee_id,
],
);
// Mark swap as completed
await connection.execute(
"UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?",
[swapId],
);
// Fetch updated swap
const [swapRows] = await connection.query(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
oc.name as original_contractor_name,
tc.name as target_contractor_name,
sb.name as swapped_by_name
FROM employee_swaps es
JOIN users e ON es.employee_id = e.id
JOIN departments od ON es.original_department_id = od.id
JOIN departments td ON es.target_department_id = td.id
LEFT JOIN users oc ON es.original_contractor_id = oc.id
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[swapId],
);
return (swapRows as EmployeeSwap[])[0];
});
ctx.response.body = updatedSwap;
} catch (error) {
console.error("Complete swap error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
const swap = swaps[0];
// Return employee to original department and contractor
await db.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[swap.original_department_id, swap.original_contractor_id, swap.employee_id]
);
// Mark swap as completed
await db.execute(
"UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?",
[swapId]
);
// Fetch updated swap
const updatedSwap = await db.query<EmployeeSwap[]>(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
oc.name as original_contractor_name,
tc.name as target_contractor_name,
sb.name as swapped_by_name
FROM employee_swaps es
JOIN users e ON es.employee_id = e.id
JOIN departments od ON es.original_department_id = od.id
JOIN departments td ON es.target_department_id = td.id
LEFT JOIN users oc ON es.original_contractor_id = oc.id
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[swapId]
);
ctx.response.body = updatedSwap[0];
} catch (error) {
console.error("Complete swap error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Cancel a swap (return employee to original department)
router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const swapId = ctx.params.id;
// Get the swap record
const swaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
[swapId]
);
if (swaps.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Active swap not found" };
return;
router.put(
"/:id/cancel",
authenticateToken,
authorize("SuperAdmin"),
async (ctx) => {
try {
const swapId = ctx.params.id;
// Get the swap record
const swaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
[swapId],
);
if (swaps.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Active swap not found" };
return;
}
const swap = swaps[0];
// Use transaction to ensure both operations succeed or fail together
const updatedSwap = await db.transaction(async (connection) => {
// Return employee to original department and contractor
await connection.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[
swap.original_department_id,
swap.original_contractor_id,
swap.employee_id,
],
);
// Mark swap as cancelled
await connection.execute(
"UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?",
[swapId],
);
// Fetch updated swap
const [swapRows] = await connection.query(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
oc.name as original_contractor_name,
tc.name as target_contractor_name,
sb.name as swapped_by_name
FROM employee_swaps es
JOIN users e ON es.employee_id = e.id
JOIN departments od ON es.original_department_id = od.id
JOIN departments td ON es.target_department_id = td.id
LEFT JOIN users oc ON es.original_contractor_id = oc.id
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[swapId],
);
return (swapRows as EmployeeSwap[])[0];
});
ctx.response.body = updatedSwap;
} catch (error) {
console.error("Cancel swap error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
const swap = swaps[0];
// Return employee to original department and contractor
await db.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[swap.original_department_id, swap.original_contractor_id, swap.employee_id]
);
// Mark swap as cancelled
await db.execute(
"UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?",
[swapId]
);
// Fetch updated swap
const updatedSwap = await db.query<EmployeeSwap[]>(
`SELECT es.*,
e.name as employee_name,
od.name as original_department_name,
td.name as target_department_name,
oc.name as original_contractor_name,
tc.name as target_contractor_name,
sb.name as swapped_by_name
FROM employee_swaps es
JOIN users e ON es.employee_id = e.id
JOIN departments od ON es.original_department_id = od.id
JOIN departments td ON es.target_department_id = td.id
LEFT JOIN users oc ON es.original_contractor_id = oc.id
LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`,
[swapId]
);
ctx.response.body = updatedSwap[0];
} catch (error) {
console.error("Cancel swap error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;

View File

@@ -1,22 +1,30 @@
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";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import type { JWTPayload, 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 = `
router.get(
"/completed-allocations",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser: JWTPayload = getCurrentUser(ctx);
const params: URLSearchParams = ctx.request.url.searchParams;
const startDate: string | null = params.get("startDate");
const endDate: string | null = params.get("endDate");
const departmentId: string | null = params.get("departmentId");
const contractorId: string | null = params.get("contractorId");
const employeeId: string | null = params.get("employeeId");
let query = `
SELECT wa.*,
e.name as employee_name, e.username as employee_username,
e.phone_number as employee_phone,
@@ -33,95 +41,110 @@ router.get("/completed-allocations", authenticateToken, authorize("Supervisor",
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),
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);
}
};
} catch (error) {
console.error("Get completed allocations report error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// 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[]>(`
router.get(
"/summary",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser: JWTPayload = getCurrentUser(ctx);
const params: URLSearchParams = ctx.request.url.searchParams;
const startDate: string | null = params.get("startDate");
const endDate: string | null = 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,
@@ -134,10 +157,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
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[]>(`
`,
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,
@@ -152,10 +178,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
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[]>(`
`,
queryParams,
);
// Get summary by activity type
const byActivity = await db.query<any[]>(
`
SELECT
COALESCE(wa.activity, 'Standard') as activity,
COUNT(*) as total_allocations,
@@ -166,18 +195,21 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
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" };
}
});
`,
queryParams,
);
ctx.response.body = {
byContractor,
bySubDepartment,
byActivity,
};
} catch (error) {
console.error("Get report summary error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
export default router;

View File

@@ -1,7 +1,12 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { Context } from "@oak/oak";
const router = new Router();
@@ -21,14 +26,16 @@ interface StandardRate {
}
// Get all standard rates (default rates for comparison)
router.get("/", authenticateToken, async (ctx) => {
router.get("/", authenticateToken, async (ctx: Context) => {
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");
const departmentId: string | number | null = params.get("departmentId");
const subDepartmentId: string | number | null = params.get(
"subDepartmentId",
);
const activity: string | null = params.get("activity");
let query = `
SELECT sr.*,
sd.name as sub_department_name,
@@ -44,30 +51,30 @@ router.get("/", authenticateToken, async (ctx) => {
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) {
@@ -78,15 +85,19 @@ router.get("/", authenticateToken, async (ctx) => {
});
// 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 = `
router.get(
"/all-rates",
authenticateToken,
authorize("SuperAdmin"),
async (ctx: Context) => {
try {
const params = ctx.request.url.searchParams;
const departmentId: string | number | null = params.get("departmentId");
const startDate: string | null = params.get("startDate");
const endDate: string | null = params.get("endDate");
// Get contractor rates
let contractorQuery = `
SELECT
cr.id,
'contractor' as rate_type,
@@ -108,25 +119,25 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx)
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 = `
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,
@@ -148,66 +159,77 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx)
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,
const standardParams: unknown[] = [];
if (departmentId) {
standardQuery += " AND d.id = ?";
standardParams.push(departmentId);
}
};
} catch (error) {
console.error("Get all rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
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 = `
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,
@@ -219,18 +241,21 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
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 = `
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,
@@ -244,103 +269,123 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
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 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,
);
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,
// 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,
};
});
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" };
}
});
} 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" };
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;
}
}
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.*,
// 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,
@@ -351,82 +396,96 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
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" };
}
});
[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 = `
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.*,
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,
@@ -437,53 +496,64 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
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" };
}
});
[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
router.delete(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx: Context) => {
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;
[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" };
}
// Supervisors can only delete rates in their department
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Access denied - rate not in your department" };
return;
}
await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]);
ctx.response.body = { message: "Standard rate deleted successfully" };
} catch (error) {
console.error("Delete standard rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;

View File

@@ -1,5 +1,5 @@
import { Router } from "@oak/oak";
import { hash, genSalt } from "bcrypt";
import { type Context, Router } from "@oak/oak";
import { genSalt, hash } from "bcrypt";
import { db } from "../config/database.ts";
import { config } from "../config/env.ts";
@@ -8,20 +8,28 @@ async function hashPassword(password: string): Promise<string> {
const salt = await genSalt(config.BCRYPT_ROUNDS);
return await hash(password, salt);
}
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput, isValidEmail } from "../middleware/security.ts";
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { isValidEmail, sanitizeInput } from "../middleware/security.ts";
import type {
CreateUserRequest,
UpdateUserRequest,
User,
} from "../types/index.ts";
const router = new Router();
// Get all users (with filters)
router.get("/", authenticateToken, async (ctx) => {
router.get("/", authenticateToken, async (ctx: Context) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const role = params.get("role");
const departmentId = params.get("departmentId");
let query = `
SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
@@ -36,25 +44,25 @@ router.get("/", authenticateToken, async (ctx) => {
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Supervisors can only see users in their department
if (currentUser.role === "Supervisor") {
query += " AND u.department_id = ?";
queryParams.push(currentUser.departmentId);
}
if (role) {
query += " AND u.role = ?";
queryParams.push(role);
}
if (departmentId) {
query += " AND u.department_id = ?";
queryParams.push(departmentId);
}
query += " ORDER BY u.created_at DESC";
const users = await db.query<User[]>(query, queryParams);
ctx.response.body = users;
} catch (error) {
@@ -65,11 +73,11 @@ router.get("/", authenticateToken, async (ctx) => {
});
// Get user by ID
router.get("/:id", authenticateToken, async (ctx) => {
router.get("/:id", authenticateToken, async (ctx: Context) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const users = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
@@ -82,22 +90,25 @@ router.get("/:id", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[userId]
[userId],
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only view users in their department
if (currentUser.role === "Supervisor" && users[0].department_id !== currentUser.departmentId) {
if (
currentUser.role === "Supervisor" &&
users[0].department_id !== currentUser.departmentId
) {
ctx.response.status = 403;
ctx.response.body = { error: "Access denied" };
return;
}
ctx.response.body = users[0];
} catch (error) {
console.error("Get user error:", error);
@@ -107,215 +118,98 @@ router.get("/:id", authenticateToken, async (ctx) => {
});
// Create user
router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateUserRequest;
const {
username, name, email, password, role, departmentId, contractorId,
phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc,
contractorAgreementNumber, pfNumber, esicNumber
} = body;
// Input validation
if (!username || !name || !email || !password || !role) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields" };
return;
}
// Sanitize inputs
const sanitizedUsername = sanitizeInput(username);
const sanitizedName = sanitizeInput(name);
const sanitizedEmail = sanitizeInput(email);
// Validate email
if (!isValidEmail(sanitizedEmail)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid email format" };
return;
}
// Supervisors can only create users in their department
if (currentUser.role === "Supervisor") {
if (departmentId !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Can only create users in your department" };
return;
}
if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Cannot create admin or supervisor users" };
return;
}
}
// Hash password
const hashedPassword = await hashPassword(password);
const result = await db.execute(
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id,
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc,
contractor_agreement_number, pf_number, esic_number)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role,
departmentId || null, contractorId || null,
phoneNumber || null, aadharNumber || null, bankAccountNumber || null,
bankName || null, bankIfsc || null,
contractorAgreementNumber || null, pfNumber || null, esicNumber || null
]
);
const newUser = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
u.phone_number, u.aadhar_number, u.bank_account_number,
u.bank_name, u.bank_ifsc,
u.contractor_agreement_number, u.pf_number, u.esic_number,
d.name as department_name,
c.name as contractor_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newUser[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Username or email already exists" };
return;
}
console.error("Create user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
router.post(
"/",
authenticateToken,
authorize("SuperAdmin", "Supervisor"),
async (ctx: Context) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateUserRequest;
const {
username,
name,
email,
password,
role,
departmentId,
contractorId,
phoneNumber,
aadharNumber,
bankAccountNumber,
bankName,
bankIfsc,
contractorAgreementNumber,
pfNumber,
esicNumber,
} = body;
// Update user
router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const body = await ctx.request.body.json() as UpdateUserRequest;
const {
name, email, role, departmentId, contractorId, isActive,
phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc,
contractorAgreementNumber, pfNumber, esicNumber
} = body;
// Check if user exists
const existingUsers = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?",
[userId]
);
if (existingUsers.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only update users in their department
if (currentUser.role === "Supervisor") {
if (existingUsers[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Can only update users in your department" };
// Input validation
if (!username || !name || !email || !password || !role) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields" };
return;
}
if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Cannot modify admin or supervisor roles" };
return;
}
}
const updates: string[] = [];
const params: unknown[] = [];
if (name !== undefined) {
updates.push("name = ?");
params.push(sanitizeInput(name));
}
if (email !== undefined) {
if (!isValidEmail(email)) {
// Sanitize inputs
const sanitizedUsername = sanitizeInput(username);
const sanitizedName = sanitizeInput(name);
const sanitizedEmail = sanitizeInput(email);
// Validate email
if (!isValidEmail(sanitizedEmail)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid email format" };
return;
}
updates.push("email = ?");
params.push(sanitizeInput(email));
}
if (role !== undefined) {
updates.push("role = ?");
params.push(role);
}
if (departmentId !== undefined) {
updates.push("department_id = ?");
params.push(departmentId);
}
if (contractorId !== undefined) {
updates.push("contractor_id = ?");
params.push(contractorId);
}
if (isActive !== undefined) {
updates.push("is_active = ?");
params.push(isActive);
}
// New fields
if (phoneNumber !== undefined) {
updates.push("phone_number = ?");
params.push(phoneNumber);
}
if (aadharNumber !== undefined) {
updates.push("aadhar_number = ?");
params.push(aadharNumber);
}
if (bankAccountNumber !== undefined) {
updates.push("bank_account_number = ?");
params.push(bankAccountNumber);
}
if (bankName !== undefined) {
updates.push("bank_name = ?");
params.push(bankName);
}
if (bankIfsc !== undefined) {
updates.push("bank_ifsc = ?");
params.push(bankIfsc);
}
if (contractorAgreementNumber !== undefined) {
updates.push("contractor_agreement_number = ?");
params.push(contractorAgreementNumber);
}
if (pfNumber !== undefined) {
updates.push("pf_number = ?");
params.push(pfNumber);
}
if (esicNumber !== undefined) {
updates.push("esic_number = ?");
params.push(esicNumber);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
params.push(userId);
await db.execute(
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
params
);
const updatedUser = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
// Supervisors can only create users in their department
if (currentUser.role === "Supervisor") {
if (departmentId !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = {
error: "Can only create users in your department",
};
return;
}
if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = {
error: "Cannot create admin or supervisor users",
};
return;
}
}
// Hash password
const hashedPassword = await hashPassword(password);
const result = await db.execute(
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id,
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc,
contractor_agreement_number, pf_number, esic_number)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
sanitizedUsername,
sanitizedName,
sanitizedEmail,
hashedPassword,
role,
departmentId || null,
contractorId || null,
phoneNumber || null,
aadharNumber || null,
bankAccountNumber || null,
bankName || null,
bankIfsc || null,
contractorAgreementNumber || null,
pfNumber || null,
esicNumber || null,
],
);
const newUser = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
u.phone_number, u.aadhar_number, u.bank_account_number,
u.bank_name, u.bank_ifsc,
@@ -326,55 +220,232 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[userId]
);
ctx.response.body = updatedUser[0];
} catch (error) {
console.error("Update user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newUser[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Username or email already exists" };
return;
}
console.error("Create user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Update user
router.put(
"/:id",
authenticateToken,
authorize("SuperAdmin", "Supervisor"),
async (ctx: Context) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const body = await ctx.request.body.json() as UpdateUserRequest;
const {
name,
email,
role,
departmentId,
contractorId,
isActive,
phoneNumber,
aadharNumber,
bankAccountNumber,
bankName,
bankIfsc,
contractorAgreementNumber,
pfNumber,
esicNumber,
} = body;
// Check if user exists
const existingUsers = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?",
[userId],
);
if (existingUsers.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only update users in their department
if (currentUser.role === "Supervisor") {
if (existingUsers[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = {
error: "Can only update users in your department",
};
return;
}
if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = {
error: "Cannot modify admin or supervisor roles",
};
return;
}
}
const updates: string[] = [];
const params: unknown[] = [];
if (name !== undefined) {
updates.push("name = ?");
params.push(sanitizeInput(name));
}
if (email !== undefined) {
if (!isValidEmail(email)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid email format" };
return;
}
updates.push("email = ?");
params.push(sanitizeInput(email));
}
if (role !== undefined) {
updates.push("role = ?");
params.push(role);
}
if (departmentId !== undefined) {
updates.push("department_id = ?");
params.push(departmentId);
}
if (contractorId !== undefined) {
updates.push("contractor_id = ?");
params.push(contractorId);
}
if (isActive !== undefined) {
updates.push("is_active = ?");
params.push(isActive);
}
// New fields
if (phoneNumber !== undefined) {
updates.push("phone_number = ?");
params.push(phoneNumber);
}
if (aadharNumber !== undefined) {
updates.push("aadhar_number = ?");
params.push(aadharNumber);
}
if (bankAccountNumber !== undefined) {
updates.push("bank_account_number = ?");
params.push(bankAccountNumber);
}
if (bankName !== undefined) {
updates.push("bank_name = ?");
params.push(bankName);
}
if (bankIfsc !== undefined) {
updates.push("bank_ifsc = ?");
params.push(bankIfsc);
}
if (contractorAgreementNumber !== undefined) {
updates.push("contractor_agreement_number = ?");
params.push(contractorAgreementNumber);
}
if (pfNumber !== undefined) {
updates.push("pf_number = ?");
params.push(pfNumber);
}
if (esicNumber !== undefined) {
updates.push("esic_number = ?");
params.push(esicNumber);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
params.push(userId);
await db.execute(
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
params,
);
const updatedUser = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
u.phone_number, u.aadhar_number, u.bank_account_number,
u.bank_name, u.bank_ifsc,
u.contractor_agreement_number, u.pf_number, u.esic_number,
d.name as department_name,
c.name as contractor_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[userId],
);
ctx.response.body = updatedUser[0];
} catch (error) {
console.error("Update user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Delete user
router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const users = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?",
[userId]
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only delete users in their department
if (currentUser.role === "Supervisor") {
if (users[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Can only delete users in your department" };
router.delete(
"/:id",
authenticateToken,
authorize("SuperAdmin", "Supervisor"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const users = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?",
[userId],
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Cannot delete admin or supervisor users" };
return;
// Supervisors can only delete users in their department
if (currentUser.role === "Supervisor") {
if (users[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = {
error: "Can only delete users in your department",
};
return;
}
if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = {
error: "Cannot delete admin or supervisor users",
};
return;
}
}
await db.execute("DELETE FROM users WHERE id = ?", [userId]);
ctx.response.body = { message: "User deleted successfully" };
} catch (error) {
console.error("Delete user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
await db.execute("DELETE FROM users WHERE id = ?", [userId]);
ctx.response.body = { message: "User deleted successfully" };
} catch (error) {
console.error("Delete user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;

View File

@@ -1,21 +1,35 @@
import { Router } from "@oak/oak";
import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { WorkAllocation, CreateWorkAllocationRequest, ContractorRate } from "../types/index.ts";
import type {
ContractorRate,
CreateWorkAllocationRequest,
JWTPayload,
WorkAllocation,
} from "../types/index.ts";
const router = new Router();
// Get all work allocations
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const employeeId = params.get("employeeId");
const status = params.get("status");
const departmentId = params.get("departmentId");
let query = `
router.get(
"/",
authenticateToken,
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try {
const currentUser: JWTPayload = getCurrentUser(ctx);
const params: URLSearchParams = ctx.request.url.searchParams;
const employeeId: string | null = params.get("employeeId");
const status: string | null = params.get("status");
const departmentId: string | null = params.get("departmentId");
let query: string = `
SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
@@ -30,52 +44,53 @@ router.get("/", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON e.department_id = d.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Role-based filtering
if (currentUser.role === "Supervisor") {
query += " AND wa.supervisor_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Employee") {
query += " AND wa.employee_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Contractor") {
query += " AND wa.contractor_id = ?";
queryParams.push(currentUser.id);
const queryParams: unknown[] = [];
// Role-based filtering
if (currentUser.role === "Supervisor") {
query += " AND wa.supervisor_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Employee") {
query += " AND wa.employee_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Contractor") {
query += " AND wa.contractor_id = ?";
queryParams.push(currentUser.id);
}
if (employeeId) {
query += " AND wa.employee_id = ?";
queryParams.push(employeeId);
}
if (status) {
query += " AND wa.status = ?";
queryParams.push(status);
}
if (departmentId) {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC";
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
ctx.response.body = allocations;
} catch (error) {
console.error("Get work allocations error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
if (employeeId) {
query += " AND wa.employee_id = ?";
queryParams.push(employeeId);
}
if (status) {
query += " AND wa.status = ?";
queryParams.push(status);
}
if (departmentId) {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC";
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
ctx.response.body = allocations;
} catch (error) {
console.error("Get work allocations error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
// Get work allocation by ID
router.get("/:id", authenticateToken, async (ctx) => {
router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => {
try {
const allocationId = ctx.params.id;
const allocations = await db.query<WorkAllocation[]>(
const allocationId: string | undefined = ctx.params.id;
const allocations: WorkAllocation[] = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
@@ -89,15 +104,15 @@ router.get("/:id", authenticateToken, async (ctx) => {
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[allocationId]
[allocationId],
);
if (allocations.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Work allocation not found" };
return;
}
ctx.response.body = allocations[0];
} catch (error) {
console.error("Get work allocation error:", error);
@@ -107,57 +122,92 @@ router.get("/:id", authenticateToken, async (ctx) => {
});
// Create work allocation (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateWorkAllocationRequest;
const { employeeId, contractorId, subDepartmentId, activity, description, assignedDate, rate, units, totalAmount, departmentId } = body;
if (!employeeId || !contractorId || !assignedDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields" };
return;
}
// Verify employee exists
let employeeQuery = "SELECT * FROM users WHERE id = ?";
const employeeParams: unknown[] = [employeeId];
if (currentUser.role === "Supervisor") {
employeeQuery += " AND department_id = ?";
employeeParams.push(currentUser.departmentId);
}
const employees = await db.query<{ id: number }[]>(employeeQuery, employeeParams);
if (employees.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Employee not found or not in your department" };
return;
}
// Use provided rate or get contractor's current rate
let finalRate = rate;
if (!finalRate) {
const rates = await db.query<ContractorRate[]>(
"SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1",
[contractorId]
router.post(
"/",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateWorkAllocationRequest;
const {
employeeId,
contractorId,
subDepartmentId,
activity,
description,
assignedDate,
rate,
units,
totalAmount,
departmentId,
} = body;
if (!employeeId || !contractorId || !assignedDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields" };
return;
}
// Verify employee exists
let employeeQuery = "SELECT * FROM users WHERE id = ?";
const employeeParams: unknown[] = [employeeId];
if (currentUser.role === "Supervisor") {
employeeQuery += " AND department_id = ?";
employeeParams.push(currentUser.departmentId);
}
const employees = await db.query<{ id: number }[]>(
employeeQuery,
employeeParams,
);
finalRate = rates.length > 0 ? rates[0].rate : null;
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const sanitizedDescription = description ? sanitizeInput(description) : null;
const result = await db.execute(
`INSERT INTO work_allocations
if (employees.length === 0) {
ctx.response.status = 403;
ctx.response.body = {
error: "Employee not found or not in your department",
};
return;
}
// Use provided rate or get contractor's current rate
let finalRate = rate;
if (!finalRate) {
const rates = await db.query<ContractorRate[]>(
"SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1",
[contractorId],
);
finalRate = rates.length > 0 ? rates[0].rate : null;
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const sanitizedDescription = description
? sanitizeInput(description)
: null;
const result = await db.execute(
`INSERT INTO work_allocations
(employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[employeeId, currentUser.id, contractorId, subDepartmentId || null, sanitizedActivity, sanitizedDescription, assignedDate, finalRate, units || null, totalAmount || null]
);
const newAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
[
employeeId,
currentUser.id,
contractorId,
subDepartmentId || null,
sanitizedActivity,
sanitizedDescription,
assignedDate,
finalRate,
units || null,
totalAmount || null,
],
);
const newAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
@@ -170,56 +220,66 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newAllocation[0];
} catch (error) {
console.error("Create work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
[result.insertId],
);
ctx.response.status = 201;
ctx.response.body = newAllocation[0];
} catch (error) {
console.error("Create work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Update work allocation status (Supervisor or SuperAdmin)
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
const body = await ctx.request.body.json() as { status: string; completionDate?: string };
const { status, completionDate } = body;
if (!status) {
ctx.response.status = 400;
ctx.response.body = { error: "Status required" };
return;
}
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Work allocation not found or access denied" };
return;
}
await db.execute(
"UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?",
[status, completionDate || null, allocationId]
);
const updatedAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
router.put(
"/:id/status",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
const body = await ctx.request.body.json() as {
status: string;
completionDate?: string;
};
const { status, completionDate } = body;
if (!status) {
ctx.response.status = 400;
ctx.response.body = { error: "Status required" };
return;
}
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = {
error: "Work allocation not found or access denied",
};
return;
}
await db.execute(
"UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?",
[status, completionDate || null, allocationId],
);
const updatedAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
@@ -232,47 +292,57 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[allocationId]
);
ctx.response.body = updatedAllocation[0];
} catch (error) {
console.error("Update work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
[allocationId],
);
ctx.response.body = updatedAllocation[0];
} catch (error) {
console.error("Update work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Delete work allocation (Supervisor or SuperAdmin)
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
router.delete(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = {
error: "Work allocation not found or access denied",
};
return;
}
await db.execute("DELETE FROM work_allocations WHERE id = ?", [
allocationId,
]);
ctx.response.body = { message: "Work allocation deleted successfully" };
} catch (error) {
console.error("Delete work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Work allocation not found or access denied" };
return;
}
await db.execute("DELETE FROM work_allocations WHERE id = ?", [allocationId]);
ctx.response.body = { message: "Work allocation deleted successfully" };
} catch (error) {
console.error("Delete work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
},
);
export default router;