Implement time off management (Issue #3)
Add full time-off lifecycle: create/edit/delete with shift conflict detection, auto-removal from conflicting shifts with admin notification, shift restoration on admin delete, and hard block on assigning volunteers with approved time off to shifts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,16 @@ function getToken(): string | null {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
class ApiError extends Error {
|
||||
status: number;
|
||||
data: any;
|
||||
constructor(message: string, status: number, data: any) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
const token = getToken();
|
||||
@@ -17,10 +27,12 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Request failed');
|
||||
if (!res.ok) throw new ApiError(data.error || data.message || 'Request failed', res.status, data);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
export { ApiError };
|
||||
|
||||
export const api = {
|
||||
// Setup
|
||||
getSetupStatus: () =>
|
||||
@@ -67,9 +79,16 @@ export const api = {
|
||||
|
||||
// Time off
|
||||
listTimeOff: () => request<TimeOffRequest[]>('GET', '/timeoff'),
|
||||
createTimeOff: (data: CreateTimeOffInput) => request<TimeOffRequest>('POST', '/timeoff', data),
|
||||
createTimeOff: (data: CreateTimeOffInput & { volunteer_id?: number; confirm_conflicts?: boolean }) =>
|
||||
request<TimeOffRequest | TimeOffConflictResponse>('POST', '/timeoff', data),
|
||||
updateTimeOff: (id: number, data: { starts_at: string; ends_at: string; reason?: string }) =>
|
||||
request<TimeOffRequest>('PUT', `/timeoff/${id}`, data),
|
||||
deleteTimeOff: (id: number) =>
|
||||
request<{ deleted: boolean; restored_shifts: ConflictingShift[] }>('DELETE', `/timeoff/${id}`),
|
||||
reviewTimeOff: (id: number, status: 'approved' | 'rejected') =>
|
||||
request<TimeOffRequest>('PUT', `/timeoff/${id}/review`, { status }),
|
||||
getRemovedShifts: (id: number) =>
|
||||
request<ConflictingShift[]>('GET', `/timeoff/${id}/shifts`),
|
||||
|
||||
// Check-in / out
|
||||
checkIn: (schedule_id?: number, notes?: string) =>
|
||||
@@ -206,6 +225,19 @@ export interface CreateTimeOffInput {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ConflictingShift {
|
||||
instance_id: number;
|
||||
name: string;
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
}
|
||||
|
||||
export interface TimeOffConflictResponse {
|
||||
message: string;
|
||||
conflicts: ConflictingShift[];
|
||||
}
|
||||
|
||||
export interface CheckIn {
|
||||
id: number;
|
||||
volunteer_id: number;
|
||||
|
||||
Reference in New Issue
Block a user