Implement Issue #2: Scheduling & Publishing
Some checks failed
CI / Go tests & lint (push) Successful in 1m42s
CI / Frontend tests & type-check (push) Failing after 1m38s
CI / Go tests & lint (pull_request) Successful in 8s
CI / Frontend tests & type-check (pull_request) Failing after 32s

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:
2026-04-08 11:40:41 -03:00
parent 96a363d28f
commit fc88b8f005
9 changed files with 2168 additions and 233 deletions

View File

@@ -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 {