(Feat): Initial Commit
This commit is contained in:
293
backend-deno/routes/attendance.ts
Normal file
293
backend-deno/routes/attendance.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import type { Attendance, CheckInOutRequest, 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 = `
|
||||
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 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);
|
||||
}
|
||||
|
||||
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,
|
||||
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]
|
||||
);
|
||||
|
||||
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);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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 = `
|
||||
SELECT
|
||||
COUNT(DISTINCT a.employee_id) as total_employees,
|
||||
COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in,
|
||||
COUNT(DISTINCT CASE WHEN a.status = 'CheckedOut' THEN a.employee_id END) as checked_out,
|
||||
d.name as department_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
163
backend-deno/routes/auth.ts
Normal file
163
backend-deno/routes/auth.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { hash, compare } from "bcrypt";
|
||||
import { db } from "../config/database.ts";
|
||||
import { config } from "../config/env.ts";
|
||||
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";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Login
|
||||
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]
|
||||
);
|
||||
|
||||
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,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
departmentId: user.department_id,
|
||||
});
|
||||
|
||||
// Return user data without password
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
|
||||
ctx.response.body = {
|
||||
token,
|
||||
user: userWithoutPassword,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user
|
||||
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]
|
||||
);
|
||||
|
||||
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);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Change password
|
||||
router.post("/change-password", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
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);
|
||||
if (!passwordCheck.valid) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: passwordCheck.message };
|
||||
return;
|
||||
}
|
||||
} else if (newPassword.length < 6) {
|
||||
ctx.response.status = 400;
|
||||
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]
|
||||
);
|
||||
|
||||
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 hash(newPassword, config.BCRYPT_ROUNDS);
|
||||
|
||||
// Update password
|
||||
await db.execute(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hashedPassword, currentUser.id]
|
||||
);
|
||||
|
||||
ctx.response.body = { message: "Password changed successfully" };
|
||||
} catch (error) {
|
||||
console.error("Change password error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
241
backend-deno/routes/contractor-rates.ts
Normal file
241
backend-deno/routes/contractor-rates.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput } from "../middleware/security.ts";
|
||||
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 = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const 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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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 = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
WHERE cr.contractor_id = ?
|
||||
`;
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
139
backend-deno/routes/departments.ts
Normal file
139
backend-deno/routes/departments.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput } from "../middleware/security.ts";
|
||||
import type { Department, SubDepartment } from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get all departments
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const departments = await db.query<Department[]>(
|
||||
"SELECT * FROM departments ORDER BY name"
|
||||
);
|
||||
ctx.response.body = departments;
|
||||
} catch (error) {
|
||||
console.error("Get departments error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get department by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const deptId = ctx.params.id;
|
||||
|
||||
const departments = await db.query<Department[]>(
|
||||
"SELECT * FROM departments WHERE id = ?",
|
||||
[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);
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 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; primaryActivity: string };
|
||||
const { name, primaryActivity } = body;
|
||||
|
||||
if (!name || !primaryActivity) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Name and primary activity required" };
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
const sanitizedActivity = sanitizeInput(primaryActivity);
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)",
|
||||
[deptId, sanitizedName, sanitizedActivity]
|
||||
);
|
||||
|
||||
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) {
|
||||
console.error("Create sub-department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
312
backend-deno/routes/users.ts
Normal file
312
backend-deno/routes/users.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { hash } from "bcrypt";
|
||||
import { db } from "../config/database.ts";
|
||||
import { config } from "../config/env.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput, isValidEmail } from "../middleware/security.ts";
|
||||
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get all users (with filters)
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
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,
|
||||
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 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) {
|
||||
console.error("Get users error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get user by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
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,
|
||||
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]
|
||||
);
|
||||
|
||||
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) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Access denied" };
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = users[0];
|
||||
} catch (error) {
|
||||
console.error("Get user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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 } = 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 hash(password, config.BCRYPT_ROUNDS);
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO users (username, name, email, password, role, department_id, contractor_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
[sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, departmentId || null, contractorId || 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,
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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 } = 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);
|
||||
}
|
||||
|
||||
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,
|
||||
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" };
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
278
backend-deno/routes/work-allocations.ts
Normal file
278
backend-deno/routes/work-allocations.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput } from "../middleware/security.ts";
|
||||
import type { WorkAllocation, CreateWorkAllocationRequest, ContractorRate } 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 = `
|
||||
SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
c.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
JOIN users s ON wa.supervisor_id = s.id
|
||||
JOIN users c ON wa.contractor_id = c.id
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE 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);
|
||||
}
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const allocationId = ctx.params.id;
|
||||
|
||||
const allocations = 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,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
JOIN users s ON wa.supervisor_id = s.id
|
||||
JOIN users c ON wa.contractor_id = c.id
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.id = ?`,
|
||||
[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);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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]
|
||||
);
|
||||
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.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
c.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
JOIN users s ON wa.supervisor_id = s.id
|
||||
JOIN users c ON wa.contractor_id = c.id
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
c.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name
|
||||
FROM work_allocations wa
|
||||
JOIN users e ON wa.employee_id = e.id
|
||||
JOIN users s ON wa.supervisor_id = s.id
|
||||
JOIN users c ON wa.contractor_id = c.id
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.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" };
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user