171 lines
5.4 KiB
TypeScript
171 lines
5.4 KiB
TypeScript
import { api } from "src/boot/axios";
|
|
import { isProxy, toRaw } from "vue";
|
|
/* eslint-disable */
|
|
export interface ShiftPayload {
|
|
start_time: string;
|
|
end_time: string;
|
|
type: string;
|
|
is_remote: boolean;
|
|
comment?: string;
|
|
}
|
|
|
|
export interface UpsertShiftsBody {
|
|
old_shift?: ShiftPayload;
|
|
new_shift?: ShiftPayload;
|
|
}
|
|
|
|
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
|
|
|
export interface DayShift {
|
|
start_time: string;
|
|
end_time: string;
|
|
type: string;
|
|
is_remote: boolean;
|
|
comment?: string | null;
|
|
}
|
|
|
|
export interface UpsertShiftsResponse {
|
|
action: UpsertAction;
|
|
day: DayShift[];
|
|
}
|
|
|
|
export const TIME_FORMAT_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
export const COMMENT_MAX_LENGTH = 512 as const;
|
|
|
|
//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_type = (input: string): string => (input ?? '').trim().toUpperCase();
|
|
|
|
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: normalize_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 upsert_shifts_by_date = 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);
|
|
}
|
|
}; |