415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
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 { CreateSwapRequest, EmployeeSwap, User } from "../types/index.ts";
|
|
|
|
const router = new Router();
|
|
|
|
// Get all employee swaps (SuperAdmin only)
|
|
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,
|
|
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 1=1
|
|
`;
|
|
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" };
|
|
}
|
|
},
|
|
);
|
|
|
|
// Get swap by ID
|
|
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,
|
|
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],
|
|
);
|
|
|
|
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" };
|
|
}
|
|
},
|
|
);
|
|
|
|
// 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,
|
|
workCompletionPercentage,
|
|
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",
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Validate swap reason
|
|
const validReasons = ["LeftWork", "Sick", "FinishedEarly", "Other"];
|
|
if (!validReasons.includes(swapReason)) {
|
|
ctx.response.status = 400;
|
|
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],
|
|
);
|
|
|
|
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],
|
|
);
|
|
|
|
if (activeSwaps.length > 0) {
|
|
ctx.response.status = 400;
|
|
ctx.response.body = {
|
|
error:
|
|
"Employee already has an active swap. Complete or cancel it first.",
|
|
};
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
} catch (error) {
|
|
console.error("Create swap error:", error);
|
|
ctx.response.status = 500;
|
|
ctx.response.body = { error: "Internal server error" };
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
|
|
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" };
|
|
}
|
|
},
|
|
);
|
|
|
|
// 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;
|
|
}
|
|
|
|
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" };
|
|
}
|
|
},
|
|
);
|
|
|
|
export default router;
|