Compare commits

..

No commits in common. "1b69e9f14c1fe09ae6c926d74486ce2f0664a467" and "65e0f601a0ce6e712099c4b35c51f5717d9eba4e" have entirely different histories.

20 changed files with 53 additions and 5369 deletions

View File

@ -1,519 +0,0 @@
import { Context } from "@oak/oak";
import { getDbPool } from "../../shared/db.ts";
const amsDb = getDbPool("hrms_ams");
const emsDb = getDbPool("hrms_ems");
/**
* Fetch top 20 raw biometric records for debugging/auditing
*/
export const getRawLogs = async (ctx: Context) => {
try {
const [rows] = await amsDb.execute(
"SELECT * FROM attendance_raw_logs ORDER BY attendance_time DESC LIMIT 20"
);
ctx.response.status = 200;
ctx.response.body = {
success: true,
data: rows,
};
} catch (error) {
console.error("DB Error:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, message: "Database query failed" };
}
};
// Helper function to generate an array of dates between two boundaries
const getDaysArray = (start: string, end: string): string[] => {
const arr: string[] = [];
const dt = new Date(start);
const endDt = new Date(end);
while (dt <= endDt) {
arr.push(new Date(dt).toISOString().split('T')[0]);
dt.setDate(dt.getDate() + 1);
}
return arr;
};
export const processDailyAttendance = async (ctx: Context) => {
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
let datesToProcess: string[] = [];
// 1. Determine date execution scope
if (body.work_date) {
datesToProcess.push(body.work_date);
} else if (body.start_date && body.end_date) {
datesToProcess = getDaysArray(body.start_date, body.end_date);
} else {
// Safely pull unique dates using direct string formatting in SQL to avoid Deno timezone shifting bugs
const [dateRows]: any = await amsDb.execute(
`SELECT DISTINCT DATE_FORMAT(attendance_time, '%Y-%m-%d') as raw_date FROM attendance_raw_logs ORDER BY raw_date ASC`
);
// Explicitly guarantee we extract the exact date string format without hitting Deno Date constructor limits
datesToProcess = dateRows.map((r: any) => {
if (r.raw_date instanceof Date) {
// Fallback if the driver parses it into a Date object despite format rules
const offset = r.raw_date.getTimezoneOffset();
const localDate = new Date(r.raw_date.getTime() - (offset * 60 * 1000));
return localDate.toISOString().split('T')[0];
}
return String(r.raw_date); // Clean 'YYYY-MM-DD' string representation
});
}
if (datesToProcess.length === 0) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "No dates found to process." };
return;
}
const amsConnection = await amsDb.getConnection();
let totalProcessedRecords = 0;
try {
await amsConnection.beginTransaction();
// Loop through every single date sequentially
for (const targetDate of datesToProcess) {
// 2. Fetch raw logs specifically grouped for this targetDate
const [rawLogs]: any = await amsConnection.execute(
`SELECT employee_code, DATE_FORMAT(attendance_time, '%Y-%m-%d %H:%i:%s') as att_time
FROM attendance_raw_logs
WHERE DATE(attendance_time) = ?
ORDER BY employee_code, attendance_time ASC`,
[targetDate]
);
if (rawLogs.length === 0) continue;
// Group raw logs by employee_code for this specific day
const groupedLogs = rawLogs.reduce((acc: any, log: any) => {
if (!acc[log.employee_code]) acc[log.employee_code] = [];
acc[log.employee_code].push(log.att_time);
return acc;
}, {});
// 3. Process calculations per employee for this targetDate
for (const empCode in groupedLogs) {
// Look up employee metadata from EMS
const [empRows]: any = await emsDb.execute(
`SELECT employee_id, is_active FROM employees WHERE employee_code = ?`,
[empCode]
);
if (empRows.length === 0 || !empRows[0].is_active) {
console.log(`[AMS Worker] Skipping unmapped or inactive code: ${empCode}`);
continue;
}
const employeeId = empRows[0].employee_id;
const punches = groupedLogs[empCode];
// Fetch shift details (Defaulting to shift_id 1 for evaluation)
const [shiftRows]: any = await amsConnection.execute(
`SELECT start_time, grace_period_minutes FROM shifts WHERE shift_id = 1`
);
const shift = shiftRows[0] || { start_time: "10:00:00", grace_period_minutes: 10 };
let check_in = null;
let check_out = null;
let worked_hours = 0.0;
let check_in_status = "ON_TIME";
let final_status = "ABSENT";
if (punches.length >= 2) {
check_in = punches[0];
check_out = punches[punches.length - 1];
const firstPunchMs = new Date(check_in.replace(' ', 'T')).getTime();
const lastPunchMs = new Date(check_out.replace(' ', 'T')).getTime();
worked_hours = Math.round(((lastPunchMs - firstPunchMs) / (1000 * 60 * 60)) * 100) / 100;
const rawCheckInTimeStr = check_in.split(' ')[1];
const [punchH, punchM, punchS] = rawCheckInTimeStr.split(':').map(Number);
const [shiftH, shiftM, shiftS] = shift.start_time.split(':').map(Number);
if ((punchH * 3600 + punchM * 60 + punchS) > (shiftH * 3600 + shiftM * 60 + shift.grace_period_minutes * 60)) {
check_in_status = "LATE";
}
if (worked_hours >= 7.0) final_status = "FULL_DAY";
else if (worked_hours >= 4.0) final_status = "HALF_DAY";
else final_status = "ABSENT";
} else if (punches.length === 1) {
check_in = punches[0];
check_in_status = "MISPUNCH";
final_status = "MISPUNCH";
}
// 4. Upsert calculations atomically into the processed daily ledger
await amsConnection.execute(
`INSERT INTO processed_daily_attendance
(employee_id, company_id, branch_id, work_date, check_in, check_out, worked_hours, check_in_status, final_status)
VALUES (?, 1, 1, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
check_in = VALUES(check_in),
check_out = VALUES(check_out),
worked_hours = VALUES(worked_hours),
check_in_status = VALUES(check_in_status),
final_status = VALUES(final_status)`,
[employeeId, targetDate, check_in, check_out, worked_hours, check_in_status, final_status]
);
totalProcessedRecords++;
}
}
await amsConnection.commit();
ctx.response.status = 200;
ctx.response.body = {
success: true,
message: `Successfully processed ${totalProcessedRecords} logs across ${datesToProcess.length} days.`
};
} catch (error) {
await amsConnection.rollback();
console.error("Bulk processing failed:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
} finally {
amsConnection.release();
}
};
export const getEmployeeSummary = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const employeeId = params.get("employee_id");
const startDate = params.get("start_date");
const endDate = params.get("end_date");
// Basic validation
if (!employeeId || !startDate || !endDate) {
ctx.response.status = 400;
ctx.response.body = {
success: false,
message: "Missing required query parameters. Expected: employee_id, start_date, and end_date."
};
return;
}
try {
// Fetch the individual breakdown rows for the period
const [rows]: any = await amsDb.execute(
`SELECT
attendance_id,
DATE_FORMAT(work_date, '%Y-%m-%d') as work_date,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours,
check_in_status,
final_status
FROM processed_daily_attendance
WHERE employee_id = ? AND work_date BETWEEN ? AND ?
ORDER BY work_date DESC`,
[employeeId, startDate, endDate]
);
// Dynamic metrics aggregation block
const summaryMetrics = rows.reduce(
(acc: any, row: any) => {
acc.total_days_tracked++;
acc.total_hours_worked += Number(row.worked_hours || 0);
// Count up categorical operational states
if (row.final_status === "FULL_DAY") acc.full_days++;
else if (row.final_status === "HALF_DAY") acc.half_days++;
else if (row.final_status === "ABSENT") acc.absences++;
else if (row.final_status === "MISPUNCH") acc.mispunches++;
if (row.check_in_status === "LATE") acc.late_arrivals++;
return acc;
},
{
total_days_tracked: 0,
total_hours_worked: 0,
full_days: 0,
half_days: 0,
absences: 0,
mispunches: 0,
late_arrivals: 0,
}
);
// Format final working duration to a neat 2-decimal scale
summaryMetrics.total_hours_worked = Math.round(summaryMetrics.total_hours_worked * 100) / 100;
ctx.response.status = 200;
ctx.response.body = {
success: true,
metrics: summaryMetrics,
history: rows
};
} catch (error) {
console.error("Failed to fetch employee summary:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const getDailyReport = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const targetDate = params.get("date");
if (!targetDate) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing required 'date' query parameter (YYYY-MM-DD)." };
return;
}
try {
// 1. Grab all calculated attendance states for this date from AMS
const [attendanceRows]: any = await amsDb.execute(
`SELECT employee_id, attendance_id,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours, check_in_status, final_status
FROM processed_daily_attendance
WHERE work_date = ?`,
[targetDate]
);
// 2. Fetch active corporate names and metadata mappings from EMS
const [employeeRows]: any = await emsDb.execute(
`SELECT e.employee_id, e.employee_code, p.first_name, p.last_name
FROM employees e
INNER JOIN partners p ON e.partner_id = p.partner_id
WHERE e.is_active = TRUE`
);
// Map the attendance records by employee_id for O(1) matching speed
const attendanceMap = attendanceRows.reduce((acc: any, row: any) => {
acc[row.employee_id] = row;
return acc;
}, {});
// Summary metrics counter block for the Admin dashboard cards
const summary = {
total_active_workforce: employeeRows.length,
present: 0,
late: 0,
mispunches: 0,
absent: 0
};
// 3. Seamlessly blend employee profiles with attendance states
const roster = employeeRows.map((emp: any) => {
const attendance = attendanceMap[emp.employee_id];
let status = "ABSENT";
let check_in = null;
let check_out = null;
let worked_hours = "0.00";
let punctuality = "N/A";
let attendance_id = null;
if (attendance) {
status = attendance.final_status;
check_in = attendance.check_in;
check_out = attendance.check_out;
worked_hours = attendance.worked_hours;
punctuality = attendance.check_in_status;
attendance_id = attendance.attendance_id;
// Process summary counter evaluations
if (status === "FULL_DAY" || status === "HALF_DAY") summary.present++;
if (status === "MISPUNCH") summary.mispunches++;
if (punctuality === "LATE") summary.late++;
} else {
summary.absent++;
}
return {
employee_id: emp.employee_id,
employee_code: emp.employee_code,
full_name: `${emp.first_name} ${emp.last_name}`,
attendance_id,
status,
check_in,
check_out,
worked_hours,
punctuality
};
});
ctx.response.status = 200;
ctx.response.body = {
success: true,
date: targetDate,
summary,
roster
};
} catch (error) {
console.error("Failed to generate daily admin report:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const getAdminRangeReport = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const fromDate = params.get("from_date");
const toDate = params.get("to_date");
const branchId = params.get("branch_id"); // Optional filter
const departmentId = params.get("department_id"); // Optional filter
if (!fromDate || !toDate) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing required date boundaries: from_date and to_date (YYYY-MM-DD)." };
return;
}
try {
// 1. Dynamically build the EMS query based on branch or department selection
let emsQuery = `
SELECT e.employee_id, e.employee_code, p.first_name, p.last_name, b.branch_name, d.name as department_name
FROM employees e
INNER JOIN partners p ON e.partner_id = p.partner_id
INNER JOIN branches b ON e.branch_id = b.branch_id
INNER JOIN contracts c ON e.employee_id = c.employee_id
INNER JOIN departments d ON c.department_id = d.department_id
WHERE e.is_active = TRUE AND c.status = 'ACTIVE'
`;
const emsParams: any[] = [];
if (branchId) {
emsQuery += ` AND e.branch_id = ?`;
emsParams.push(branchId);
}
if (departmentId) {
emsQuery += ` AND c.department_id = ?`;
emsParams.push(departmentId);
}
const [employees]: any = await emsDb.execute(emsQuery, emsParams);
if (employees.length === 0) {
ctx.response.status = 200;
ctx.response.body = { success: true, summary: {}, report: [] };
return;
}
const employeeIds = employees.map((e: any) => e.employee_id);
// 2. Query AMS for all processed ledger entries matching these exact employees in the date range
// Using a parameterized string hack for the IN clause since Deno MySQL handles arrays explicitly
const placeholders = employeeIds.map(() => "?").join(",");
const [attendanceRows]: any = await amsDb.execute(
`SELECT
employee_id,
DATE_FORMAT(work_date, '%Y-%m-%d') as work_date,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours, check_in_status, final_status
FROM processed_daily_attendance
WHERE work_date BETWEEN ? AND ? AND employee_id IN (${placeholders})
ORDER BY work_date ASC`,
[fromDate, toDate, ...employeeIds]
);
// Group logs by employee_id for structural assembly
const attendanceGrouped = attendanceRows.reduce((acc: any, row: any) => {
if (!acc[row.employee_id]) acc[row.employee_id] = [];
acc[row.employee_id].push(row);
return acc;
}, {});
// Overall global counters for the Admin's upper summary ribbon card metrics
const globalSummary = { total_records_evaluated: attendanceRows.length, full_days: 0, half_days: 0, late_instances: 0, mispunches: 0 };
const report = employees.map((emp: any) => {
const logs = attendanceGrouped[emp.employee_id] || [];
// Calculate individual metrics summary for this specific employee across the range
const individualMetrics = logs.reduce((acc: any, log: any) => {
if (log.final_status === "FULL_DAY") { acc.full_days++; globalSummary.full_days++; }
else if (log.final_status === "HALF_DAY") { acc.half_days++; globalSummary.half_days++; }
else if (log.final_status === "MISPUNCH") { acc.mispunches++; globalSummary.mispunches++; }
if (log.check_in_status === "LATE") { acc.late_arrivals++; globalSummary.late_instances++; }
return acc;
}, { full_days: 0, half_days: 0, mispunches: 0, late_arrivals: 0 });
return {
...emp,
range_metrics: individualMetrics,
attendance_history: logs
};
});
ctx.response.status = 200;
ctx.response.body = { success: true, date_range: { from: fromDate, to: toDate }, global_summary: globalSummary, report };
} catch (error) {
console.error("Admin range report compilation failed:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const getSingleEmployeeRangeReport = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const employeeId = params.get("employee_id");
const fromDate = params.get("from_date");
const toDate = params.get("to_date");
if (!employeeId || !fromDate || !toDate) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing query parameters. Required: employee_id, from_date, to_date." };
return;
}
try {
// Grab profile data
const [empProfile]: any = await emsDb.execute(
`SELECT e.employee_id, e.employee_code, p.first_name, p.last_name
FROM employees e INNER JOIN partners p ON e.partner_id = p.partner_id WHERE e.employee_id = ?`,
[employeeId]
);
if (empProfile.length === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Employee profile not found." };
return;
}
// Grab attendance logs for date-to-date query window
const [logs]: any = await amsDb.execute(
`SELECT
attendance_id,
DATE_FORMAT(work_date, '%Y-%m-%d') as work_date,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours, check_in_status, final_status
FROM processed_daily_attendance
WHERE employee_id = ? AND work_date BETWEEN ? AND ?
ORDER BY work_date ASC`,
[employeeId, fromDate, toDate]
);
ctx.response.status = 200;
ctx.response.body = {
success: true,
employee: empProfile[0],
range: { from: fromDate, to: toDate },
total_days: logs.length,
history: logs
};
} catch (error) {
console.error("Single employee range query failed:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};

View File

@ -1,173 +0,0 @@
import { Context } from "@oak/oak";
import { getDbPool } from "../../shared/db.ts";
const amsDb = getDbPool("hrms_ams");
export const createRegularizationRequest = async (ctx: Context) => {
if (!ctx.request.hasBody) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing request body." };
return;
}
const body = await ctx.request.body.json();
const { attendance_id, employee_id, requested_check_in, requested_check_out, reason } = body;
// Basic validation
if (!attendance_id || !employee_id || !reason || !requested_check_in) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing required fields: attendance_id, employee_id, requested_check_in, and reason." };
return;
}
try {
// Extract 'YYYY-MM-DD' safely from the requested_check_in string
const target_date = requested_check_in.split(' ')[0];
// Insert into the attendance_regularizations table including target_date
await amsDb.execute(
`INSERT INTO attendance_regularizations
(attendance_id, employee_id, target_date, requested_check_in, requested_check_out, reason, status)
VALUES (?, ?, ?, ?, ?, ?, 'PENDING')`,
[attendance_id, employee_id, target_date, requested_check_in, requested_check_out || null, reason]
);
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Regularization request submitted successfully and is pending approval.",
};
} catch (error) {
console.error("Failed to submit regularization:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const reviewRegularizationRequest = async (ctx: Context) => {
if (!ctx.request.hasBody) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing request body." };
return;
}
const body = await ctx.request.body.json();
const { regularization_id, action, reviewed_by_id } = body; // action can be 'APPROVED' or 'REJECTED'
if (!regularization_id || !reviewed_by_id || !["APPROVED", "REJECTED"].includes(action)) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Invalid payload. Required: regularization_id, reviewed_by_id, action." };
return;
}
const amsConnection = await amsDb.getConnection();
try {
await amsConnection.beginTransaction();
// 1. Fetch the regularization request details
const [requestRows]: any = await amsConnection.execute(
`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 = ?`,
[regularization_id]
);
if (requestRows.length === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Regularization request not found." };
await amsConnection.rollback();
return;
}
const request = requestRows[0];
if (request.status !== "PENDING") {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "This request has already been processed." };
await amsConnection.rollback();
return;
}
// 2. Update the regularization request record status and reviewer ID (Removed 'remarks')
await amsConnection.execute(
`UPDATE attendance_regularizations
SET status = ?, reviewed_by_id = ?
WHERE regularization_id = ?`,
[action, reviewed_by_id, regularization_id]
);
// 3. If APPROVED, dynamically overwrite the target day's calculated ledger row
if (action === "APPROVED") {
let worked_hours = 0.0;
let final_status = "FULL_DAY";
let check_in_status = "ON_TIME";
// Fetch shift details to accurately re-evaluate punctuality boundaries (Defaulting to shift_id 1)
const [shiftRows]: any = await amsConnection.execute(
`SELECT start_time, grace_period_minutes FROM shifts WHERE shift_id = 1`
);
const shift = shiftRows[0] || { start_time: "10:00:00", grace_period_minutes: 10 };
if (request.requested_check_in && request.requested_check_out) {
// --- A. Recalculate Working Duration Metrics ---
const checkInMs = new Date(request.requested_check_in.replace(' ', 'T')).getTime();
const checkOutMs = new Date(request.requested_check_out.replace(' ', 'T')).getTime();
worked_hours = Math.round(((checkOutMs - checkInMs) / (1000 * 60 * 60)) * 100) / 100;
if (worked_hours >= 7.0) final_status = "FULL_DAY";
else if (worked_hours >= 4.0) final_status = "HALF_DAY";
else final_status = "ABSENT";
// --- B. Dynamically Re-evaluate Punctuality Status (Preserves LATE flags) ---
const rawCheckInTimeStr = request.requested_check_in.split(' ')[1];
const [punchH, punchM, punchS] = rawCheckInTimeStr.split(':').map(Number);
const [shiftH, shiftM, shiftS] = shift.start_time.split(':').map(Number);
const punchTotalSeconds = punchH * 3600 + punchM * 60 + punchS;
const shiftCutoffSeconds = shiftH * 3600 + shiftM * 60 + (shift.grace_period_minutes * 60);
if (punchTotalSeconds > shiftCutoffSeconds) {
check_in_status = "LATE";
}
}
await amsConnection.execute(
`UPDATE processed_daily_attendance
SET check_in = ?,
check_out = ?,
worked_hours = ?,
final_status = ?,
check_in_status = ?
WHERE attendance_id = ?`,
[
request.requested_check_in,
request.requested_check_out,
worked_hours,
final_status,
check_in_status,
request.attendance_id
]
);
}
await amsConnection.commit();
ctx.response.status = 200;
ctx.response.body = {
success: true,
message: `Request has been successfully ${action.toLowerCase()} by admin ID ${reviewed_by_id}.`,
};
} catch (error) {
await amsConnection.rollback();
console.error("Admin review action failed:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
} finally {
amsConnection.release();
}
};

View File

@ -1,16 +0,0 @@
import { Application } from "@oak/oak";
import router from "./routes.ts";
const app = new Application();
const PORT = 8002; // Running AMS on 8002 to avoid conflict with EMS on 8001
app.use(async (ctx, next) => {
console.log(`[AMS] ${ctx.request.method} ${ctx.request.url.pathname}`);
await next();
});
app.use(router.routes());
app.use(router.allowedMethods());
console.log(`AMS Service running on http://localhost:${PORT}`);
await app.listen({ port: PORT });

View File

@ -1,28 +0,0 @@
import { Router } from "@oak/oak";
import {
getRawLogs,
processDailyAttendance,
getEmployeeSummary,
getDailyReport,
getAdminRangeReport,
getSingleEmployeeRangeReport} from "./controllers/attendance.controller.ts";
import {createRegularizationRequest, reviewRegularizationRequest} from "./controllers/regularization.controller.ts"
const router = new Router();
const apiV1 = new Router();
apiV1.get("/attendance/logs", getRawLogs);
apiV1.post("/attendance/process-daily", processDailyAttendance);
apiV1.post("/attendance/regularize", createRegularizationRequest);
apiV1.post("/attendance/regularize/review", reviewRegularizationRequest);
apiV1.get("/attendance/my-summary", getEmployeeSummary);
apiV1.get("/attendance/daily-report", getDailyReport);
apiV1.get("/attendance/admin-report", getAdminRangeReport);
apiV1.get("/attendance/employee-range-report", getSingleEmployeeRangeReport);
router.use("/api/v1", apiV1.routes(), apiV1.allowedMethods());
export default router;

View File

@ -1,9 +1,9 @@
version: '3.8'
services:
hrms_db:
ems_mysql:
image: docker.io/library/mysql:8.4
container_name: hrms_db
container_name: hrms_ems_db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: hrms_ems
@ -12,9 +12,9 @@ services:
ports:
- "3306:3306"
volumes:
- ./init-db:/docker-entrypoint-initdb.d:Z
- hrms_db_data:/var/lib/mysql
- ./init-db:/docker-entrypoint-initdb.d:Z # <-- Added :Z here
- mysql_ems_data:/var/lib/mysql
restart: unless-stopped
volumes:
hrms_db_data:
mysql_ems_data:

File diff suppressed because it is too large Load Diff

View File

@ -1,98 +0,0 @@
import { getDbPool } from "../../shared/db.ts"
const pool = getDbPool("hrms_ems");
export const getEmployeeContracts = async (ctx: any) => {
const employeeId = ctx.params.id;
try {
const [rows] = await pool.query(
`SELECT
c.contract_id,
c.work_email,
c.date_joining,
c.probation_days,
c.status,
c.salary_structure_id,
d.name AS department,
j.title AS designation
FROM contracts c
JOIN employees e ON c.employee_id = e.employee_id
JOIN departments d ON c.department_id = d.department_id
JOIN job_positions j ON c.job_id = j.job_id
WHERE c.employee_id = ?
ORDER BY c.date_joining DESC`,
[employeeId]
);
ctx.response.status = 200;
ctx.response.body = {
success: true,
count: (rows as any[]).length,
data: rows
};
} catch (error) {
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: (error as Error).message
};
}
};
export const createContract = async (ctx: any) => {
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 1. Expire any currently active contracts for this specific employee
await connection.execute(
`UPDATE contracts SET status = 'EXPIRED' WHERE employee_id = ? AND status = 'ACTIVE'`,
[data.employeeId]
);
// 2. Insert the brand new active contract
const [result] = await connection.execute(
`INSERT INTO contracts (employee_id, department_id, job_id, date_joining, probation_days, status, salary_structure_id)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
data.employeeId,
data.departmentId,
data.jobId,
data.dateJoining,
data.probationDays,
'ACTIVE',
data.salaryStructureId
]
);
await connection.commit();
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "New contract executed successfully. Previous contracts expired.",
contract_id: (result as any).insertId
};
} catch (error) {
await connection.rollback();
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: "Transaction failed: " + (error as Error).message
};
} finally {
connection.release();
}
};

View File

@ -1,346 +0,0 @@
import { getDbPool } from "../../shared/db.ts";
import { generateNextCode } from "../../shared/sequence.ts";
const pool = getDbPool("hrms_ems");
export const getEmployees = async (ctx: any) => {
try {
// FIX: Filter by ACTIVE contract to prevent duplicate rows for employees with past contracts
const [rows] = await pool.query(
`SELECT
e.employee_id,
e.employee_code,
p.first_name,
p.last_name,
d.name AS department,
j.title AS designation,
c.work_email
FROM employees e
JOIN partners p ON e.partner_id = p.partner_id
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
JOIN departments d ON c.department_id = d.department_id
JOIN job_positions j ON c.job_id = j.job_id
WHERE e.is_active = true`,
);
ctx.response.body = {
success: true,
count: (rows as any[]).length,
data: rows,
};
} catch (error) {
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: (error as Error).message,
};
}
};
export const createEmployee = async (ctx: any) => {
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
let empCode = data.employeeCode;
if (!empCode || empCode.trim() === "") {
empCode = await generateNextCode("EMPLOYEE_MAIN", "CLRI", 3);
}
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const [partnerResult] = await connection.execute(
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
VALUES (?, ?, ?, ?, ?, ?)`,
[
data.firstName,
data.lastName,
data.dob,
data.gender,
data.personalEmail,
data.personalPhone,
],
);
const partnerId = (partnerResult as any).insertId;
await connection.execute(
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
partnerId,
data.address.type,
data.address.doorNumber,
data.address.landmark,
data.address.line,
data.address.pincode,
data.address.district,
data.address.state,
],
);
const [employeeResult] = await connection.execute(
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
VALUES (?, ?, ?, ?, ?)`,
[empCode, partnerId, data.companyId, data.branchId, true],
);
const employeeId = (employeeResult as any).insertId;
await connection.execute(
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, probation_days, status, salary_structure_id, reporting_to_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
employeeId,
data.departmentId,
data.jobId,
data.work_email,
data.dateJoining,
data.probationDays,
"ACTIVE",
data.salaryStructureId,
data.reportingToId || null,
],
);
await connection.commit();
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Employee created successfully",
employee_id: employeeId,
employee_code: empCode,
};
} catch (error) {
await connection.rollback();
let errorMessage = (error as Error).message;
if (errorMessage.includes("Duplicate entry")) {
ctx.response.status = 409;
ctx.response.body = {
success: false,
error:
"A unique constraint failed. The Employee Code, Personal Email, or Phone number already exists.",
};
return;
}
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: "Transaction failed: " + errorMessage,
};
} finally {
connection.release();
}
};
export const getEmployeeById = async (ctx: any) => {
const id = ctx.params.id;
try {
// 1. Fetch core employee, partner, contract, and manager details
const [empRows] = await pool.query(
`SELECT
e.employee_id,
e.employee_code,
e.is_active,
p.first_name,
p.last_name,
p.dob,
p.gender,
p.personal_email,
p.personal_phone,
c.work_email,
c.date_joining,
c.probation_days,
c.status AS contract_status,
d.name AS department,
d.department_id,
j.title AS designation,
j.job_id,
mgr_p.first_name AS manager_first_name,
mgr_p.last_name AS manager_last_name,
mgr_e.employee_code AS manager_employee_code
FROM employees e
JOIN partners p ON e.partner_id = p.partner_id
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
JOIN departments d ON c.department_id = d.department_id
JOIN job_positions j ON c.job_id = j.job_id
LEFT JOIN employees mgr_e ON c.reporting_to_id = mgr_e.employee_id
LEFT JOIN partners mgr_p ON mgr_e.partner_id = mgr_p.partner_id
WHERE e.employee_id = ?`,
[id],
);
const empData = empRows as any[];
if (empData.length === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Employee not found" };
return;
}
const employee = empData[0];
// 2. Fetch all addresses for this partner separately to return as an array
const [addrRows] = await pool.query(
`SELECT address_type, door_number, landmark, address_line, pincode, district, state
FROM addresses
WHERE partner_id = (SELECT partner_id FROM employees WHERE employee_id = ?)`,
[id],
);
// 3. Combine into a single structured JSON response
ctx.response.status = 200;
ctx.response.body = {
success: true,
data: {
employee_id: employee.employee_id,
employee_code: employee.employee_code,
is_active: employee.is_active,
first_name: employee.first_name,
last_name: employee.last_name,
dob: employee.dob,
gender: employee.gender,
personal_email: employee.personal_email,
personal_phone: employee.personal_phone,
contract: {
work_email: employee.work_email,
date_joining: employee.date_joining,
probation_days: employee.probation_days,
status: employee.contract_status,
department: employee.department,
department_id: employee.department_id,
designation: employee.designation,
job_id: employee.job_id,
manager: employee.manager_employee_code
? {
employee_code: employee.manager_employee_code,
first_name: employee.manager_first_name,
last_name: employee.manager_last_name,
}
: null,
},
addresses: addrRows, // Array of all addresses (PERMANENT, CURRENT, EMERGENCY)
},
};
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const updateEmployee = async (ctx: any) => {
const id = ctx.params.id;
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const [employeeRows] = await connection.execute(
`SELECT partner_id FROM employees WHERE employee_id = ?`,
[id],
);
const employees = employeeRows as any[];
if (employees.length === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Employee not found" };
return;
}
const partnerId = employees[0].partner_id;
await connection.execute(
`UPDATE partners
SET first_name = ?, last_name = ?, personal_email = ?, personal_phone = ?
WHERE partner_id = ?`,
[
data.firstName,
data.lastName,
data.personalEmail,
data.personalPhone,
partnerId,
],
);
await connection.execute(
`UPDATE addresses
SET door_number = ?, landmark = ?, address_line = ?, pincode = ?, district = ?, state = ?
WHERE partner_id = ? AND address_type = ?`,
[
data.address.doorNumber,
data.address.landmark,
data.address.line,
data.address.pincode,
data.address.district,
data.address.state,
partnerId,
data.address.type,
],
);
await connection.commit();
ctx.response.status = 200;
ctx.response.body = {
success: true,
message: "Employee profile updated successfully",
};
} catch (error) {
await connection.rollback();
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: "Update failed: " + (error as Error).message,
};
} finally {
connection.release();
}
};
export const deleteEmployee = async (ctx: any) => {
const id = ctx.params.id;
try {
const [result] = await pool.execute(
`UPDATE employees SET is_active = false WHERE employee_id = ?`,
[id],
);
const updateResult = result as any;
if (updateResult.affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Employee not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = {
success: true,
message: "Employee account deactivated successfully",
};
} catch (error) {
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: "Deactivation failed: " + (error as Error).message,
};
}
};

View File

@ -1,443 +0,0 @@
import { getDbPool } from "../../shared/db.ts"
import {generateNextCode} from "../../shared/sequence.ts"
const pool = getDbPool("hrms_ems");
export const getCompanies = async (ctx: any) => {
try {
const [rows] = await pool.query(`SELECT company_id, name FROM companies`);
ctx.response.status = 200;
ctx.response.body = { success: true, data: rows };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const getBranches = async (ctx: any) => {
try {
const [rows] = await pool.query(`SELECT branch_id, branch_name,code, company_id FROM branches`);
ctx.response.status = 200;
ctx.response.body = { success: true, data: rows };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const getDepartments = async (ctx: any) => {
try {
const [rows] = await pool.query(`SELECT department_id, name, parent_id FROM departments`);
ctx.response.status = 200;
ctx.response.body = { success: true, data: rows };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const getJobs = async (ctx: any) => {
try {
const [rows] = await pool.query(`SELECT job_id, title, department_id FROM job_positions`);
ctx.response.status = 200;
ctx.response.body = { success: true, data: rows };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
// ==========================================
// COMPANIES MANAGEMENT
// ==========================================
export const createCompany = async (ctx: any) => {
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
try {
const [result] = await pool.execute(
`INSERT INTO companies (name, parent_id) VALUES (?, ?)`,
[data.name, data.parentId || null]
);
ctx.response.status = 201;
ctx.response.body = { success: true, message: "Company created", company_id: (result as any).insertId };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const updateCompany = async (ctx: any) => {
const id = ctx.params.id;
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
try {
const [result] = await pool.execute(
`UPDATE companies SET name = ?, parent_id = ? WHERE company_id = ?`,
[data.name, data.parentId || null, id]
);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Company not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Company updated successfully" };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const deleteCompany = async (ctx: any) => {
const id = ctx.params.id;
try {
const [result] = await pool.execute(`DELETE FROM companies WHERE company_id = ?`, [id]);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Company not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Company deleted successfully" };
} catch (error) {
let errorMessage = (error as Error).message;
// Catch ON DELETE RESTRICT from employees table
if (errorMessage.includes("foreign key constraint fails")) {
errorMessage = "Cannot delete company. It is currently assigned to one or more employees.";
}
ctx.response.status = 409;
ctx.response.body = { success: false, error: errorMessage };
}
};
// ==========================================
// BRANCHES MANAGEMENT
// ==========================================
export const createBranch = async (ctx: any) => {
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
let branchCode = data.code;
// If no code is provided, automatically generate a sequential one
if (!branchCode || branchCode.trim() === "") {
// Get first 3 letters (e.g., "Bengaluru" -> "BEN")
const shortName = data.branchName ? data.branchName.substring(0, 3).toUpperCase() : 'BRN';
// Generate: Sequence ID 'BRANCH_BEN', Prefix 'BEN-', Padding 3 -> Output: 'BEN-001'
branchCode = await generateNextCode(`BRANCH_${shortName}`, `${shortName}-`, 3);
}
try {
const [result] = await pool.execute(
`INSERT INTO branches (company_id, branch_name, code) VALUES (?, ?, ?)`,
[data.companyId, data.branchName, branchCode]
);
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Branch created successfully",
branch_id: (result as any).insertId,
code: branchCode
};
} catch (error) {
let errorMessage = (error as Error).message;
if (errorMessage.includes("Duplicate entry")) {
ctx.response.status = 409;
ctx.response.body = { success: false, error: `The branch code '${branchCode}' already exists.` };
return;
}
ctx.response.status = 500;
ctx.response.body = { success: false, error: errorMessage };
}
};
export const updateBranch = async (ctx: any) => {
const id = ctx.params.id;
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
try {
const [result] = await pool.execute(
`UPDATE branches SET company_id = ?, branch_name = ?, code = ? WHERE branch_id = ?`,
[data.companyId, data.branchName, data.code, id]
);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Branch not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Branch updated successfully" };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const deleteBranch = async (ctx: any) => {
const id = ctx.params.id;
try {
const [result] = await pool.execute(`DELETE FROM branches WHERE branch_id = ?`, [id]);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Branch not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Branch deleted successfully" };
} catch (error) {
let errorMessage = (error as Error).message;
// Catch ON DELETE RESTRICT from employees table
if (errorMessage.includes("foreign key constraint fails")) {
errorMessage = "Cannot delete branch. It is currently assigned to one or more employees.";
}
ctx.response.status = 409;
ctx.response.body = { success: false, error: errorMessage };
}
};
// ==========================================
// DEPARTMENTS MANAGEMENT
// ==========================================
export const createDepartment = async (ctx: any) => {
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
if (!data.companyId || !data.branchId || !data.name) {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "companyId, branchId, and name are required." };
return;
}
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 1. Insert the department
const [result] = await connection.execute(
`INSERT INTO departments (company_id, branch_id, name, parent_id) VALUES (?, ?, ?, ?)`,
[data.companyId, data.branchId, data.name, data.parentId ?? null]
);
const departmentId = (result as any).insertId;
// 2. If a manager is provided, add them to the junction table
if (data.managerId) {
await connection.execute(
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
[departmentId, data.managerId]
);
}
await connection.commit();
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Department created",
department_id: departmentId
};
} catch (error) {
await connection.rollback();
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
} finally {
connection.release();
}
};
export const updateDepartment = async (ctx: any) => {
const id = ctx.params.id;
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 1. Update core department details
const [result] = await connection.execute(
`UPDATE departments SET company_id = ?, branch_id = ?, name = ?, parent_id = ? WHERE department_id = ?`,
[data.companyId ?? null, data.branchId ?? null, data.name ?? null, data.parentId ?? null, id]
);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Department not found" };
await connection.rollback();
connection.release();
return;
}
// 2. Manage reassignment if a new manager is passed in the update
if (data.managerId) {
// For simplicity in this update endpoint, we overwrite the existing managers.
// You can create a dedicated POST /departments/:id/managers endpoint later for multi-manager logic.
await connection.execute(`DELETE FROM department_managers WHERE department_id = ?`, [id]);
await connection.execute(
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
[id, data.managerId]
);
}
await connection.commit();
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Department updated successfully" };
} catch (error) {
await connection.rollback();
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
} finally {
connection.release();
}
};
export const deleteDepartment = async (ctx: any) => {
const id = ctx.params.id;
try {
const [result] = await pool.execute(`DELETE FROM departments WHERE department_id = ?`, [id]);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Department not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Department deleted successfully" };
} catch (error) {
let errorMessage = (error as Error).message;
// Catch ON DELETE RESTRICT from contracts table
if (errorMessage.includes("foreign key constraint fails")) {
errorMessage = "Cannot delete department. There are active or historical contracts tied to this department.";
}
ctx.response.status = 409;
ctx.response.body = { success: false, error: errorMessage };
}
};
// ==========================================
// JOB POSITIONS MANAGEMENT
// ==========================================
export const createJob = async (ctx: any) => {
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
if (!data.departmentId || !data.title) {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "departmentId and title are required fields." };
return;
}
try {
const [result] = await pool.execute(
`INSERT INTO job_positions (department_id, title, description) VALUES (?, ?, ?)`,
[data.departmentId, data.title, data.description ?? null]
);
ctx.response.status = 201;
ctx.response.body = { success: true, message: "Job created", job_id: (result as any).insertId };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
export const updateJob = async (ctx: any) => {
const id = ctx.params.id;
const body = ctx.request.body;
if (body.type() !== "json") {
ctx.response.status = 400;
ctx.response.body = { success: false, error: "Invalid JSON body" };
return;
}
const data = await body.json();
try {
const [result] = await pool.execute(
`UPDATE job_positions SET department_id = ?, title = ?, description = ? WHERE job_id = ?`,
[data.departmentId ?? null, data.title ?? null, data.description ?? null, id]
);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Job not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Job updated successfully" };
} catch (error) {
ctx.response.status = 500;
ctx.response.body = { success: false, error: (error as Error).message };
}
};
// Note: deleteJob function remains the same as before.
export const deleteJob = async (ctx: any) => {
const id = ctx.params.id;
try {
const [result] = await pool.execute(`DELETE FROM job_positions WHERE job_id = ?`, [id]);
if ((result as any).affectedRows === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Job not found" };
return;
}
ctx.response.status = 200;
ctx.response.body = { success: true, message: "Job deleted successfully" };
} catch (error) {
let errorMessage = (error as Error).message;
// Catch ON DELETE RESTRICT from contracts table
if (errorMessage.includes("foreign key constraint fails")) {
errorMessage = "Cannot delete job position. It is currently linked to one or more employee contracts.";
}
ctx.response.status = 409;
ctx.response.body = { success: false, error: errorMessage };
}
};

View File

@ -1,173 +0,0 @@
import { Context } from "@oak/oak";
import { getDbPool } from "../../shared/db.ts";
const emsPool = getDbPool("hrms_ems");
export const bulkSeedEmployees = async (ctx: Context) => {
const body = ctx.request.hasBody ? await ctx.request.body.json() : [];
if (!Array.isArray(body) || body.length === 0) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Provide an array of employee logs." };
return;
}
const connection = await emsPool.getConnection();
let insertedCount = 0;
try {
await connection.beginTransaction();
for (const item of body) {
const { emp_code, name, designation, department, email } = item;
const nameParts = name.trim().split(" ");
const firstName = nameParts[0] || "Employee";
const lastName = nameParts.slice(1).join(" ") || "LNU";
// 1. Insert into partners
const [partnerResult]: any = await connection.execute(
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
VALUES (?, ?, '1995-01-01', 'Not Specified', ?, ?)
ON DUPLICATE KEY UPDATE partner_id = LAST_INSERT_ID(partner_id)`,
[firstName, lastName, `personal.${email}`, `MOCK_${emp_code}`]
);
const partnerId = partnerResult.insertId;
// 2. Ensure department exists (FIXED: Added branch_id to satisfy NOT NULL constraint)
const [deptResult]: any = await connection.execute(
`INSERT INTO departments (company_id, branch_id, name)
VALUES (1, 1, ?)
ON DUPLICATE KEY UPDATE department_id = LAST_INSERT_ID(department_id)`,
[department || "General"]
);
const departmentId = deptResult.insertId;
// 3. Ensure job position exists (FIXED: Linked to department_id instead of company_id)
const [jobResult]: any = await connection.execute(
`INSERT INTO job_positions (department_id, title)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE job_id = LAST_INSERT_ID(job_id)`,
[departmentId, designation || "Trainee"]
);
const jobId = jobResult.insertId;
// 4. Create core Employee record
const [empResult]: any = await connection.execute(
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
VALUES (?, ?, 1, 1, TRUE)
ON DUPLICATE KEY UPDATE employee_id = LAST_INSERT_ID(employee_id)`,
[emp_code, partnerId]
);
const employeeId = empResult.insertId;
// 5. Establish operational Contract
await connection.execute(
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, status, salary_structure_id)
VALUES (?, ?, ?, ?, '2026-01-01', 'ACTIVE', 100)
ON DUPLICATE KEY UPDATE work_email = VALUES(work_email)`,
[employeeId, departmentId, jobId, email]
);
insertedCount++;
}
await connection.commit();
ctx.response.status = 200;
ctx.response.body = {
success: true,
message: `Successfully seeded/aligned ${insertedCount} normalized employee profiles.`,
};
} catch (error) {
await connection.rollback();
console.error("Seeding failed, changes rolled back:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
ctx.response.status = 500;
ctx.response.body = { success: false, error: errorMessage };
} finally {
connection.release();
}
};
export const bulkMapHierarchy = async (ctx: Context) => {
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
const { employeeHierarchy = [], departmentHierarchy = [] } = body;
if (employeeHierarchy.length === 0 && departmentHierarchy.length === 0) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Provide hierarchy mapping arrays." };
return;
}
const connection = await emsPool.getConnection();
let contractsUpdated = 0;
let departmentsUpdated = 0;
try {
await connection.beginTransaction();
// 1. Map Employees to their Managers (Contracts table)
for (const mapping of employeeHierarchy) {
const { emp_code, manager_code } = mapping;
if (!emp_code || !manager_code) continue;
const [result]: any = await connection.execute(
`UPDATE contracts c
JOIN employees e ON c.employee_id = e.employee_id
JOIN employees m ON m.employee_code = ?
SET c.reporting_to_id = m.employee_id
WHERE e.employee_code = ? AND c.status = 'ACTIVE'`,
[manager_code, emp_code]
);
if (result.affectedRows > 0) contractsUpdated++;
}
// 2. Map Departments to Managers and Parent Departments
for (const dept of departmentHierarchy) {
const { department_name, manager_code, parent_department_name } = dept;
if (!department_name) continue;
// FIXED: Use department_managers junction table instead of departments.manager_id
if (manager_code) {
await connection.execute(
`INSERT IGNORE INTO department_managers (department_id, employee_id)
SELECT d.department_id, m.employee_id
FROM departments d
JOIN employees m ON m.employee_code = ?
WHERE d.name = ?`,
[manager_code, department_name]
);
}
// Update Parent Department Hierarchy
if (parent_department_name) {
await connection.execute(
`UPDATE departments d
JOIN departments p ON p.name = ?
SET d.parent_id = p.department_id
WHERE d.name = ?`,
[parent_department_name, department_name]
);
}
departmentsUpdated++;
}
await connection.commit();
ctx.response.status = 200;
ctx.response.body = {
success: true,
message: "Hierarchy mapping completed successfully.",
metrics: { contractsUpdated, departmentsProcessed: departmentsUpdated }
};
} catch (error) {
await connection.rollback();
console.error("Hierarchy mapping failed:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
ctx.response.status = 500;
ctx.response.body = { success: false, error: errorMessage };
} finally {
connection.release();
}
};

