# ShiftsPage ## Overview The ShiftsPage provides complete volunteer shift management for the Map module, enabling administrators to schedule canvassing shifts, manage volunteer signups, and coordinate field operations. Features include date/time scheduling, volunteer capacity tracking, public shift publishing, area (cut) assignment, signup management, and bulk email notifications to confirmed volunteers. **Route:** `/app/map/shifts` **Component:** `admin/src/pages/ShiftsPage.tsx` (757 lines) **Auth Required:** Yes (SUPER_ADMIN, MAP_ADMIN roles) **Layout:** AppLayout ## Screenshot [Screenshot: Shifts page with 6 statistics cards at top (Total, Open, Full, Cancelled, Upcoming, Signups) showing counts with colored icons. Below stats are search bar + status filter dropdown. Main table shows columns: Title, Date, Time (start — end), Location, Area (cut name), Volunteers (progress bar showing X/Y), Status (colored tags), Public (checkmark icon if true), Actions (edit + delete). Rows clickable to open signups drawer. Page header has "Create Shift" button.] ## Features ### Core Features - **Full CRUD operations** — Create, read, update, delete shifts - **Advanced search** — 300ms debounced search by title or location - **Status filtering** — Filter by OPEN, FULL, CANCELLED, COMPLETED - **Statistics dashboard** — 6 cards showing total, open, full, cancelled, upcoming, total signups - **Date/time scheduling** — Date picker + time pickers with 5-minute intervals - **Volunteer capacity** — Max volunteers setting with progress bar visualization - **Area assignment** — Assign shift to a canvass cut (area) - **Public publishing** — Toggle to show shift on public `/shifts` page - **Clickable rows** — Click any row to open signups drawer - **Responsive table** — Columns hide on smaller screens (Time: md+, Location: lg+, Area: md+) ### Signup Management - **Signups drawer** — Click shift row to view all confirmed volunteers - **Manual signup** — Add volunteer by email + name (creates temp user if needed) - **Signup source tracking** — PUBLIC (self-signup), ADMIN (added by admin) - **Remove volunteers** — Delete button for each confirmed signup - **Email all volunteers** — Bulk email button in drawer header - **Signup stats** — Progress bar in table shows current/max volunteers - **Auto-status management** — Shift status auto-updates to FULL when capacity reached ### Shift Status Workflow 1. **OPEN** — Shift created, accepting signups 2. **FULL** — Max volunteers reached (currentVolunteers >= maxVolunteers) 3. **CANCELLED** — Shift cancelled by admin 4. **COMPLETED** — Shift date passed (auto-marked by backend) ## User Workflow ### Viewing Shifts List 1. Navigate to `/app/map/shifts` 2. Page loads with statistics cards at top: - **Total:** All shifts count - **Open:** Shifts accepting signups (green) - **Full:** Shifts at capacity (orange) - **Cancelled:** Admin-cancelled shifts (red) - **Upcoming:** Future shifts (blue) - **Signups:** Total confirmed volunteers across all shifts 3. Table shows first 20 shifts (paginated) 4. View shift details: - Title (bold) - Date (YYYY-MM-DD) - Time (HH:mm — HH:mm format) - Location (e.g., "Campaign HQ, 123 Main St") - Area (cut name if assigned) - Volunteers (progress bar X/Y) - Status tag (color-coded) - Public checkmark icon (if published) - Actions (edit, delete) 5. Click any row to open signups drawer ### Creating a Shift 1. Click **"Create Shift"** button in page header 2. Modal opens (560px width) with vertical form 3. Fill **required fields**: - **Title** — Shift name (e.g., "Door Knocking", "Phone Banking") - **Date** — Date picker (calendar popup) - **Start Time** — Time picker (HH:mm, 5-minute intervals) - **End Time** — Time picker (HH:mm, 5-minute intervals) - **Max Volunteers** — Number input (min: 1) 4. Fill **optional fields**: - **Description** — Multi-line text (shift details + instructions) - **Location** — Text (e.g., "Campaign HQ, 123 Main St") - **Area (Cut)** — Dropdown (select canvass area, searchable) - **Public** — Switch toggle (default: false) 5. Click **"Create"** button 6. Success message: "Shift created" 7. Modal closes, table refreshes to page 1, stats refresh 8. New shift appears with status OPEN ### Editing a Shift 1. Locate shift in table 2. Click **Edit** icon button (EditOutlined) in Actions column 3. Drawer opens on right side (520px width) with vertical form 4. Modify any fields (same as create, plus **Status** dropdown): - Status options: OPEN, FULL, CANCELLED, COMPLETED 5. Click **"Save"** button in drawer header 6. Success message: "Shift updated" 7. Drawer closes, table refreshes, stats refresh ### Publishing a Shift to Public Page 1. Open shift in edit drawer 2. Toggle **"Public"** switch to ON 3. Click **"Save"** 4. Shift now visible on public `/shifts` page 5. Users can self-signup via public page 6. Signups source tracked as PUBLIC ### Assigning a Cut (Area) to Shift 1. Open shift in edit drawer 2. Click **"Area (Cut)"** dropdown 3. Search for cut by name 4. Select cut from list 5. Click **"Save"** 6. **Volunteer portal integration:** - Volunteers assigned to this shift now see it in `/volunteer/assignments` page - Shift with cut enables volunteer canvassing workflow - No cut = general shift (no canvass area) ### Viewing Shift Signups 1. Click any shift row in table 2. **Signups drawer** opens on right side (640px width) 3. Drawer header shows: - TeamOutlined icon + "Signups — {Shift Title}" - **"Email All"** button in header (disabled if no confirmed volunteers) 4. Info card at top displays shift summary: - Date - Time (start — end) - Volunteers (current / max) 5. Table shows confirmed volunteers: - Columns: Email, Name, Phone, Source (PUBLIC/ADMIN tag), Date, Remove button - Pagination: 20 per page (if > 20 signups) - Cancelled signups hidden (filtered out) 6. **Add volunteer section** at bottom: - Email input (required) - Name input (optional) - **"Add"** button (disabled if email empty) ### Manually Adding a Volunteer 1. Open signups drawer for any shift 2. Scroll to bottom "Add volunteer" section 3. Enter **email** (required) 4. Enter **name** (optional) 5. Click **"Add"** button 6. Backend logic: - If user exists: Create ShiftSignup record - If user doesn't exist: Create temp User + ShiftSignup - Signup source: ADMIN - Signup status: CONFIRMED 7. Success message: "Volunteer added" 8. Table refreshes with new volunteer 9. Email and name inputs clear 10. Main shifts table progress bar updates **Temp user creation:** - Role: TEMP - Email: provided email - Password: Readable format (e.g., "BlueEagle42") - Expires: shift date + 1 day - Used for public signups without account ### Removing a Volunteer 1. Open signups drawer 2. Locate volunteer in table 3. Click **Delete** icon button (red, last column) 4. Popconfirm: "Remove this volunteer?" 5. Click **"OK"** 6. Success message: "Volunteer removed" 7. Table refreshes (volunteer row disappears) 8. Main shifts table progress bar updates 9. Shift status may change from FULL to OPEN if capacity now available ### Emailing All Volunteers 1. Open signups drawer with confirmed volunteers 2. Click **"Email All"** button in drawer header 3. Backend sends email to all confirmed volunteers: - Email template: Shift details (title, date, time, location, description) - Subject: "Shift Reminder: {Shift Title}" - From: Site settings sender (e.g., "Changemaker Lite ") 4. Success message: "Emailed N volunteer(s)" (or "N sent, M failed" if failures) 5. Email uses SMTP settings from Settings page ### Searching and Filtering 1. **Search bar** (top left): - Type title or location keywords - 300ms debounce (waits for typing pause) - Search resets pagination to page 1 2. **Status filter** dropdown (top right): - Select OPEN, FULL, CANCELLED, or COMPLETED - Filter resets pagination to page 1 - Clear to show all shifts 3. Filters persist during pagination ### Deleting a Shift 1. Locate shift in table 2. Click **Delete** icon button (DeleteOutlined) in Actions column 3. Popconfirm: "Delete this shift?" 4. Click **"OK"** to confirm 5. Success message: "Shift deleted" 6. Table refreshes, stats refresh 7. **Cascade behavior:** All ShiftSignup records also deleted (Prisma cascade) ## Component Breakdown ### Ant Design Components Used - **Table** — Main shifts list + signups table in drawer - **Button** — Create (primary), edit, delete, add volunteer, email all - **Input** — Search bar, email input, name input (signups drawer) - **Select** — Status filter dropdown, area (cut) dropdown - **Tag** — Status tags (color-coded), signup source tags - **Space** — Action button grouping, drawer header - **Card** — Statistics cards (6 cards), shift summary card in signups drawer - **Statistic** — Numeric displays with icons + prefixes - **Progress** — Volunteer capacity progress bar (in Volunteers column) - **Modal** — Create shift form - **Drawer** — Edit shift (520px), signups drawer (640px) - **Form** — Create/edit shift forms - **Form.Item** — Field wrappers with labels + rules - **Input.TextArea** — Description field (multi-line) - **DatePicker** — Date selection with calendar popup - **TimePicker** — Time selection with hour/minute dropdowns (5-minute steps) - **InputNumber** — Max volunteers numeric input (min: 1) - **Switch** — Public toggle (valuePropName="checked") - **Row, Col** — Responsive grid for stats cards, date/time fields - **Popconfirm** — Delete confirmation (shift + volunteer removal) - **Typography.Text** — Labels, descriptions ### Table Columns (Main Shifts Table) ```typescript const columns: ColumnsType = [ { title: 'Title', dataIndex: 'title', render: (title) => {title}, }, { title: 'Date', dataIndex: 'date', render: (date) => dayjs(date).format('YYYY-MM-DD'), }, { title: 'Time', render: (_, record) => `${record.startTime} — ${record.endTime}`, responsive: ['md'], }, { title: 'Location', dataIndex: 'location', render: (loc) => loc || '--', responsive: ['lg'], }, { title: 'Area', render: (_, record) => record.cut?.name || '--', responsive: ['md'], }, { title: 'Volunteers', width: 140, render: (_, record) => { const confirmed = record._count?.signups ?? record.currentVolunteers; const pct = record.maxVolunteers > 0 ? Math.round((confirmed / record.maxVolunteers) * 100) : 0; return ( = 100 ? 'exception' : 'active'} format={() => `${confirmed}/${record.maxVolunteers}`} /> ); }, }, { title: 'Status', dataIndex: 'status', width: 100, render: (status) => {SHIFT_STATUS_LABELS[status]}, }, { title: 'Public', dataIndex: 'isPublic', width: 70, render: (isPublic) => isPublic ? : null, }, { title: 'Actions', width: 120, render: (_, record) => (