Compare commits

..

No commits in common. "ec407364d9c424c8af01d6d6cdf11b2d78ebe9ef" and "513dc81931b7141f5e7b7b3d3cb9179a36154281" have entirely different histories.

3 changed files with 76 additions and 252 deletions

View File

@ -1,13 +1,8 @@
import { Context } from "@oak/oak"; import { Context } from "@oak/oak";
import { getDbPool } from "../../shared/db.ts"; import { getDbPool } from "../../shared/db.ts";
import { AppRole } from "../../shared/auth.ts"; // Importing roles for security checks
const amsDb = getDbPool("hrms_ams"); const amsDb = getDbPool("hrms_ams");
const emsDb = getDbPool("hrms_ems"); // Used for cross-database joins
/**
* EMPLOYEE: Create a new regularization request
*/
export const createRegularizationRequest = async (ctx: Context) => { export const createRegularizationRequest = async (ctx: Context) => {
if (!ctx.request.hasBody) { if (!ctx.request.hasBody) {
ctx.response.status = 400; ctx.response.status = 400;
@ -16,143 +11,40 @@ export const createRegularizationRequest = async (ctx: Context) => {
} }
const body = await ctx.request.body.json(); const body = await ctx.request.body.json();
const { const { attendance_id, employee_id, requested_check_in, requested_check_out, reason } = body;
attendance_id,
employee_id,
regularization_type,
target_date,
requested_check_in,
requested_check_out,
reason,
} = body;
if (!employee_id || !reason || !regularization_type || !target_date) { // Basic validation
if (!attendance_id || !employee_id || !reason || !requested_check_in) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { ctx.response.body = { success: false, message: "Missing required fields: attendance_id, employee_id, requested_check_in, and reason." };
success: false,
message:
"Missing required fields: employee_id, target_date, regularization_type, and reason.",
};
return; return;
} }
try { try {
// Fetch employee's branch_id from EMS so AMS knows which branch this request belongs to // Extract 'YYYY-MM-DD' safely from the requested_check_in string
const [empRows]: any = await emsDb.execute( const target_date = requested_check_in.split(' ')[0];
`SELECT branch_id FROM employees WHERE employee_id = ?`,
[employee_id],
);
if (empRows.length === 0) { // Insert into the attendance_regularizations table including target_date
ctx.response.status = 404;
ctx.response.body = {
success: false,
message: "Employee not found in EMS.",
};
return;
}
const branch_id = empRows[0].branch_id;
// Insert into AMS with the cached branch_id
await amsDb.execute( await amsDb.execute(
`INSERT INTO attendance_regularizations `INSERT INTO attendance_regularizations
(attendance_id, employee_id, branch_id, regularization_type, target_date, requested_check_in, requested_check_out, reason, status) (attendance_id, employee_id, target_date, requested_check_in, requested_check_out, reason, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'PENDING')`, VALUES (?, ?, ?, ?, ?, ?, 'PENDING')`,
[ [attendance_id, employee_id, target_date, requested_check_in, requested_check_out || null, reason]
attendance_id || null,
employee_id,
branch_id,
regularization_type,
target_date,
requested_check_in || null,
requested_check_out || null,
reason,
],
); );
ctx.response.status = 201; ctx.response.status = 201;
ctx.response.body = { ctx.response.body = {
success: true, success: true,
message: message: "Regularization request submitted successfully and is pending approval.",
"Regularization request submitted successfully and is pending approval.",
}; };
} catch (error) { } catch (error) {
console.error("Failed to submit regularization:", error); console.error("Failed to submit regularization:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
} }
}; };
/**
* ADMIN: Fetch pending regularizations based on their assigned branches
*/
export const getPendingRegularizations = async (ctx: Context) => {
const user = ctx.state.user;
try {
let query = `
SELECT
r.regularization_id,
r.employee_id,
r.target_date,
r.regularization_type,
r.requested_check_in,
r.requested_check_out,
r.reason,
e.employee_code,
p.first_name,
p.last_name,
b.branch_name
FROM attendance_regularizations r
INNER JOIN hrms_ems.employees e ON r.employee_id = e.employee_id
INNER JOIN hrms_ems.partners p ON e.partner_id = p.partner_id
INNER JOIN hrms_ems.branches b ON r.branch_id = b.branch_id
WHERE r.status = 'PENDING'
`;
const params: any[] = [];
// If the user is an ADMIN (not SUPER_ADMIN), restrict to their managed branches
if (user.role !== AppRole.SUPER_ADMIN) {
const branches = user.managed_branches;
if (!branches || branches.length === 0) {
// If an admin has no branches assigned, return empty list
ctx.response.status = 200;
ctx.response.body = { success: true, data: [] };
return;
}
const placeholders = branches.map(() => "?").join(",");
query += ` AND r.branch_id IN (${placeholders})`;
params.push(...branches);
}
query += ` ORDER BY r.target_date DESC`;
const [rows]: any = await amsDb.execute(query, params);
ctx.response.status = 200;
ctx.response.body = {
success: true,
count: rows.length,
data: rows,
};
} catch (error) {
console.error("Failed to fetch pending regularizations:", error);
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
};
/**
* ADMIN: Review (Approve/Reject) a regularization request
*/
export const reviewRegularizationRequest = async (ctx: Context) => { export const reviewRegularizationRequest = async (ctx: Context) => {
if (!ctx.request.hasBody) { if (!ctx.request.hasBody) {
ctx.response.status = 400; ctx.response.status = 400;
@ -161,15 +53,11 @@ export const reviewRegularizationRequest = async (ctx: Context) => {
} }
const body = await ctx.request.body.json(); const body = await ctx.request.body.json();
const user = ctx.state.user; // The admin reviewing it const { regularization_id, action, reviewed_by_id } = body; // action can be 'APPROVED' or 'REJECTED'
const { regularization_id, action } = body; // action can be 'APPROVED' or 'REJECTED'
if (!regularization_id || !["APPROVED", "REJECTED"].includes(action)) { if (!regularization_id || !reviewed_by_id || !["APPROVED", "REJECTED"].includes(action)) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { ctx.response.body = { success: false, message: "Invalid payload. Required: regularization_id, reviewed_by_id, action." };
success: false,
message: "Invalid payload. Required: regularization_id, action.",
};
return; return;
} }
@ -180,119 +68,81 @@ export const reviewRegularizationRequest = async (ctx: Context) => {
// 1. Fetch the regularization request details // 1. Fetch the regularization request details
const [requestRows]: any = await amsConnection.execute( const [requestRows]: any = await amsConnection.execute(
`SELECT regularization_id, attendance_id, branch_id, requested_check_in, requested_check_out, status `SELECT
attendance_id,
DATE_FORMAT(requested_check_in, '%Y-%m-%d %H:%i:%s') as requested_check_in,
DATE_FORMAT(requested_check_out, '%Y-%m-%d %H:%i:%s') as requested_check_out,
status
FROM attendance_regularizations WHERE regularization_id = ?`, FROM attendance_regularizations WHERE regularization_id = ?`,
[regularization_id], [regularization_id]
); );
if (requestRows.length === 0) { if (requestRows.length === 0) {
ctx.response.status = 404; ctx.response.status = 404;
ctx.response.body = { ctx.response.body = { success: false, message: "Regularization request not found." };
success: false,
message: "Regularization request not found.",
};
await amsConnection.rollback(); await amsConnection.rollback();
return; return;
} }
const request = requestRows[0]; const request = requestRows[0];
// SECURITY CHECK: Ensure the admin actually manages this branch
if (
user.role !== AppRole.SUPER_ADMIN &&
!user.managed_branches.includes(request.branch_id)
) {
ctx.response.status = 403;
ctx.response.body = {
success: false,
message: "Access Denied: You do not manage this branch.",
};
await amsConnection.rollback();
return;
}
if (request.status !== "PENDING") { if (request.status !== "PENDING") {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { ctx.response.body = { success: false, message: "This request has already been processed." };
success: false,
message: "This request has already been processed.",
};
await amsConnection.rollback(); await amsConnection.rollback();
return; return;
} }
// 2. Update the regularization request record status and reviewer ID // 2. Update the regularization request record status and reviewer ID (Removed 'remarks')
await amsConnection.execute( await amsConnection.execute(
`UPDATE attendance_regularizations `UPDATE attendance_regularizations
SET status = ?, reviewed_by_id = ? SET status = ?, reviewed_by_id = ?
WHERE regularization_id = ?`, WHERE regularization_id = ?`,
[action, user.employee_id, regularization_id], [action, reviewed_by_id, regularization_id]
); );
// 3. If APPROVED, dynamically recalculate and overwrite the target day's processed ledger row // 3. If APPROVED, dynamically overwrite the target day's calculated ledger row
if (action === "APPROVED") { if (action === "APPROVED") {
let worked_hours = 0.0; let worked_hours = 0.0;
let final_status = "FULL_DAY"; let final_status = "FULL_DAY";
let check_in_status = "ON_TIME"; let check_in_status = "ON_TIME";
// Fetch shift details (Fallback pattern) // Fetch shift details to accurately re-evaluate punctuality boundaries (Defaulting to shift_id 1)
const [empRows]: any = await amsConnection.execute(
`SELECT company_id, branch_id FROM hrms_ems.employees WHERE employee_id = ?`,
[requestRows[0].employee_id],
);
const emp = empRows[0];
const [shiftRows]: any = await amsConnection.execute( const [shiftRows]: any = await amsConnection.execute(
`SELECT start_time, grace_period_minutes FROM shifts `SELECT start_time, grace_period_minutes FROM shifts WHERE shift_id = 1`
WHERE (branch_id = ? OR branch_id IS NULL) AND company_id = ?
ORDER BY branch_id IS NULL ASC LIMIT 1`,
[emp.branch_id, emp.company_id],
); );
const shift = shiftRows[0] || const shift = shiftRows[0] || { start_time: "10:00:00", grace_period_minutes: 10 };
{ start_time: "10:00:00", grace_period_minutes: 10 };
if (request.requested_check_in && request.requested_check_out) { if (request.requested_check_in && request.requested_check_out) {
const checkInMs = new Date(request.requested_check_in.replace(" ", "T")) // --- A. Recalculate Working Duration Metrics ---
.getTime(); const checkInMs = new Date(request.requested_check_in.replace(' ', 'T')).getTime();
const checkOutMs = new Date( const checkOutMs = new Date(request.requested_check_out.replace(' ', 'T')).getTime();
request.requested_check_out.replace(" ", "T"), worked_hours = Math.round(((checkOutMs - checkInMs) / (1000 * 60 * 60)) * 100) / 100;
).getTime();
// Handle potential night shift edge case
const effectiveCheckOutMs = checkOutMs < checkInMs
? checkOutMs + (24 * 60 * 60 * 1000)
: checkOutMs;
worked_hours = Math.round(
((effectiveCheckOutMs - checkInMs) / (1000 * 60 * 60)) * 100,
) / 100;
if (worked_hours >= 7.0) final_status = "FULL_DAY"; if (worked_hours >= 7.0) final_status = "FULL_DAY";
else if (worked_hours >= 4.0) final_status = "HALF_DAY"; else if (worked_hours >= 4.0) final_status = "HALF_DAY";
else final_status = "ABSENT"; else final_status = "ABSENT";
const rawCheckInTimeStr = request.requested_check_in.split(" ")[1]; // --- B. Dynamically Re-evaluate Punctuality Status (Preserves LATE flags) ---
const [punchH, punchM, punchS] = rawCheckInTimeStr.split(":").map( const rawCheckInTimeStr = request.requested_check_in.split(' ')[1];
Number, const [punchH, punchM, punchS] = rawCheckInTimeStr.split(':').map(Number);
); const [shiftH, shiftM, shiftS] = shift.start_time.split(':').map(Number);
const [shiftH, shiftM, shiftS] = shift.start_time.split(":").map(
Number,
);
const punchTotalSeconds = punchH * 3600 + punchM * 60 + punchS; const punchTotalSeconds = punchH * 3600 + punchM * 60 + punchS;
const shiftCutoffSeconds = shiftH * 3600 + shiftM * 60 + const shiftCutoffSeconds = shiftH * 3600 + shiftM * 60 + (shift.grace_period_minutes * 60);
(shift.grace_period_minutes * 60);
if (punchTotalSeconds > shiftCutoffSeconds) { if (punchTotalSeconds > shiftCutoffSeconds) {
check_in_status = "LATE"; check_in_status = "LATE";
} }
} }
// Only update the ledger if an actual attendance record exists
if (request.attendance_id) {
await amsConnection.execute( await amsConnection.execute(
`UPDATE processed_daily_attendance `UPDATE processed_daily_attendance
SET check_in = ?, check_out = ?, worked_hours = ?, final_status = ?, check_in_status = ? SET check_in = ?,
check_out = ?,
worked_hours = ?,
final_status = ?,
check_in_status = ?
WHERE attendance_id = ?`, WHERE attendance_id = ?`,
[ [
request.requested_check_in, request.requested_check_in,
@ -300,26 +150,23 @@ export const reviewRegularizationRequest = async (ctx: Context) => {
worked_hours, worked_hours,
final_status, final_status,
check_in_status, check_in_status,
request.attendance_id, request.attendance_id
], ]
); );
} }
}
await amsConnection.commit(); await amsConnection.commit();
ctx.response.status = 200; ctx.response.status = 200;
ctx.response.body = { ctx.response.body = {
success: true, success: true,
message: `Request has been successfully ${action.toLowerCase()}.`, message: `Request has been successfully ${action.toLowerCase()} by admin ID ${reviewed_by_id}.`,
}; };
} catch (error) { } catch (error) {
await amsConnection.rollback(); await amsConnection.rollback();
console.error("Admin review action failed:", error); console.error("Admin review action failed:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
} finally { } finally {
amsConnection.release(); amsConnection.release();
} }

View File

@ -6,10 +6,7 @@ import {
getDailyReport, getDailyReport,
getAdminRangeReport, getAdminRangeReport,
getSingleEmployeeRangeReport} from "./controllers/attendance.controller.ts"; getSingleEmployeeRangeReport} from "./controllers/attendance.controller.ts";
import {createRegularizationRequest, import {createRegularizationRequest, reviewRegularizationRequest} from "./controllers/regularization.controller.ts"
getPendingRegularizations,
reviewRegularizationRequest} from "./controllers/regularization.controller.ts"
import { requireAuth, requireRole, AppRole } from "../shared/auth.ts";
const router = new Router(); const router = new Router();
const apiV1 = new Router(); const apiV1 = new Router();
@ -17,14 +14,8 @@ const apiV1 = new Router();
apiV1.get("/attendance/logs", getRawLogs); apiV1.get("/attendance/logs", getRawLogs);
apiV1.post("/attendance/process-daily", processDailyAttendance); apiV1.post("/attendance/process-daily", processDailyAttendance);
// Regularization Routes apiV1.post("/attendance/regularize", createRegularizationRequest);
apiV1.post("/attendance/regularize", requireAuth, requireRole([AppRole.EMPLOYEE, AppRole.ADMIN]), createRegularizationRequest); apiV1.post("/attendance/regularize/review", reviewRegularizationRequest);
// Admin fetches requests assigned to their branches
apiV1.get("/attendance/regularize/pending", requireAuth, requireRole([AppRole.ADMIN, AppRole.SUPER_ADMIN]), getPendingRegularizations);
// Admin reviews requests
apiV1.post("/attendance/regularize/review", requireAuth, requireRole([AppRole.ADMIN, AppRole.SUPER_ADMIN]), reviewRegularizationRequest);
apiV1.get("/attendance/my-summary", getEmployeeSummary); apiV1.get("/attendance/my-summary", getEmployeeSummary);

View File

@ -1,6 +1,5 @@
// shared/auth.ts // shared/auth.ts
import { Context, Next } from "@oak/oak"; import { Context, Next } from "@oak/oak";
import { getDbPool } from "./db.ts";
export enum AppRole { export enum AppRole {
SUPER_ADMIN = "SUPER_ADMIN", SUPER_ADMIN = "SUPER_ADMIN",
@ -9,49 +8,35 @@ export enum AppRole {
EMPLOYEE = "EMPLOYEE", EMPLOYEE = "EMPLOYEE",
} }
// 1. Authentication Middleware // 1. Authentication Middleware (Who are you?)
export const requireAuth = async (ctx: Context, next: Next) => { export const requireAuth = async (ctx: Context, next: Next) => {
const isAuthenticated = true; // Mocking the authorization for now.
// Later, we will extract the JWT from ctx.request.headers.get("Authorization")
// and verify it with your SSO provider here.
const isAuthenticated = true; // Simulating a successful login
if (!isAuthenticated) { if (!isAuthenticated) {
ctx.response.status = 401; ctx.response.status = 401;
ctx.response.body = { success: false, message: "Missing or invalid token" }; ctx.response.body = { success: false, message: "Missing or invalid token" };
return; return;
} }
// FIX: Explicitly define the type as AppRole so TypeScript allows comparisons // Injecting a mock user state so the next middleware can read it
const mockRole: AppRole = AppRole.ADMIN;
const mockEmployeeId = 135;
let managedBranches: number[] = [];
if (mockRole !== AppRole.SUPER_ADMIN) {
try {
const emsDb = getDbPool("hrms_ems");
const [rows]: any = await emsDb.execute(
`SELECT branch_id FROM branch_admins WHERE employee_id = ?`,
[mockEmployeeId]
);
managedBranches = rows.map((r: any) => r.branch_id);
if (managedBranches.length === 0) managedBranches = [1];
} catch (error) {
console.error("Failed to fetch branch admin mappings", error);
}
}
ctx.state.user = { ctx.state.user = {
employee_id: mockEmployeeId, employee_id: 135, // Example ID
role: mockRole, role: AppRole.SUPER_ADMIN, // Change this to test different access levels
managed_branches: managedBranches branch_id: 1
}; };
await next(); await next();
}; };
// 2. Authorization Middleware // 2. Authorization Middleware (What are you allowed to do?)
export const requireRole = (allowedRoles: AppRole[]) => { export const requireRole = (allowedRoles: AppRole[]) => {
return async (ctx: Context, next: Next) => { return async (ctx: Context, next: Next) => {
const userRole = ctx.state.user?.role; const userRole = ctx.state.user?.role;
if (!userRole || !allowedRoles.includes(userRole)) { if (!userRole || !allowedRoles.includes(userRole)) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { ctx.response.body = {
@ -60,6 +45,7 @@ export const requireRole = (allowedRoles: AppRole[]) => {
}; };
return; return;
} }
await next(); await next();
}; };
}; };