# Volunteer Shifts Page ## Overview **File Path:** `admin/src/pages/volunteer/VolunteerShiftsPage.tsx` (312 lines) **Route:** `/volunteer/assignments` **Role Requirements:** Authenticated users (USER or TEMP role) **Purpose:** Volunteer-facing shift management showing upcoming shifts and personal signups with signup/cancel functionality. **Key Features:** - Segmented tabs: "Upcoming Shifts" / "My Signups" - Responsive shift cards grid (xs=1, sm=2 columns) - Progress bar showing volunteer capacity - Signup confirmation modal - Cancel signup modal (danger button) - "Signed Up" badge + cancel link on signed-up shifts - Empty states for no shifts/signups - Dark theme (VolunteerLayout) **Layout:** Uses `VolunteerLayout` with top navigation --- ## Features ### 1. Segmented Tabs ```tsx ``` ### 2. Shift Cards **Upcoming Shifts Tab:** - Shows all available shifts - "Sign Up" button (primary) - "Signed Up" badge if user already signed up - "Cancel Signup" link if signed up **My Signups Tab:** - Shows only user's signups - "Cancel Signup" button (danger) - Shift details emphasized ### 3. Capacity Progress Bar ```tsx const percentage = (shift.currentSignups / shift.maxVolunteers) * 100; const color = percentage < 70 ? 'success' : percentage < 90 ? 'warning' : 'exception'; {shift.currentSignups} of {shift.maxVolunteers} volunteers ``` ### 4. Signup Confirmation Modal ```tsx setSignupModalVisible(false)} > Are you sure you want to sign up for:
{selectedShift?.title}
{dayjs(selectedShift?.date).format('MMMM D, YYYY')}
{selectedShift?.startTime} - {selectedShift?.endTime}
``` ### 5. Cancel Signup Modal ```tsx setCancelModalVisible(false)} okText="Yes, Cancel Signup" okButtonProps={{ danger: true }} > Are you sure you want to cancel your signup for this shift? ``` --- ## State Management ```tsx const [activeTab, setActiveTab] = useState<'upcoming' | 'signups'>('upcoming'); const [shifts, setShifts] = useState([]); const [mySignups, setMySignups] = useState([]); const [loading, setLoading] = useState(true); const [signupModalVisible, setSignupModalVisible] = useState(false); const [cancelModalVisible, setCancelModalVisible] = useState(false); const [selectedShift, setSelectedShift] = useState(null); ``` --- ## API Integration ### Endpoints #### 1. Get Upcoming Shifts ```http GET /api/map/shifts/upcoming Authorization: Bearer {token} ``` Response: ```json [ { "id": "cm1abc123", "title": "Weekend Canvass", "date": "2025-02-15", "startTime": "10:00", "endTime": "14:00", "location": "Campaign Office", "maxVolunteers": 10, "currentSignups": 7, "cutId": "cm2cut123", "cutName": "Downtown", "isSignedUp": false } ] ``` #### 2. Get My Signups ```http GET /api/map/shifts/my-signups Authorization: Bearer {token} ``` #### 3. Sign Up ```http POST /api/map/shifts/:id/signup Authorization: Bearer {token} ``` Response: ```json { "success": true, "signupId": "cm3signup456", "message": "Successfully signed up for shift" } ``` #### 4. Cancel Signup ```http DELETE /api/map/shifts/:id/cancel Authorization: Bearer {token} ``` --- ## Code Examples ### Shift Card (Upcoming Tab) ```tsx {shift.isSignedUp && ( Signed Up )} {shift.title} {dayjs(shift.date).format('MMMM D, YYYY')} {shift.startTime} - {shift.endTime} {shift.location} {shift.cutName && {shift.cutName}} {shift.isSignedUp ? ( ) : ( )} ``` ### My Signups Tab ```tsx {activeTab === 'signups' && ( <> {mySignups.length === 0 ? ( ) : ( {mySignups.map(shift => ( {shift.title} {/* Shift details */} ))} )} )} ``` --- ## Performance Considerations 1. **Parallel Fetches**: Upcoming shifts and signups fetched simultaneously 2. **Optimistic Updates**: Signup/cancel updates UI immediately 3. **Tab State**: No refetch when switching tabs (cached) 4. **Debounced Modals**: Prevent double-submission with loading state --- ## Responsive Design - **Mobile**: Single column cards (xs=24) - **Tablet**: Two column grid (sm=12) - **Desktop**: Two column grid maintained (not 3+ for readability) - **Segmented Tabs**: Full-width on mobile, auto-width on desktop --- ## Accessibility - **Tab Navigation**: Segmented component keyboard accessible - **Button Labels**: Clear action labels ("Sign Up", "Cancel Signup") - **Modal Focus**: Auto-focus on OK button - **Screen Reader**: Empty states announce "No shifts available" --- ## Troubleshooting ### Issue: Signed Up Badge Not Showing **Cause:** `isSignedUp` field not populated by API **Solution:** ```typescript // Backend: Include isSignedUp in shift query const shifts = await prisma.shift.findMany({ where: { date: { gte: new Date() } }, include: { signups: { where: { userId: req.user!.id }, select: { id: true } } } }); // Map to include isSignedUp return shifts.map(shift => ({ ...shift, isSignedUp: shift.signups.length > 0 })); ``` ### Issue: Cancel Not Refreshing List **Solution:** ```tsx const handleCancel = async () => { try { await api.delete(`/api/map/shifts/${selectedShift.id}/cancel`); message.success('Signup cancelled'); // Refresh both lists const [upcomingRes, signupsRes] = await Promise.all([ api.get('/api/map/shifts/upcoming'), api.get('/api/map/shifts/my-signups') ]); setShifts(upcomingRes.data); setMySignups(signupsRes.data); } catch (error) { message.error('Failed to cancel signup'); } finally { setCancelModalVisible(false); } }; ``` --- ## Related Documentation - [Public Shifts Page](../public/shifts-page.md) - [Admin Shifts Page](../admin/shifts-page.md) - [Volunteer Map Page](./volunteer-map-page.md) - [VolunteerLayout](../../components/volunteer-layout.md)