View File

@ -1,82 +0,0 @@
// generate_mapping.ts
import data from "./AllStaff.json" with { type: "json" };
// 1. Helper to clean up messy department strings
const normalizeDept = (dept: string) => {
if (dept.toLowerCase() === "finance" || dept === "FInance") return "Finance";
if (dept.toLowerCase() === "digital marketing") return "Digital Marketing";
return dept.trim();
};
// 2. Helper to determine leadership rank based on designation
const getRank = (designation: string) => {
const title = designation.toLowerCase();
if (title.includes("cfo") || title.includes("vice president") || title.includes("co- founder")) return 100;
if (title.includes("senior manager")) return 90;
if (title.includes("manager") && !title.includes("assistant")) return 80;
if (title.includes("assistant manager")) return 70;
if (title.includes("team leader")) return 60;
return 0; // Regular employee
};
const departmentsMap = new Map();
const employeeHierarchy: any[] = [];
const departmentHierarchy: any[] = [];
// 3. First Pass: Group employees by department and find the leader
for (const emp of data) {
const deptName = normalizeDept(emp.department);
const rank = getRank(emp.designation);
if (!departmentsMap.has(deptName)) {
departmentsMap.set(deptName, { name: deptName, leaderCode: null, highestRank: -1 });
}
const currentDept = departmentsMap.get(deptName);
// If this employee has a higher rank than the current department leader, replace them
if (rank > currentDept.highestRank) {
currentDept.leaderCode = emp.emp_code;
currentDept.highestRank = rank;
}
}
// 4. Second Pass: Build the mappings
for (const emp of data) {
const deptName = normalizeDept(emp.department);
const deptInfo = departmentsMap.get(deptName);
// Only map the employee if they aren't the leader themselves and a leader exists
let managerCode = deptInfo.leaderCode;
if (managerCode === emp.emp_code) {
managerCode = null; // The top boss doesn't report to themselves in this scope
}
employeeHierarchy.push({
emp_code: emp.emp_code,
manager_code: managerCode
});
}
// 5. Build Department Hierarchy
for (const [deptName, info] of departmentsMap.entries()) {
departmentHierarchy.push({
department_name: deptName,
manager_code: info.leaderCode,
parent_department_name: null // You can manually tweak parent/child relationships later
});
}
// 6. Write the final JSON payload
const output = {
departmentHierarchy,
employeeHierarchy
};
await Deno.writeTextFile(
"./hierarchy_mapping.json",
JSON.stringify(output, null, 2)
);
console.log(`Successfully mapped ${employeeHierarchy.length} employees across ${departmentsMap.size} departments!`);
console.log("File saved to ./hierarchy_mapping.json");

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,41 @@
import { Application } from "@oak/oak";
import router from "./routes.ts";
import { Application, Router } from "@oak/oak";
import pool from "../shared/db.ts";
const app = new Application();
const router = new Router();
// Define the endpoint Sufail will call from the Vite frontend
router.get("/api/employees", async (ctx) => {
try {
const [rows] = await pool.query(
`SELECT
e.employee_code,
p.first_name,
p.last_name,
d.name AS department,
j.title AS designation,
c.work_email
FROM employees e
JOIN partners p ON e.partner_id = p.partner_id
JOIN contracts c ON e.employee_id = c.employee_id
JOIN departments d ON c.department_id = d.department_id
JOIN job_positions j ON c.job_id = j.job_id`
);
ctx.response.body = {
success: true,
count: (rows as any[]).length,
data: rows
};
} catch (error) {
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: (error as Error).message
};
}
});
// Register the routes and allowed methods from routes.ts
app.use(router.routes());
app.use(router.allowedMethods());

