Compare commits
2 Commits
7d1b760934
...
131cf68600
| Author | SHA1 | Date | |
|---|---|---|---|
| 131cf68600 | |||
| ab1f9444f9 |
36
deno.lock
generated
36
deno.lock
generated
@ -1247,6 +1247,42 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||||
|
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||||
|
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||||
|
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||||
|
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||||
|
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||||
|
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||||
|
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||||
|
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||||
|
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||||
|
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
|
||||||
|
"https://deno.land/std@0.224.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db"
|
||||||
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@oak/oak@^17.1.3",
|
"jsr:@oak/oak@^17.1.3",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-ts_proto v2.12.0
|
// protoc-gen-ts_proto v2.12.0
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-ts_proto v2.12.0
|
// protoc-gen-ts_proto v2.12.0
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-ts_proto v2.12.0
|
// protoc-gen-ts_proto v2.12.0
|
||||||
|
|||||||
@ -10,4 +10,15 @@ podman exec hrms_proto_compiler sh -c "
|
|||||||
-I ./protos \
|
-I ./protos \
|
||||||
./protos/*.proto
|
./protos/*.proto
|
||||||
"
|
"
|
||||||
|
|
||||||
|
# FIX: Prepend // @ts-nocheck to all generated files to prevent Deno strict type-checking errors
|
||||||
|
for file in generated/*.ts; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
# Check if the first line is not already @ts-nocheck
|
||||||
|
if ! head -n 1 "$file" | grep -q "@ts-nocheck"; then
|
||||||
|
echo "// @ts-nocheck" | cat - "$file" > temp && mv temp "$file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo "âś… gRPC code generation complete!"
|
echo "âś… gRPC code generation complete!"
|
||||||
@ -1,10 +1,30 @@
|
|||||||
// services/ams/handlers/regularization.handler.ts
|
// services/ams/handlers/regularization.handler.ts
|
||||||
import * as RegService from "../services/regularization.service.ts";
|
import * as RegService from "../services/regularization.service.ts";
|
||||||
|
|
||||||
|
// FIX: Timezone safe date formatters
|
||||||
|
const formatDateTime = (dateVal: any): string => {
|
||||||
|
if (!dateVal) return "";
|
||||||
|
if (dateVal instanceof Date) {
|
||||||
|
const offset = dateVal.getTimezoneOffset() * 60000;
|
||||||
|
const localDate = new Date(dateVal.getTime() - offset);
|
||||||
|
return localDate.toISOString().replace("T", " ").substring(0, 19);
|
||||||
|
}
|
||||||
|
return String(dateVal).replace("T", " ").substring(0, 19);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateVal: any): string => {
|
||||||
|
if (!dateVal) return "";
|
||||||
|
if (dateVal instanceof Date) {
|
||||||
|
const offset = dateVal.getTimezoneOffset() * 60000;
|
||||||
|
const localDate = new Date(dateVal.getTime() - offset);
|
||||||
|
return localDate.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
return String(dateVal).split("T")[0];
|
||||||
|
};
|
||||||
|
|
||||||
export const RegularizationServiceImplementation = {
|
export const RegularizationServiceImplementation = {
|
||||||
async createRegularizationRequest(req: any) {
|
async createRegularizationRequest(req: any) {
|
||||||
try {
|
try {
|
||||||
// Map camelCase gRPC payload back to snake_case for the DB service layer
|
|
||||||
const dbPayload = {
|
const dbPayload = {
|
||||||
attendance_id: req.attendanceId || null,
|
attendance_id: req.attendanceId || null,
|
||||||
employee_id: req.employeeId,
|
employee_id: req.employeeId,
|
||||||
@ -33,10 +53,10 @@ export const RegularizationServiceImplementation = {
|
|||||||
const mappedData = data.map((reg: any) => ({
|
const mappedData = data.map((reg: any) => ({
|
||||||
regularizationId: Number(reg.regularization_id) || 0,
|
regularizationId: Number(reg.regularization_id) || 0,
|
||||||
employeeId: Number(reg.employee_id) || 0,
|
employeeId: Number(reg.employee_id) || 0,
|
||||||
targetDate: reg.target_date || "",
|
targetDate: formatDate(reg.target_date),
|
||||||
regularizationType: reg.regularization_type || "",
|
regularizationType: reg.regularization_type || "",
|
||||||
requestedCheckIn: reg.requested_check_in || undefined,
|
requestedCheckIn: formatDateTime(reg.requested_check_in) || undefined,
|
||||||
requestedCheckOut: reg.requested_check_out || undefined,
|
requestedCheckOut: formatDateTime(reg.requested_check_out) || undefined,
|
||||||
reason: reg.reason || "",
|
reason: reg.reason || "",
|
||||||
status: reg.status || "",
|
status: reg.status || "",
|
||||||
branchId: Number(reg.branch_id) || 0,
|
branchId: Number(reg.branch_id) || 0,
|
||||||
@ -70,16 +90,17 @@ export const RegularizationServiceImplementation = {
|
|||||||
return { success: false, message: error.message };
|
return { success: false, message: error.message };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAllRegularizations(req: any) {
|
async getAllRegularizations(req: any) {
|
||||||
try {
|
try {
|
||||||
const data = await RegService.getAllRegularizations();
|
const data = await RegService.getAllRegularizations();
|
||||||
const mappedData = data.map((reg: any) => ({
|
const mappedData = data.map((reg: any) => ({
|
||||||
regularizationId: Number(reg.regularization_id) || 0,
|
regularizationId: Number(reg.regularization_id) || 0,
|
||||||
employeeId: Number(reg.employee_id) || 0,
|
employeeId: Number(reg.employee_id) || 0,
|
||||||
targetDate: reg.target_date ? new Date(reg.target_date).toISOString() : "",
|
targetDate: formatDate(reg.target_date),
|
||||||
regularizationType: reg.regularization_type || "",
|
regularizationType: reg.regularization_type || "",
|
||||||
requestedCheckIn: reg.requested_check_in ? new Date(reg.requested_check_in).toISOString() : "",
|
requestedCheckIn: formatDateTime(reg.requested_check_in) || "",
|
||||||
requestedCheckOut: reg.requested_check_out ? new Date(reg.requested_check_out).toISOString() : "",
|
requestedCheckOut: formatDateTime(reg.requested_check_out) || "",
|
||||||
reason: reg.reason || "",
|
reason: reg.reason || "",
|
||||||
status: reg.status || "",
|
status: reg.status || "",
|
||||||
branchId: Number(reg.branch_id) || 0,
|
branchId: Number(reg.branch_id) || 0,
|
||||||
|
|||||||
@ -21,12 +21,14 @@ export const findPendingRegularizations = async (branchIds: number[] | null) =>
|
|||||||
}
|
}
|
||||||
query += ` ORDER BY target_date DESC`;
|
query += ` ORDER BY target_date DESC`;
|
||||||
|
|
||||||
const [rows] = await pool.query(query, params);
|
// FIX: Cast result as any[]
|
||||||
|
const [rows]: any[] = await pool.query(query, params);
|
||||||
return rows;
|
return rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findRegularizationById = async (id: number, conn: any) => {
|
export const findRegularizationById = async (id: number, conn: any) => {
|
||||||
const [rows]: any = await conn.execute(
|
// FIX: Cast result as any[]
|
||||||
|
const [rows]: any[] = await conn.execute(
|
||||||
`SELECT regularization_id, attendance_id, branch_id, employee_id, requested_check_in, requested_check_out, status
|
`SELECT regularization_id, attendance_id, branch_id, employee_id, requested_check_in, requested_check_out, status
|
||||||
FROM attendance_regularizations WHERE regularization_id = ?`,
|
FROM attendance_regularizations WHERE regularization_id = ?`,
|
||||||
[id]
|
[id]
|
||||||
@ -42,7 +44,8 @@ export const updateRegularizationStatus = async (id: number, status: string, rev
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const findAllRegularizations = async () => {
|
export const findAllRegularizations = async () => {
|
||||||
const [rows] = await pool.execute(
|
// FIX: Cast result as any[]
|
||||||
|
const [rows]: any[] = await pool.execute(
|
||||||
`SELECT regularization_id, employee_id, target_date, regularization_type,
|
`SELECT regularization_id, employee_id, target_date, regularization_type,
|
||||||
requested_check_in, requested_check_out, reason, status, branch_id, reviewed_by_id
|
requested_check_in, requested_check_out, reason, status, branch_id, reviewed_by_id
|
||||||
FROM attendance_regularizations
|
FROM attendance_regularizations
|
||||||
|
|||||||
102
services/ams/services/regularization.service.test.ts
Normal file
102
services/ams/services/regularization.service.test.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// services/ams/services/regularization.service.test.ts
|
||||||
|
import { assertEquals, assertRejects} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||||
|
import { stub } from "https://deno.land/std@0.224.0/testing/mock.ts";
|
||||||
|
import * as RegService from "./regularization.service.ts";
|
||||||
|
import { __deps } from "./regularization.service.ts";
|
||||||
|
import { formatDateTime } from "./regularization.service.ts";
|
||||||
|
|
||||||
|
|
||||||
|
Deno.test("createRegularizationRequest should throw error if target_date is a Sunday", async () => {
|
||||||
|
const payload = {
|
||||||
|
employee_id: 135,
|
||||||
|
reason: "Forgot to punch",
|
||||||
|
regularization_type: "MISPUNCH",
|
||||||
|
target_date: "2026-07-12", // This is a Sunday
|
||||||
|
};
|
||||||
|
|
||||||
|
// We expect this promise to reject with a specific error
|
||||||
|
await assertRejects(
|
||||||
|
() => RegService.createRegularizationRequest(payload),
|
||||||
|
Error,
|
||||||
|
"Cannot apply for regularization on a weekly off (Sunday)."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("createRegularizationRequest should throw error if missing required fields", async () => {
|
||||||
|
const payload = {
|
||||||
|
employee_id: 135,
|
||||||
|
// missing reason and target_date
|
||||||
|
};
|
||||||
|
|
||||||
|
await assertRejects(
|
||||||
|
() => RegService.createRegularizationRequest(payload as any),
|
||||||
|
Error,
|
||||||
|
"Missing required fields."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("createRegularizationRequest should throw error if employee is not found in EMS", async () => {
|
||||||
|
const payload = {
|
||||||
|
employee_id: 999,
|
||||||
|
reason: "Forgot to punch",
|
||||||
|
regularization_type: "MISPUNCH",
|
||||||
|
target_date: "2026-07-15",
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIX: Stub the method on the __deps object
|
||||||
|
const rosterStub = stub(__deps, "getEmployeeRoster", () => Promise.resolve([]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assertRejects(
|
||||||
|
() => RegService.createRegularizationRequest(payload),
|
||||||
|
Error,
|
||||||
|
"Employee not found in EMS."
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rosterStub.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("createRegularizationRequest should succeed and insert into DB for valid data", async () => {
|
||||||
|
const payload = {
|
||||||
|
employee_id: 135,
|
||||||
|
reason: "Forgot to punch",
|
||||||
|
regularization_type: "MISPUNCH",
|
||||||
|
target_date: "2026-07-15",
|
||||||
|
requested_check_in: "2026-07-15 10:00:00",
|
||||||
|
requested_check_out: "2026-07-15 19:00:00"
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIX: Stub the methods on the __deps object
|
||||||
|
const rosterStub = stub(__deps, "getEmployeeRoster", () =>
|
||||||
|
Promise.resolve([{ employeeId: 135, branchId: 1 } as any])
|
||||||
|
);
|
||||||
|
|
||||||
|
let insertedData: any = null;
|
||||||
|
const insertStub = stub(__deps, "insertRegularization", async (data: any) => {
|
||||||
|
insertedData = data;
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await RegService.createRegularizationRequest(payload);
|
||||||
|
|
||||||
|
assertEquals(insertedData.employee_id, 135);
|
||||||
|
assertEquals(insertedData.branch_id, 1);
|
||||||
|
assertEquals(insertedData.target_date, "2026-07-15");
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
rosterStub.restore();
|
||||||
|
insertStub.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("formatDateTime should not shift the date forward due to timezone", () => {
|
||||||
|
// Simulate MySQL returning a Date object for "2026-07-12 18:30:00" local time
|
||||||
|
const mysqlDateObject = new Date(2026, 6, 12, 18, 30, 0); // Month is 0-indexed (6 = July)
|
||||||
|
|
||||||
|
const formattedString = formatDateTime(mysqlDateObject);
|
||||||
|
|
||||||
|
// Assert it stays on the 12th, doesn't shift to the 13th
|
||||||
|
assertEquals(formattedString?.startsWith("2026-07-12"), true);
|
||||||
|
});
|
||||||
@ -1,21 +1,62 @@
|
|||||||
// services/ams/services/regularization.service.ts
|
// services/ams/services/regularization.service.ts
|
||||||
import pool from "../db.ts";
|
import pool from "../db.ts";
|
||||||
import { AppRole } from "core/auth.ts";
|
import { AppRole } from "core/auth.ts";
|
||||||
import { getEmployeeRoster } from "core/internal-client.ts";
|
//import { getEmployeeRoster } from "core/internal-client.ts";
|
||||||
|
import * as InternalClient from "core/internal-client.ts"; // CHANGED TO NAMESPACE
|
||||||
import * as RegRepo from "../repositories/regularization.repository.ts";
|
import * as RegRepo from "../repositories/regularization.repository.ts";
|
||||||
import * as AttendanceRepo from "../repositories/attendance.repository.ts";
|
import * as AttendanceRepo from "../repositories/attendance.repository.ts";
|
||||||
|
|
||||||
|
// FIX: Dependency Injection Object for test mocking
|
||||||
|
export const __deps = {
|
||||||
|
getEmployeeRoster: InternalClient.getEmployeeRoster,
|
||||||
|
insertRegularization: RegRepo.insertRegularization,
|
||||||
|
findPendingRegularizations: RegRepo.findPendingRegularizations,
|
||||||
|
findAllRegularizations: RegRepo.findAllRegularizations,
|
||||||
|
findRegularizationById: RegRepo.findRegularizationById,
|
||||||
|
updateRegularizationStatus: RegRepo.updateRegularizationStatus,
|
||||||
|
findShiftDetails: AttendanceRepo.findShiftDetails,
|
||||||
|
updateProcessedAttendanceById: AttendanceRepo.updateProcessedAttendanceById,
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIX: Timezone safe formatter for recalculating attendance
|
||||||
|
export const formatDateTime = (dateVal: any): string | undefined => {
|
||||||
|
if (!dateVal) return undefined;
|
||||||
|
if (dateVal instanceof Date) {
|
||||||
|
const offset = dateVal.getTimezoneOffset() * 60000;
|
||||||
|
const localDate = new Date(dateVal.getTime() - offset);
|
||||||
|
return localDate.toISOString().replace("T", " ").substring(0, 19);
|
||||||
|
}
|
||||||
|
return String(dateVal).replace("T", " ").substring(0, 19);
|
||||||
|
};
|
||||||
|
|
||||||
export const createRegularizationRequest = async (data: any) => {
|
export const createRegularizationRequest = async (data: any) => {
|
||||||
if (!data.employee_id || !data.reason || !data.regularization_type || !data.target_date) {
|
if (!data.employee_id || !data.reason || !data.regularization_type || !data.target_date) {
|
||||||
throw new Error("Missing required fields.");
|
throw new Error("Missing required fields.");
|
||||||
}
|
}
|
||||||
const roster = await getEmployeeRoster([data.employee_id]);
|
|
||||||
|
// FIX: Weekend validation check
|
||||||
|
const targetDateObj = new Date(data.target_date);
|
||||||
|
const dayOfWeek = targetDateObj.getDay(); // 0 = Sunday, 6 = Saturday
|
||||||
|
|
||||||
|
// Assuming Sunday (0) is the default weekly off.
|
||||||
|
if (dayOfWeek === 0) {
|
||||||
|
throw new Error("Cannot apply for regularization on a weekly off (Sunday).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate requested check-in date matches target date
|
||||||
|
if (data.requested_check_in) {
|
||||||
|
const checkInDate = new Date(data.requested_check_in).toISOString().split('T')[0];
|
||||||
|
if (checkInDate !== data.target_date) {
|
||||||
|
throw new Error("Requested Check-In date does not match the Target Date.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roster = await __deps.getEmployeeRoster([data.employee_id]);
|
||||||
if (roster.length === 0) throw new Error("Employee not found in EMS.");
|
if (roster.length === 0) throw new Error("Employee not found in EMS.");
|
||||||
|
|
||||||
// Fallback to 0 if branchId is undefined to prevent NOT NULL constraint crash
|
|
||||||
const branch_id = roster[0].branchId || 0;
|
const branch_id = roster[0].branchId || 0;
|
||||||
|
|
||||||
await RegRepo.insertRegularization({ ...data, branch_id });
|
await __deps.insertRegularization({ ...data, branch_id });
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,11 +67,11 @@ export const getPendingRegularizations = async (user: any) => {
|
|||||||
if (!branchIds || branchIds.length === 0) return [];
|
if (!branchIds || branchIds.length === 0) return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await RegRepo.findPendingRegularizations(branchIds);
|
const rows = await __deps.findPendingRegularizations(branchIds);
|
||||||
if (rows.length === 0) return [];
|
if (rows.length === 0) return [];
|
||||||
|
|
||||||
const employeeIds = [...new Set(rows.map((r: any) => r.employee_id))];
|
const employeeIds = [...new Set(rows.map((r: any) => Number(r.employee_id)))] as number[];
|
||||||
const roster = await getEmployeeRoster(employeeIds);
|
const roster = await __deps.getEmployeeRoster(employeeIds);
|
||||||
|
|
||||||
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
|
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
|
||||||
|
|
||||||
@ -47,17 +88,16 @@ export const getPendingRegularizations = async (user: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getAllRegularizations = async () => {
|
export const getAllRegularizations = async () => {
|
||||||
const rows = await RegRepo.findAllRegularizations();
|
const rows = await __deps.findAllRegularizations();
|
||||||
if (rows.length === 0) return [];
|
if (rows.length === 0) return [];
|
||||||
|
|
||||||
// Collect both employee_id and reviewed_by_id to fetch their names from EMS
|
|
||||||
const allIds = new Set<number>();
|
const allIds = new Set<number>();
|
||||||
rows.forEach((r: any) => {
|
rows.forEach((r: any) => {
|
||||||
if (r.employee_id) allIds.add(Number(r.employee_id));
|
if (r.employee_id) allIds.add(Number(r.employee_id));
|
||||||
if (r.reviewed_by_id) allIds.add(Number(r.reviewed_by_id));
|
if (r.reviewed_by_id) allIds.add(Number(r.reviewed_by_id));
|
||||||
});
|
});
|
||||||
|
|
||||||
const roster = await getEmployeeRoster(Array.from(allIds));
|
const roster = await __deps.getEmployeeRoster(Array.from(allIds));
|
||||||
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
|
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
|
||||||
|
|
||||||
return rows.map((req: any) => {
|
return rows.map((req: any) => {
|
||||||
@ -70,7 +110,6 @@ export const getAllRegularizations = async () => {
|
|||||||
first_name: emp?.fullName?.split(" ")[0] || "",
|
first_name: emp?.fullName?.split(" ")[0] || "",
|
||||||
last_name: emp?.fullName?.split(" ").slice(1).join(" ") || "",
|
last_name: emp?.fullName?.split(" ").slice(1).join(" ") || "",
|
||||||
branch_name: emp?.branchName || "N/A",
|
branch_name: emp?.branchName || "N/A",
|
||||||
// FIX: EMS Roster returns 'fullName', not 'firstName'/'lastName'
|
|
||||||
reviewer_name: reviewer?.fullName || "Pending Review"
|
reviewer_name: reviewer?.fullName || "Pending Review"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -84,11 +123,10 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
|
|||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
const request = await RegRepo.findRegularizationById(data.regularization_id, connection);
|
const request = await __deps.findRegularizationById(data.regularization_id, connection);
|
||||||
|
|
||||||
if (!request) throw new Error("Regularization request not found.");
|
if (!request) throw new Error("Regularization request not found.");
|
||||||
|
|
||||||
// AUTHORIZATION LOGIC: Allow Directors globally, but restrict HR Managers to their branches
|
|
||||||
if (user.role !== AppRole.DIRECTOR) {
|
if (user.role !== AppRole.DIRECTOR) {
|
||||||
if (user.role !== AppRole.HR_MANAGER) {
|
if (user.role !== AppRole.HR_MANAGER) {
|
||||||
throw new Error("Access Denied: Only HR Managers and Directors can review requests.");
|
throw new Error("Access Denied: Only HR Managers and Directors can review requests.");
|
||||||
@ -100,29 +138,24 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
|
|||||||
|
|
||||||
if (request.status !== "PENDING") throw new Error("This request has already been processed.");
|
if (request.status !== "PENDING") throw new Error("This request has already been processed.");
|
||||||
|
|
||||||
await RegRepo.updateRegularizationStatus(data.regularization_id, data.action, user.employee_id, connection);
|
// FIX: Use __deps
|
||||||
|
await __deps.updateRegularizationStatus(data.regularization_id, data.action, user.employee_id, connection);
|
||||||
|
|
||||||
if (data.action === "APPROVED") {
|
if (data.action === "APPROVED") {
|
||||||
let worked_hours = 0.0, final_status = "FULL_DAY", check_in_status = "ON_TIME";
|
let worked_hours = 0.0, final_status = "FULL_DAY", check_in_status = "ON_TIME";
|
||||||
let rawCheckIn: string | undefined;
|
|
||||||
let rawCheckOut: string | undefined;
|
|
||||||
|
|
||||||
const roster = await getEmployeeRoster([request.employee_id]);
|
const rawCheckIn = formatDateTime(request.requested_check_in);
|
||||||
|
const rawCheckOut = formatDateTime(request.requested_check_out);
|
||||||
|
|
||||||
|
// FIX: Use __deps
|
||||||
|
const roster = await __deps.getEmployeeRoster([request.employee_id]);
|
||||||
const emp = roster[0];
|
const emp = roster[0];
|
||||||
|
|
||||||
if (emp) {
|
if (emp) {
|
||||||
const shift = await AttendanceRepo.findShiftDetails(emp.branchId, emp.companyId, connection);
|
// FIX: Use __deps
|
||||||
|
const shift = await __deps.findShiftDetails(emp.branchId, emp.companyId, connection);
|
||||||
if (request.requested_check_in && request.requested_check_out) {
|
|
||||||
// Safely convert Date objects to strings if necessary
|
|
||||||
rawCheckIn = request.requested_check_in instanceof Date
|
|
||||||
? request.requested_check_in.toISOString().replace("T", " ").substring(0, 19)
|
|
||||||
: request.requested_check_in;
|
|
||||||
|
|
||||||
rawCheckOut = request.requested_check_out instanceof Date
|
|
||||||
? request.requested_check_out.toISOString().replace("T", " ").substring(0, 19)
|
|
||||||
: request.requested_check_out;
|
|
||||||
|
|
||||||
|
if (rawCheckIn && rawCheckOut) {
|
||||||
const checkInMs = new Date(rawCheckIn.replace(" ", "T")).getTime();
|
const checkInMs = new Date(rawCheckIn.replace(" ", "T")).getTime();
|
||||||
const checkOutMs = new Date(rawCheckOut.replace(" ", "T")).getTime();
|
const checkOutMs = new Date(rawCheckOut.replace(" ", "T")).getTime();
|
||||||
const effectiveCheckOutMs = checkOutMs < checkInMs ? checkOutMs + (24 * 60 * 60 * 1000) : checkOutMs;
|
const effectiveCheckOutMs = checkOutMs < checkInMs ? checkOutMs + (24 * 60 * 60 * 1000) : checkOutMs;
|
||||||
@ -132,7 +165,6 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
|
|||||||
else if (worked_hours >= 4.0) final_status = "HALF_DAY";
|
else if (worked_hours >= 4.0) final_status = "HALF_DAY";
|
||||||
else final_status = "ABSENT";
|
else final_status = "ABSENT";
|
||||||
|
|
||||||
// rawCheckIn is already "YYYY-MM-DD HH:MM:SS"
|
|
||||||
const timePart = rawCheckIn.split(" ")[1];
|
const timePart = rawCheckIn.split(" ")[1];
|
||||||
const [punchH, punchM, punchS] = timePart.split(":").map(Number);
|
const [punchH, punchM, punchS] = timePart.split(":").map(Number);
|
||||||
const [shiftH, shiftM, shiftS] = shift.start_time.split(":").map(Number);
|
const [shiftH, shiftM, shiftS] = shift.start_time.split(":").map(Number);
|
||||||
@ -140,21 +172,28 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
|
|||||||
const punchTotalSeconds = punchH * 3600 + punchM * 60 + punchS;
|
const punchTotalSeconds = punchH * 3600 + punchM * 60 + punchS;
|
||||||
const shiftCutoffSeconds = shiftH * 3600 + shiftM * 60 + (shift.grace_period_minutes * 60);
|
const shiftCutoffSeconds = shiftH * 3600 + shiftM * 60 + (shift.grace_period_minutes * 60);
|
||||||
|
|
||||||
if (punchTotalSeconds > shiftCutoffSeconds) {
|
if (punchTotalSeconds > shiftCutoffSeconds) check_in_status = "LATE";
|
||||||
check_in_status = "LATE";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Use the safe string variables for the DB update
|
let attendanceIdToUpdate = request.attendance_id;
|
||||||
if (request.attendance_id && rawCheckIn && rawCheckOut) {
|
if (!attendanceIdToUpdate) {
|
||||||
await AttendanceRepo.updateProcessedAttendanceById(request.attendance_id, {
|
const [attRows]: any[] = await connection.execute(
|
||||||
requested_check_in: rawCheckIn,
|
`SELECT attendance_id FROM processed_daily_attendance WHERE employee_id = ? AND work_date = ?`,
|
||||||
requested_check_out: rawCheckOut,
|
[request.employee_id, request.target_date]
|
||||||
worked_hours,
|
);
|
||||||
final_status,
|
if (attRows.length > 0) attendanceIdToUpdate = attRows[0].attendance_id;
|
||||||
check_in_status
|
}
|
||||||
}, connection);
|
|
||||||
|
if (attendanceIdToUpdate && (rawCheckIn || rawCheckOut)) {
|
||||||
|
// FIX: Use __deps
|
||||||
|
await __deps.updateProcessedAttendanceById(attendanceIdToUpdate, {
|
||||||
|
requested_check_in: rawCheckIn,
|
||||||
|
requested_check_out: rawCheckOut,
|
||||||
|
worked_hours,
|
||||||
|
final_status,
|
||||||
|
check_in_status
|
||||||
|
}, connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user