Compare commits
No commits in common. "131cf6860089acfa2ba041973f157cb819fc0001" and "7d1b7609345728eb0a30382037430b0cbea9d0bd" have entirely different histories.
131cf68600
...
7d1b760934
36
deno.lock
generated
36
deno.lock
generated
@ -1247,42 +1247,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,4 +1,3 @@
|
|||||||
// @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,4 +1,3 @@
|
|||||||
// @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,4 +1,3 @@
|
|||||||
// @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,15 +10,4 @@ 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,30 +1,10 @@
|
|||||||
// 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,
|
||||||
@ -53,10 +33,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: formatDate(reg.target_date),
|
targetDate: reg.target_date || "",
|
||||||
regularizationType: reg.regularization_type || "",
|
regularizationType: reg.regularization_type || "",
|
||||||
requestedCheckIn: formatDateTime(reg.requested_check_in) || undefined,
|
requestedCheckIn: reg.requested_check_in || undefined,
|
||||||
requestedCheckOut: formatDateTime(reg.requested_check_out) || undefined,
|
requestedCheckOut: 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,
|
||||||
@ -90,17 +70,16 @@ 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: formatDate(reg.target_date),
|
targetDate: reg.target_date ? new Date(reg.target_date).toISOString() : "",
|
||||||
regularizationType: reg.regularization_type || "",
|
regularizationType: reg.regularization_type || "",
|
||||||
requestedCheckIn: formatDateTime(reg.requested_check_in) || "",
|
requestedCheckIn: reg.requested_check_in ? new Date(reg.requested_check_in).toISOString() : "",
|
||||||
requestedCheckOut: formatDateTime(reg.requested_check_out) || "",
|
requestedCheckOut: reg.requested_check_out ? new Date(reg.requested_check_out).toISOString() : "",
|
||||||
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,14 +21,12 @@ export const findPendingRegularizations = async (branchIds: number[] | null) =>
|
|||||||
}
|
}
|
||||||
query += ` ORDER BY target_date DESC`;
|
query += ` ORDER BY target_date DESC`;
|
||||||
|
|
||||||
// FIX: Cast result as any[]
|
const [rows] = await pool.query(query, params);
|
||||||
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) => {
|
||||||
// FIX: Cast result as any[]
|
const [rows]: any = await conn.execute(
|
||||||
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]
|
||||||
@ -44,8 +42,7 @@ export const updateRegularizationStatus = async (id: number, status: string, rev
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const findAllRegularizations = async () => {
|
export const findAllRegularizations = async () => {
|
||||||
// FIX: Cast result as any[]
|
const [rows] = await pool.execute(
|
||||||
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
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
// 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,62 +1,21 @@
|
|||||||
// 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 __deps.insertRegularization({ ...data, branch_id });
|
await RegRepo.insertRegularization({ ...data, branch_id });
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,11 +26,11 @@ export const getPendingRegularizations = async (user: any) => {
|
|||||||
if (!branchIds || branchIds.length === 0) return [];
|
if (!branchIds || branchIds.length === 0) return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await __deps.findPendingRegularizations(branchIds);
|
const rows = await RegRepo.findPendingRegularizations(branchIds);
|
||||||
if (rows.length === 0) return [];
|
if (rows.length === 0) return [];
|
||||||
|
|
||||||
const employeeIds = [...new Set(rows.map((r: any) => Number(r.employee_id)))] as number[];
|
const employeeIds = [...new Set(rows.map((r: any) => r.employee_id))];
|
||||||
const roster = await __deps.getEmployeeRoster(employeeIds);
|
const roster = await getEmployeeRoster(employeeIds);
|
||||||
|
|
||||||
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
|
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
|
||||||
|
|
||||||
@ -88,16 +47,17 @@ export const getPendingRegularizations = async (user: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getAllRegularizations = async () => {
|
export const getAllRegularizations = async () => {
|
||||||
const rows = await __deps.findAllRegularizations();
|
const rows = await RegRepo.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 __deps.getEmployeeRoster(Array.from(allIds));
|
const roster = await 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) => {
|
||||||
@ -110,6 +70,7 @@ 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"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -123,10 +84,11 @@ 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 __deps.findRegularizationById(data.regularization_id, connection);
|
const request = await RegRepo.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.");
|
||||||
@ -138,24 +100,29 @@ 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.");
|
||||||
|
|
||||||
// FIX: Use __deps
|
await RegRepo.updateRegularizationStatus(data.regularization_id, data.action, user.employee_id, connection);
|
||||||
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;
|
||||||
const rawCheckIn = formatDateTime(request.requested_check_in);
|
let rawCheckOut: string | undefined;
|
||||||
const rawCheckOut = formatDateTime(request.requested_check_out);
|
|
||||||
|
|
||||||
// FIX: Use __deps
|
const roster = await getEmployeeRoster([request.employee_id]);
|
||||||
const roster = await __deps.getEmployeeRoster([request.employee_id]);
|
|
||||||
const emp = roster[0];
|
const emp = roster[0];
|
||||||
|
|
||||||
if (emp) {
|
if (emp) {
|
||||||
// FIX: Use __deps
|
const shift = await AttendanceRepo.findShiftDetails(emp.branchId, emp.companyId, connection);
|
||||||
const shift = await __deps.findShiftDetails(emp.branchId, emp.companyId, connection);
|
|
||||||
|
|
||||||
if (rawCheckIn && rawCheckOut) {
|
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;
|
||||||
|
|
||||||
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;
|
||||||
@ -165,6 +132,7 @@ 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);
|
||||||
@ -172,28 +140,21 @@ 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) check_in_status = "LATE";
|
if (punchTotalSeconds > shiftCutoffSeconds) {
|
||||||
|
check_in_status = "LATE";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let attendanceIdToUpdate = request.attendance_id;
|
// Use the safe string variables for the DB update
|
||||||
if (!attendanceIdToUpdate) {
|
if (request.attendance_id && rawCheckIn && rawCheckOut) {
|
||||||
const [attRows]: any[] = await connection.execute(
|
await AttendanceRepo.updateProcessedAttendanceById(request.attendance_id, {
|
||||||
`SELECT attendance_id FROM processed_daily_attendance WHERE employee_id = ? AND work_date = ?`,
|
requested_check_in: rawCheckIn,
|
||||||
[request.employee_id, request.target_date]
|
requested_check_out: rawCheckOut,
|
||||||
);
|
worked_hours,
|
||||||
if (attRows.length > 0) attendanceIdToUpdate = attRows[0].attendance_id;
|
final_status,
|
||||||
}
|
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