targo-frontend/src/modules/timesheets/composables/api/use-shift-api.ts

141 lines
4.9 KiB
TypeScript

import { api } from "src/boot/axios";
import { isProxy, toRaw } from "vue";
import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "../../constants/shift.constants";
import type { ShiftPayload } from "../../types/shift.types";
import type { UpsertShiftsBody, UpsertShiftsResponse } from "../../types/shift.interfaces";
/* eslint-disable */
//normalize payload to match backend data
export const normalize_comment = (input?: string): string | undefined => {
if ( typeof input === 'undefined' || input === null) return undefined;
const trimmed = String(input).trim();
return trimmed.length ? trimmed : undefined;
}
export const normalize_payload = (payload: ShiftPayload): ShiftPayload => {
const comment = normalize_comment(payload.comment);
return {
start_time: payload.start_time,
end_time: payload.end_time,
type: payload.type,
is_remote: Boolean(payload.is_remote),
...(comment !== undefined ? { comment } : {}),
};
};
const toPlain = <T extends object>(obj: T): T => {
const raw = isProxy(obj) ? toRaw(obj): obj;
if(typeof (globalThis as any).structuredClone === 'function') {
return (globalThis as any).structuredClone(raw);
}
return JSON.parse(JSON.stringify(raw));
}
//error handling
export interface ApiErrorPayload {
status_code: number;
error_code?: string;
message?: string;
context?: Record<string, unknown>;
}
export class UpsertShiftsError extends Error {
status_code: number;
error_code?: string | undefined;
context?: Record<string, unknown> | undefined;
constructor(payload: ApiErrorPayload) {
super(payload.message || 'Request failed');
this.name = 'UpsertShiftsError';
this.status_code = payload.status_code;
this.error_code = payload.error_code;
this.context = payload.context;
}
}
const parseHHMM = (s:string): [number, number] => {
const m = /^(\d{2}):(\d{2})$/.exec(s);
if(!m) {
throw new UpsertShiftsError({ 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 UpsertShiftsError({ 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 = (payload: ShiftPayload, label: 'old_shift'|'new_shift') => {
if(!TIME_FORMAT_PATTERN.test(payload.start_time) || !TIME_FORMAT_PATTERN.test(payload.end_time)) {
throw new UpsertShiftsError({
status_code: 400,
message: `Invalid time format in ${label}. Expected HH:MM`,
context: { [label]: payload }
});
}
if(toMinutes(payload.end_time) <= toMinutes(payload.start_time)) {
throw new UpsertShiftsError({
status_code: 400,
message: `Invalid time range in ${label}. The End time must be after the Start time`,
context: { [label]: payload}
});
}
}
export const upsertShiftsByDate = async (
email: string,
date: string,
body: UpsertShiftsBody,
): Promise<UpsertShiftsResponse> => {
if (!DATE_FORMAT_PATTERN.test(date)){
throw new UpsertShiftsError({
status_code: 400,
message: 'Invalid date format, expected YYYY-MM-DD',
});
}
const flatBody: UpsertShiftsBody = {
...(body.old_shift ? { old_shift: toPlain(body.old_shift) }: {}),
...(body.new_shift ? { new_shift: toPlain(body.new_shift) }: {}),
};
const normalized: UpsertShiftsBody = {
...(flatBody.old_shift ? { old_shift: normalize_payload(flatBody.old_shift) } : {}),
...(flatBody.new_shift ? { new_shift: normalize_payload(flatBody.new_shift) } : {}),
};
if(normalized.old_shift) validateShift(normalized.old_shift, 'old_shift');
if(normalized.new_shift) validateShift(normalized.new_shift, 'new_shift');
const encoded_email = encodeURIComponent(email);
const encoded_date = encodeURIComponent(date);
//error handling to be used with notify in case of bad input
try {
const { data } = await api.put<UpsertShiftsResponse>(
`/shifts/upsert/${encoded_email}/${encoded_date}`,
normalized,
{ headers: {'content-type': 'application/json'}}
);
return data;
} catch (err: any) {
const status_code: number = err?.response?.status ?? 500;
const data = err?.response?.data ?? {};
const payload: ApiErrorPayload = {
status_code,
error_code: data.error_code,
message: data.message || data.error || err.message,
context: data.context,
};
throw new UpsertShiftsError(payload);
}
};