Compare commits
No commits in common. "ceb8c97531d7320046ae5253fb63022e7a8b289d" and "3d1e24d641e536260d937691a8fe3783771d8720" have entirely different histories.
ceb8c97531
...
3d1e24d641
@ -25,123 +25,115 @@ export const getRawLogs = async (ctx: Context) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to generate an array of dates between two boundaries
|
/**
|
||||||
const getDaysArray = (start: string, end: string): string[] => {
|
* Timezone-safe Daily Processing Engine
|
||||||
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) => {
|
export const processDailyAttendance = async (ctx: Context) => {
|
||||||
|
try {
|
||||||
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
|
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
|
||||||
let datesToProcess: string[] = [];
|
const { work_date } = body;
|
||||||
|
|
||||||
// 1. Determine date execution scope
|
if (!work_date) {
|
||||||
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.status = 400;
|
||||||
ctx.response.body = { success: false, message: "No dates found to process." };
|
ctx.response.body = { success: false, message: "work_date (YYYY-MM-DD) is required." };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const amsConnection = await amsDb.getConnection();
|
// 1. Fetch raw logs as strings directly from the DB to preserve device-local times
|
||||||
let totalProcessedRecords = 0;
|
const [rawLogs]: any = await amsDb.execute(
|
||||||
|
`SELECT employee_code, DATE_FORMAT(attendance_time, '%Y-%m-%d %H:%i:%s') as attendance_time
|
||||||
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
|
FROM attendance_raw_logs
|
||||||
WHERE DATE(attendance_time) = ?
|
WHERE DATE(attendance_time) = ?
|
||||||
ORDER BY employee_code, attendance_time ASC`,
|
ORDER BY employee_code, attendance_time ASC`,
|
||||||
[targetDate]
|
[work_date]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rawLogs.length === 0) continue;
|
if (rawLogs.length === 0) {
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: `No raw logs found for ${work_date}.` };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Group raw logs by employee_code for this specific day
|
// Group logs by employee_code as local string arrays
|
||||||
const groupedLogs = rawLogs.reduce((acc: any, log: any) => {
|
const groupedLogs = rawLogs.reduce((acc: any, log: any) => {
|
||||||
if (!acc[log.employee_code]) acc[log.employee_code] = [];
|
if (!acc[log.employee_code]) acc[log.employee_code] = [];
|
||||||
acc[log.employee_code].push(log.att_time);
|
acc[log.employee_code].push(log.attendance_time);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// 3. Process calculations per employee for this targetDate
|
let processedCount = 0;
|
||||||
for (const empCode in groupedLogs) {
|
|
||||||
// Look up employee metadata from EMS
|
// 2. Process logs for each unique employee code
|
||||||
const [empRows]: any = await emsDb.execute(
|
for (const empCode of Object.keys(groupedLogs)) {
|
||||||
`SELECT employee_id, is_active FROM employees WHERE employee_code = ?`,
|
const punches: string[] = groupedLogs[empCode];
|
||||||
|
|
||||||
|
// Map external biometric string code to internal EMS corporate entities
|
||||||
|
const [empMetaData]: any = await emsDb.execute(
|
||||||
|
`SELECT employee_id, company_id, branch_id
|
||||||
|
FROM employees
|
||||||
|
WHERE employee_code = ? AND is_active = TRUE LIMIT 1`,
|
||||||
[empCode]
|
[empCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (empRows.length === 0 || !empRows[0].is_active) {
|
if (empMetaData.length === 0) {
|
||||||
console.log(`[AMS Worker] Skipping unmapped or inactive code: ${empCode}`);
|
console.warn(`[AMS Worker] Skipping unmapped or inactive code: ${empCode}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const employeeId = empRows[0].employee_id;
|
const { employee_id, company_id, branch_id } = empMetaData[0];
|
||||||
const punches = groupedLogs[empCode];
|
|
||||||
|
|
||||||
// Fetch shift details (Defaulting to shift_id 1 for evaluation)
|
// Fetch operational shift profile rules using the Fallback pattern
|
||||||
const [shiftRows]: any = await amsConnection.execute(
|
const [shiftData]: any = await amsDb.execute(
|
||||||
`SELECT start_time, grace_period_minutes FROM shifts WHERE shift_id = 1`
|
`SELECT start_time, end_time, grace_period_minutes, is_night_shift
|
||||||
|
FROM shifts
|
||||||
|
WHERE company_id = ? AND (branch_id = ? OR branch_id IS NULL)
|
||||||
|
ORDER BY branch_id DESC LIMIT 1`,
|
||||||
|
[company_id, branch_id]
|
||||||
);
|
);
|
||||||
const shift = shiftRows[0] || { start_time: "10:00:00", grace_period_minutes: 10 };
|
|
||||||
|
|
||||||
let check_in = null;
|
const shift = shiftData[0] || { start_time: "09:30:00", grace_period_minutes: 15, is_night_shift: false };
|
||||||
let check_out = null;
|
|
||||||
let worked_hours = 0.0;
|
let check_in: string | null = null;
|
||||||
let check_in_status = "ON_TIME";
|
let check_out: string | null = null;
|
||||||
|
let worked_hours = 0.00;
|
||||||
|
let check_in_status = "ABSENT";
|
||||||
let final_status = "ABSENT";
|
let final_status = "ABSENT";
|
||||||
|
|
||||||
if (punches.length >= 2) {
|
if (punches.length >= 2) {
|
||||||
check_in = punches[0];
|
check_in = punches[0];
|
||||||
check_out = punches[punches.length - 1];
|
check_out = punches[punches.length - 1];
|
||||||
|
|
||||||
|
// Safely parse internal local duration metrics via string substitution
|
||||||
const firstPunchMs = new Date(check_in.replace(' ', 'T')).getTime();
|
const firstPunchMs = new Date(check_in.replace(' ', 'T')).getTime();
|
||||||
const lastPunchMs = new Date(check_out.replace(' ', 'T')).getTime();
|
const lastPunchMs = new Date(check_out.replace(' ', 'T')).getTime();
|
||||||
worked_hours = Math.round(((lastPunchMs - firstPunchMs) / (1000 * 60 * 60)) * 100) / 100;
|
const diffMs = lastPunchMs - firstPunchMs;
|
||||||
|
worked_hours = Math.round((diffMs / (1000 * 60 * 60)) * 100) / 100;
|
||||||
|
|
||||||
|
// Perform time calculations purely in total seconds to bypass UTC offset traps
|
||||||
const rawCheckInTimeStr = check_in.split(' ')[1];
|
const rawCheckInTimeStr = check_in.split(' ')[1];
|
||||||
const [punchH, punchM, punchS] = rawCheckInTimeStr.split(':').map(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);
|
||||||
|
|
||||||
if ((punchH * 3600 + punchM * 60 + punchS) > (shiftH * 3600 + shiftM * 60 + shift.grace_period_minutes * 60)) {
|
const punchSeconds = (punchH * 3600) + (punchM * 60) + punchS;
|
||||||
|
const shiftSeconds = (shiftH * 3600) + (shiftM * 60) + shiftS;
|
||||||
|
const graceSeconds = shift.grace_period_minutes * 60;
|
||||||
|
|
||||||
|
// Determine Punctuality Status for the Check-In ledger column
|
||||||
|
if (punchSeconds > (shiftSeconds + graceSeconds)) {
|
||||||
check_in_status = "LATE";
|
check_in_status = "LATE";
|
||||||
|
} else {
|
||||||
|
check_in_status = "ON_TIME";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (worked_hours >= 7.0) final_status = "FULL_DAY";
|
// Apply corporate duration rules
|
||||||
else if (worked_hours >= 4.0) final_status = "HALF_DAY";
|
// Evaluate Shift Duration Rules securely within ENUM compliance bounds
|
||||||
else final_status = "ABSENT";
|
if (worked_hours >= 7.0) {
|
||||||
|
final_status = "FULL_DAY"; // Safely maps to the allowed database ENUM value
|
||||||
|
} else if (worked_hours >= 4.0 && worked_hours < 7.0) {
|
||||||
|
final_status = "HALF_DAY";
|
||||||
|
} else {
|
||||||
|
final_status = "ABSENT";
|
||||||
|
}
|
||||||
|
|
||||||
} else if (punches.length === 1) {
|
} else if (punches.length === 1) {
|
||||||
check_in = punches[0];
|
check_in = punches[0];
|
||||||
@ -149,37 +141,31 @@ export const processDailyAttendance = async (ctx: Context) => {
|
|||||||
final_status = "MISPUNCH";
|
final_status = "MISPUNCH";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Upsert calculations atomically into the processed daily ledger
|
// 3. Operational Upsert into the Processed Attendance Summary ledger
|
||||||
await amsConnection.execute(
|
await amsDb.execute(
|
||||||
`INSERT INTO processed_daily_attendance
|
`INSERT INTO processed_daily_attendance
|
||||||
(employee_id, company_id, branch_id, work_date, check_in, check_out, worked_hours, check_in_status, final_status)
|
(employee_id, company_id, branch_id, work_date, check_in, check_out, worked_hours, check_in_status, final_status)
|
||||||
VALUES (?, 1, 1, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
check_in = VALUES(check_in),
|
check_in = VALUES(check_in),
|
||||||
check_out = VALUES(check_out),
|
check_out = VALUES(check_out),
|
||||||
worked_hours = VALUES(worked_hours),
|
worked_hours = VALUES(worked_hours),
|
||||||
check_in_status = VALUES(check_in_status),
|
check_in_status = VALUES(check_in_status),
|
||||||
final_status = VALUES(final_status)`,
|
final_status = VALUES(final_status)`,
|
||||||
[employeeId, targetDate, check_in, check_out, worked_hours, check_in_status, final_status]
|
[employee_id, company_id, branch_id, work_date, check_in, check_out, worked_hours, check_in_status, final_status]
|
||||||
);
|
);
|
||||||
|
|
||||||
totalProcessedRecords++;
|
processedCount++;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await amsConnection.commit();
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully processed ${totalProcessedRecords} logs across ${datesToProcess.length} days.`
|
message: `Successfully processed attendance for ${processedCount} employees on ${work_date}.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await amsConnection.rollback();
|
console.error("Processing Engine Error:", error);
|
||||||
console.error("Bulk processing failed:", error);
|
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
ctx.response.body = { success: false, message: "Internal Server Processing Error." };
|
||||||
} finally {
|
|
||||||
amsConnection.release();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { Router } from "@oak/oak";
|
import { Router } from "@oak/oak";
|
||||||
import { getRawLogs, processDailyAttendance } from "./controllers/attendance.controller.ts";
|
import { getRawLogs, processDailyAttendance } from "./controllers/attendance.controller.ts";
|
||||||
import {createRegularizationRequest, reviewRegularizationRequest} from "./controllers/regularization.controller.ts"
|
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
const apiV1 = new Router();
|
const apiV1 = new Router();
|
||||||
@ -8,9 +7,6 @@ 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);
|
||||||
|
|
||||||
apiV1.post("/attendance/regularize", createRegularizationRequest);
|
|
||||||
apiV1.post("/attendance/regularize/review", reviewRegularizationRequest);
|
|
||||||
|
|
||||||
router.use("/api/v1", apiV1.routes(), apiV1.allowedMethods());
|
router.use("/api/v1", apiV1.routes(), apiV1.allowedMethods());
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
Loading…
x
Reference in New Issue
Block a user