import { unwrapAndClone } from "src/utils/unwrap-and-clone"; import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants"; import { GenericApiError } from "src/modules/timesheets/models/expense.validation"; import { useShiftStore } from "src/stores/shift-store"; import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models"; import { deepEqual } from "src/utils/deep-equal"; export const useShiftApi = () => { const shift_store = useShiftStore(); const normalizeShiftPayload = (shift: Shift): Shift => { const comment = shift.comment?.trim() || undefined; return { date: shift.date, start_time: shift.start_time, end_time: shift.end_time, type: shift.type, is_approved: false, is_remote: shift.is_remote, comment: comment, }; }; const parseHHMM = (s: string): [number, number] => { const m = /^(\d{2}):(\d{2})$/.exec(s); if (!m) { throw new GenericApiError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.` }); } const h = Number(m[1]); const min = Number(m[2]); if (Number.isNaN(h) || Number.isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) { throw new GenericApiError({ status_code: 400, message: `Invalid time value: ${s}.` }) } return [h, min]; }; const toMinutes = (hhmm: string): number => { const [h, m] = parseHHMM(hhmm); return h * 60 + m; }; const validateShift = (shift: Shift, label: 'old_shift' | 'new_shift') => { if (!TIME_FORMAT_PATTERN.test(shift.start_time) || !TIME_FORMAT_PATTERN.test(shift.end_time)) { throw new GenericApiError({ status_code: 400, message: `Invalid time format in ${label}. Expected HH:MM`, context: { [label]: shift } }); } if (toMinutes(shift.end_time) <= toMinutes(shift.start_time)) { throw new GenericApiError({ status_code: 400, message: `Invalid time range in ${label}. The End time must be after the Start time`, context: { [label]: shift } }); } }; const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string): Promise => { const flat_upsert_shift: UpsertShift = { ...(deepEqual(shift_store.initial_shift, default_shift) ? { old_shift: unwrapAndClone(shift_store.initial_shift) } : {}), ...(deepEqual(shift_store.current_shift, default_shift) ? { new_shift: unwrapAndClone(shift_store.current_shift) } : {}), }; const normalized_upsert_shift: UpsertShift = { ...(flat_upsert_shift.old_shift ? { old_shift: normalizeShiftPayload(flat_upsert_shift.old_shift) } : {}), ...(flat_upsert_shift.new_shift ? { new_shift: normalizeShiftPayload(flat_upsert_shift.new_shift) } : {}), }; if (normalized_upsert_shift.old_shift) validateShift(normalized_upsert_shift.old_shift, 'old_shift'); if (normalized_upsert_shift.new_shift) validateShift(normalized_upsert_shift.new_shift, 'new_shift'); await shift_store.upsertOrDeleteShiftByEmployeeEmail(employee_email, normalized_upsert_shift); }; return { upsertOrDeleteShiftByEmployeeEmail, }; }