View File

@ -1,100 +0,0 @@
import { Router } from "@oak/oak";
import {
getEmployees,
createEmployee,
getEmployeeById,
updateEmployee,
deleteEmployee
} from "./controllers/employee.controller.ts";
import {
getCompanies, createCompany, updateCompany, deleteCompany,
getBranches, createBranch, updateBranch, deleteBranch,
getDepartments, createDepartment, updateDepartment, deleteDepartment,
getJobs, createJob, updateJob, deleteJob
} from "./controllers/lookup.controller.ts";
import {
getEmployeeContracts,
createContract
} from "./controllers/contract.controller.ts";
import {
bulkSeedEmployees,
bulkMapHierarchy
} from "./controllers/system.controller.ts";
// Shared authentication and authorization middleware
import { requireAuth, requireRole, AppRole } from "../shared/auth.ts";
const router = new Router();
// ============================================================================
// EMPLOYEE CORE DOMAIN
// Manages the corporate identity (employees) and personal data (partners).
// ============================================================================
// Retrieve a list of all active employees. Visible to management and administrators.
router.get("/api/v1/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN, AppRole.MANAGER]), getEmployees);
// Retrieve a single employee's comprehensive profile (identity, address, contract). Accessible to any authenticated user.
router.get("/api/v1/employees/:id", requireAuth, getEmployeeById);
// Create a new employee profile and initial contract. Restricted to administrators.
router.post("/api/v1/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createEmployee);
// Update an existing employee's personal or address data. Restricted to administrators.
router.put("/api/v1/employees/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateEmployee);
// Soft-delete (deactivate) an employee account. Restricted to administrators.
router.delete("/api/v1/employees/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteEmployee);
// ============================================================================
// CONTRACT & ASSIGNMENT DOMAIN
// Manages operational assignments, reporting hierarchies, and job roles.
// ============================================================================
// Retrieve the contract history for an individual employee.
router.get("/api/v1/employees/:id/contracts", requireAuth, getEmployeeContracts);
// Expire current active contracts and execute a new employment agreement.
router.post("/api/v1/contracts", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createContract);
// ============================================================================
// ORGANIZATIONAL LOOKUP DOMAIN
// Read-only reference endpoints for the frontend UI to populate select options.
// ============================================================================
// Fetch legal entities and parent group structures.
router.get("/api/v1/companies", requireAuth, getCompanies);
router.post("/api/v1/companies", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createCompany);
router.put("/api/v1/companies/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateCompany);
router.delete("/api/v1/companies/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteCompany);
// Fetch structural branch offices and physical locations.
router.get("/api/v1/branches", requireAuth, getBranches);
router.post("/api/v1/branches", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createBranch);
router.put("/api/v1/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateBranch);
router.delete("/api/v1/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteBranch);
// List corporate departments and organizational chart reporting lines.
router.get("/api/v1/departments", requireAuth, getDepartments);
router.post("/api/v1/departments", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createDepartment);
router.put("/api/v1/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateDepartment);
router.delete("/api/v1/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteDepartment);
// List company designations and employment titles.
router.get("/api/v1/jobs", requireAuth, getJobs);
router.post("/api/v1/jobs", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createJob);
router.put("/api/v1/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateJob);
router.delete("/api/v1/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteJob);
// ============================================================================
// SYSTEM & MIGRATION DOMAIN
// High-risk administrative endpoints for bulk data execution.
// ============================================================================
// Process raw bulk data to seed initial employee/partner structures into the DB.
router.post("/api/v1/system/seed-employees", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkSeedEmployees);
// Execute mapping script to establish reporting_to_id and manager_id links.
router.post("/api/v1/system/map-hierarchy", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkMapHierarchy);
export default router;

View File

@ -1,69 +0,0 @@
import { Router } from "@oak/oak";
import {
getEmployees,
createEmployee,
getEmployeeById,
updateEmployee,
deleteEmployee
} from "./controllers/employee.controller.ts";
import {
getCompanies,
getBranches,
getDepartments,
getJobs
} from "./controllers/lookup.controller.ts";
import {
getEmployeeContracts,
createContract
} from "./controllers/contract.controller.ts";
import {
bulkSeedEmployees,
bulkMapHierarchy
} from "./controllers/system.controller.ts"
const router = new Router();
// --- Employee Core Endpoints ---
// Retrieve all employees
router.get("/api/v1/employees", getEmployees);
// Retrieve a single employee's comprehensive profile
router.get("/api/v1/employees/:id", getEmployeeById);
// Create a new employee
router.post("/api/v1/employees", createEmployee);
// Update an existing employee
router.put("/api/v1/employees/:id", updateEmployee);
// Deactivate an employee
router.delete("/api/v1/employees/:id", deleteEmployee);
// --- Contract Endpoints ---
//Retrieve contract history for an individual employee
router.get("/api/v1/employees/:id/contracts", getEmployeeContracts);
//Terminate, transition, or execute a new employment agreement.
router.post("/api/v1/contracts", createContract);
// --- Organizational Lookup Endpoints ---
// Fetch legal entities and parent group structures.
router.get("/api/v1/companies", getCompanies);
//Fetch structural branch offices
router.get("/api/v1/branches", getBranches);
//List corporate departments and organizational chart reporting lines
router.get("/api/v1/departments", getDepartments);
//List company designations and employment titles.
router.get("/api/v1/jobs", getJobs);
router.post("/api/v1/system/seed-employees", bulkSeedEmployees);
router.post("/api/v1/system/map-hierarchy", bulkMapHierarchy);
export default router;

View File

@ -15,7 +15,7 @@ CREATE TABLE branches (
branch_id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
branch_name VARCHAR(100) NOT NULL,
code VARCHAR(20) NOT NULL UNIQUE,
code VARCHAR(20) NOT NULL UNIQUE, -- e.g., 'BLR-HQ'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -51,27 +51,26 @@ CREATE TABLE addresses (
CREATE TABLE departments (
department_id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
branch_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
parent_id INT NULL,
manager_id INT NULL, -- Logical Reference to employee_id
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE,
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES departments(department_id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 6. Functional Designations
CREATE TABLE job_positions (
job_id INT AUTO_INCREMENT PRIMARY KEY,
department_id INT NOT NULL,
company_id INT NOT NULL,
title VARCHAR(50) NOT NULL,
description TEXT NULL,
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 7. Master Employee Mapping Engine
CREATE TABLE employees (
employee_id INT AUTO_INCREMENT PRIMARY KEY,
employee_code VARCHAR(20) NOT NULL UNIQUE,
employee_code VARCHAR(20) NOT NULL UNIQUE, -- Matches your Excel Base Column
partner_id INT NOT NULL,
company_id INT NOT NULL,
branch_id INT NOT NULL,
@ -88,30 +87,13 @@ CREATE TABLE contracts (
employee_id INT NOT NULL,
department_id INT NOT NULL,
job_id INT NOT NULL,
reporting_to_id INT NULL,
reporting_to_id INT NULL, -- Logical Reference to employee_id
work_email VARCHAR(100) NOT NULL UNIQUE,
date_joining DATE NOT NULL,
probation_days INT DEFAULT 90,
status ENUM('DRAFT', 'ACTIVE', 'EXPIRED', 'TERMINATED') DEFAULT 'DRAFT',
salary_structure_id INT NOT NULL,
salary_structure_id INT NOT NULL, -- Logical Reference to PMS Microservice database
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE,
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE RESTRICT,
FOREIGN KEY (job_id) REFERENCES job_positions(job_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 9. Department Managers (Junction Table)
CREATE TABLE department_managers (
department_id INT NOT NULL,
employee_id INT NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (department_id, employee_id),
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE,
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 10. System Sequences
CREATE TABLE system_sequences (
sequence_id VARCHAR(50) PRIMARY KEY,
prefix VARCHAR(10) NOT NULL,
current_value INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -1,63 +0,0 @@
CREATE DATABASE IF NOT EXISTS hrms_ams;
USE hrms_ams;
-- Grant access to the existing admin user
GRANT ALL PRIVILEGES ON hrms_ams.* TO 'admin'@'%';
FLUSH PRIVILEGES;
-- 1. Shift Schedules (Supports the Fallback Pattern)
CREATE TABLE shifts (
shift_id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL, -- Logical Reference from EMS
branch_id INT NULL, -- Fallback Pattern: NULL means applies company-wide
shift_name VARCHAR(50) NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
grace_period_minutes INT DEFAULT 15,
is_night_shift BOOLEAN DEFAULT FALSE, -- Handles overnight calculations (e.g., 22:00 to 06:00)
INDEX idx_fallback_lookup (company_id, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. Raw Biometric Scan Sink (Ingested directly from KENT Device pipeline)
CREATE TABLE attendance_raw_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
attendance_time TIMESTAMP NOT NULL,
device_id VARCHAR(50) NOT NULL,
device_name VARCHAR(100) NULL,
employee_id INT NULL, -- Logical Reference from EMS
employee_code VARCHAR(20) NOT NULL, -- Cached for quick biometric mapping checks
employee_name VARCHAR(100) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_emp_time (employee_id, attendance_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 3. Processed Attendance Summary (Populated nightly via automated Worker Engine)
CREATE TABLE processed_daily_attendance (
attendance_id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL, -- Logical Reference from EMS
company_id INT NOT NULL, -- Cached to accelerate scoped multi-branch analytics
branch_id INT NOT NULL, -- Cached to accelerate scoped multi-branch analytics
work_date DATE NOT NULL,
check_in TIMESTAMP NULL,
check_out TIMESTAMP NULL,
worked_hours DECIMAL(5,2) DEFAULT 0.00,
check_in_status ENUM('ON_TIME', 'LATE', 'MISPUNCH', 'ABSENT') NOT NULL,
final_status ENUM('FULL_DAY', 'HALF_DAY', 'ABSENT', 'MISPUNCH') NOT NULL,
UNIQUE KEY uq_emp_date (employee_id, work_date),
INDEX idx_reporting_lookup (company_id, branch_id, work_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 4. Correction Workflow Requests
CREATE TABLE attendance_regularizations (
regularization_id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL, -- Logical Reference (The Applicant)
attendance_id INT NULL, -- Internal FK to processed ledger row
regularization_type ENUM('MISPUNCH', 'WFH_REQUEST', 'ON_DUTY') NOT NULL,
target_date DATE NOT NULL,
requested_check_in TIMESTAMP NULL,
requested_check_out TIMESTAMP NULL,
reason TEXT NOT NULL,
status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
reviewed_by_id INT NOT NULL, -- Logical Reference to Manager's employee_id
FOREIGN KEY (attendance_id) REFERENCES processed_daily_attendance(attendance_id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -1,51 +0,0 @@
// shared/auth.ts
import { Context, Next } from "@oak/oak";
export enum AppRole {
SUPER_ADMIN = "SUPER_ADMIN",
ADMIN = "ADMIN",
MANAGER = "MANAGER",
EMPLOYEE = "EMPLOYEE",
}
// 1. Authentication Middleware (Who are you?)
export const requireAuth = async (ctx: Context, next: Next) => {
// 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) {
ctx.response.status = 401;
ctx.response.body = { success: false, message: "Missing or invalid token" };
return;
}
// Injecting a mock user state so the next middleware can read it
ctx.state.user = {
employee_id: 135, // Example ID
role: AppRole.SUPER_ADMIN, // Change this to test different access levels
branch_id: 1
};
await next();
};
// 2. Authorization Middleware (What are you allowed to do?)
export const requireRole = (allowedRoles: AppRole[]) => {
return async (ctx: Context, next: Next) => {
const userRole = ctx.state.user?.role;
if (!userRole || !allowedRoles.includes(userRole)) {
ctx.response.status = 403;
ctx.response.body = {
success: false,
message: "Access Denied: You do not have the required permissions."
};
return;
}
await next();
};
};

View File

@ -1,23 +1,16 @@
import mysql from "mysql2/promise";
const dbConfig = {
// Create a connection pool using the credentials from docker-compose.yml
const pool = mysql.createPool({
host: "127.0.0.1",
port: 3306,
user: "admin",
password: "admin123",
database: "hrms_ems",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
};
const pools: Record<string, mysql.Pool> = {};
export const getDbPool = (databaseName: string): mysql.Pool => {
if (!pools[databaseName]) {
pools[databaseName] = mysql.createPool({
...dbConfig,
database: databaseName,
});
}
return pools[databaseName];
};
// Export the pool to be used across your EMS, AMS, and LMS services
export default pool;

View File

@ -1,50 +0,0 @@
// shared/sequence.ts
import { getDbPool } from "./db.ts";
const pool = getDbPool("hrms_ems");
/**
* Generates a sequential code for a given entity.
* @param sequenceId Unique identifier for the counter (e.g., 'EMPLOYEE', 'BRANCH_BEN')
* @param prefix The string to prepend to the number (e.g., 'CLRI', 'BEN-')
* @param padding How many digits the number should be (e.g., 3 -> '001')
*/
export const generateNextCode = async (
sequenceId: string,
prefix: string,
padding: number = 3
): Promise<string> => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 1. Insert a new counter if it doesn't exist, OR increment the existing one atomically
await connection.execute(
`INSERT INTO system_sequences (sequence_id, prefix, current_value)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE current_value = current_value + 1, prefix = ?`,
[sequenceId, prefix, prefix]
);
// 2. Safely retrieve the updated value
const [rows] = await connection.execute(
`SELECT prefix, current_value FROM system_sequences WHERE sequence_id = ?`,
[sequenceId]
);
await connection.commit();
const data = (rows as any[])[0];
// 3. Format the result (e.g., prefix "CLRI" + value 1 + padding 3 = "CLRI001")
const paddedValue = data.current_value.toString().padStart(padding, '0');
return `${data.prefix}${paddedValue}`;
} catch (error) {
await connection.rollback();
throw new Error(`Failed to generate sequence for ${sequenceId}: ${(error as Error).message}`);
} finally {
connection.release();
}
};