86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
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<void> => {
|
|
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,
|
|
};
|
|
}
|
|
|