Implement Issue #2: Scheduling & Publishing
Replaces stub schedule CRUD with full shift template + instance system. - DB: add shift_templates, shift_template_roles, shift_template_volunteers, shift_instances, shift_instance_volunteers tables - schedule package: ShiftTemplate and ShiftInstance models with store (generate, publish/unpublish, per-instance edits, volunteer confirmation) - API: shift-templates CRUD + shifts generate/publish/unpublish/update/confirm - Notifications sent on publish (FR-S04), unpublish (FR-S05), instance edit (FR-S09), and volunteer added mid-month (FR-S10) - Frontend: Schedules page with month navigation, template management, publish/unpublish controls, and per-shift edit/confirm - Tests: Go handler tests (14 cases) + React tests (11 cases) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,12 +38,26 @@ export const api = {
|
||||
resendInvite: (id: number) =>
|
||||
request<{ invite_token: string }>('POST', `/volunteers/${id}/invite`, {}),
|
||||
|
||||
// Schedules
|
||||
listSchedules: () => request<Schedule[]>('GET', '/schedules'),
|
||||
createSchedule: (data: CreateScheduleInput) => request<Schedule>('POST', '/schedules', data),
|
||||
updateSchedule: (id: number, data: Partial<CreateScheduleInput>) =>
|
||||
request<Schedule>('PUT', `/schedules/${id}`, data),
|
||||
deleteSchedule: (id: number) => request<void>('DELETE', `/schedules/${id}`),
|
||||
// Shift templates
|
||||
listShiftTemplates: () => request<ShiftTemplate[]>('GET', '/shift-templates'),
|
||||
createShiftTemplate: (data: CreateShiftTemplateInput) =>
|
||||
request<ShiftTemplate>('POST', '/shift-templates', data),
|
||||
updateShiftTemplate: (id: number, data: Partial<CreateShiftTemplateInput>) =>
|
||||
request<ShiftTemplate>('PUT', `/shift-templates/${id}`, data),
|
||||
deleteShiftTemplate: (id: number) => request<void>('DELETE', `/shift-templates/${id}`),
|
||||
|
||||
// Shift instances
|
||||
listShifts: (year: number, month: number) =>
|
||||
request<ShiftInstance[]>('GET', `/shifts?year=${year}&month=${month}`),
|
||||
generateShifts: (year: number, month: number) =>
|
||||
request<ShiftInstance[]>('POST', '/shifts/generate', { year, month }),
|
||||
publishShifts: (year: number, month: number) =>
|
||||
request<{ year: number; month: number }>('POST', '/shifts/publish', { year, month }),
|
||||
unpublishShifts: (year: number, month: number) =>
|
||||
request<{ year: number; month: number }>('POST', '/shifts/unpublish', { year, month }),
|
||||
updateShift: (id: number, data: UpdateShiftInput) =>
|
||||
request<ShiftInstance>('PUT', `/shifts/${id}`, data),
|
||||
confirmShift: (id: number) => request<void>('POST', `/shifts/${id}/confirm`, {}),
|
||||
|
||||
// Time off
|
||||
listTimeOff: () => request<TimeOffRequest[]>('GET', '/timeoff'),
|
||||
@@ -104,23 +118,67 @@ export interface UpdateVolunteerInput {
|
||||
admin_notes?: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
export interface TemplateRole {
|
||||
id?: number;
|
||||
template_id?: number;
|
||||
role_name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ShiftTemplate {
|
||||
id: number;
|
||||
volunteer_id: number;
|
||||
title: string;
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
notes?: string;
|
||||
name: string;
|
||||
day_of_week: number; // 0=Sun, 1=Mon, ..., 6=Sat
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
min_capacity: number;
|
||||
max_capacity: number;
|
||||
roles: TemplateRole[];
|
||||
volunteer_ids: number[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateScheduleInput {
|
||||
volunteer_id?: number;
|
||||
title: string;
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
notes?: string;
|
||||
export interface CreateShiftTemplateInput {
|
||||
name: string;
|
||||
day_of_week: number;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
min_capacity: number;
|
||||
max_capacity: number;
|
||||
roles?: TemplateRole[];
|
||||
volunteer_ids?: number[];
|
||||
}
|
||||
|
||||
export interface InstanceVolunteer {
|
||||
instance_id: number;
|
||||
volunteer_id: number;
|
||||
name: string;
|
||||
confirmed: boolean;
|
||||
confirmed_at?: string;
|
||||
}
|
||||
|
||||
export interface ShiftInstance {
|
||||
id: number;
|
||||
template_id?: number;
|
||||
name: string;
|
||||
date: string; // "YYYY-MM-DD"
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
min_capacity: number;
|
||||
max_capacity: number;
|
||||
status: 'draft' | 'published';
|
||||
year: number;
|
||||
month: number;
|
||||
volunteers: InstanceVolunteer[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UpdateShiftInput {
|
||||
volunteer_ids?: number[];
|
||||
min_capacity?: number;
|
||||
max_capacity?: number;
|
||||
}
|
||||
|
||||
export interface TimeOffRequest {
|
||||
|
||||
Reference in New Issue
Block a user