What Exists
File Paths
| File | Purpose |
|---|---|
src/app/dashboard/calendar/page.tsx | Current schedule page (week grid, request/swap views) |
src/app/dashboard/agency/page.tsx | Agency management hub |
src/components/agency/scheduling/MonthCalendarView.tsx | Month calendar grid component |
src/components/agency/ElderTabSelector.tsx | Horizontal scrollable elder tabs |
src/components/agency/ShiftInfoBar.tsx | Active shift timer bar |
src/components/navigation/MoreMenuDrawer.tsx | Bottom sheet (mobile) / side panel (desktop) |
src/components/ui/dialog.tsx | Radix Dialog (modal) |
src/components/ui/select.tsx | Radix Select (accessible dropdown) |
src/components/dashboard/ElderSelector.tsx | Elder dropdown picker |
src/lib/firebase/scheduleShifts.ts | Shift CRUD operations |
src/lib/firebase/shiftCascade.ts | Priority cascade auto-assign |
src/lib/firebase/shiftSwap.ts | Swap request logic |
src/types/index.ts | All TypeScript interfaces |
Reusable Components
- •MoreMenuDrawer (
isOpen,onClose) — Bottom sheet pattern on mobile, side panel on desktop. Use this pattern for shift forms. - •ElderTabSelector (
elders,selectedElderId,onSelect,taskCounts?) — Horizontal tabs for elder switching. - •Radix Select — Accessible dropdown for caregiver/elder picking.
- •Radix Dialog — Modal overlay for confirmations.
- •
cn()utility fromsrc/lib/utils.ts— clsx + tailwind-merge.
Current Firestore Schema
Collection: scheduledShifts
interface ScheduledShift {
id: string;
agencyId: string;
groupId: string;
elderId: string;
elderName: string;
caregiverId: string;
caregiverName: string;
date: Date;
startTime: string; // "09:00"
endTime: string; // "17:00"
duration: number; // minutes
status: 'offered' | 'scheduled' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled' | 'no_show' | 'unfilled';
notes?: string;
assignmentMode?: 'direct' | 'cascade';
cascadeState?: CascadeState;
createdAt: Date;
updatedAt: Date;
createdBy: string;
confirmedAt?: Date;
cancelledAt?: Date;
isRecurring?: boolean;
recurringScheduleId?: string;
}
Collection: shiftRequests
interface ShiftRequest {
id: string;
agencyId: string;
caregiverId: string;
caregiverName: string;
requestType: 'specific' | 'recurring';
specificDate?: Date;
recurringDays?: number[]; // [0-6] Sun-Sat
startTime: string;
endTime: string;
preferredElders?: string[];
notes?: string;
status: 'pending' | 'approved' | 'rejected' | 'cancelled';
requestedAt: Date;
}
Collection: shiftSwapRequests
interface ShiftSwapRequest {
id: string;
agencyId: string;
requestingCaregiverId: string;
targetCaregiverId?: string;
shiftToSwapId: string;
shiftToSwap: { elderId, elderName, date, startTime, endTime };
reason?: string;
status: 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'admin_approved';
requestedAt: Date;
}
What We're Building
Day View (default)
- •Single-day timeline showing all shifts for the agency
- •Each shift card: elder name, caregiver (or "Unassigned"), time range, status badge
- •Tap a shift to open assignment/edit bottom sheet
- •Date navigation: prev/next day arrows, "Today" button
Week View
- •7-day overview as vertical cards (one per day)
- •Each day shows shift count, gap count, total hours
- •Color-coded: green (filled), yellow (pending), red (gaps/unfilled)
- •Tap a day card to switch to Day View for that date
New Shift Form (bottom sheet)
- •Date picker (native
<input type="date">) - •Start time / End time (native
<input type="time">) - •Elder picker (Radix Select, populated from agency elders)
- •Caregiver picker (Radix Select, populated from agency caregivers)
- •Repeat options: None, Daily, Weekdays, Custom (day checkboxes)
- •Notes (textarea, optional)
- •Submit button: creates
scheduledShiftdoc
Implementation Phases
Phase 1: Day View (read-only)
Create src/app/dashboard/agency/schedule/page.tsx:
- •Query
scheduledShiftswhereagencyId == user.agencyIdanddate == selectedDate - •Render shift cards sorted by
startTime - •Date navigation (prev/next/today) using
date-fns - •Status badges with existing color scheme
- •Empty state: "No shifts scheduled for this day"
Test: Verify shifts display for a known date. Verify date navigation works. Verify empty state.
Phase 2: Assign Caregiver
- •Tap shift card → open bottom sheet (reuse MoreMenuDrawer pattern)
- •Bottom sheet shows: shift details (elder, time, status) + caregiver Radix Select
- •On caregiver selection, update
scheduledShift.caregiverIdandcaregiverName - •Update status from
unfilled→scheduled - •Close sheet, refresh list
Test: Tap unassigned shift → pick caregiver → verify Firestore update. Verify UI refreshes.
Phase 3: Week View
- •Add view toggle: Day | Week (pill buttons at top)
- •Query shifts for 7-day window (
startOfWeektoendOfWeek) - •Render 7 vertical day cards with summary stats
- •Gap detection: compare shifts against expected coverage hours
- •Tap day card → switch to Day View for that date
Test: Verify 7 cards render. Verify gap count matches missing coverage. Verify day-tap navigation.
Phase 4: Create Shift (FAB + bottom sheet)
- •Add FAB (floating action button) — bottom-right,
+icon, 56px - •FAB opens bottom sheet with shift form fields
- •Form validation: date required, start < end, elder required
- •On submit: call
createShift()fromscheduleShifts.ts - •Support repeat: if recurring, create shifts for each selected day in next 4 weeks
Test: Create single shift → verify in Firestore. Create recurring → verify multiple docs. Validate form errors.
Phase 5: Edit/Delete Shifts
- •Long-press or tap edit icon on shift card → open edit bottom sheet
- •Pre-populate form with existing shift data
- •"Save Changes" updates doc, "Delete" sets status to
cancelled - •Confirmation dialog (Radix Dialog) before delete
Test: Edit time → verify update. Delete → verify status change. Verify confirmation dialog.
Data Schema Updates
None required. The existing ScheduledShift interface covers all needed fields:
- •
date,startTime,endTimefor scheduling - •
caregiverId,caregiverNamefor assignment - •
elderId,elderNamefor elder association - •
statusfor lifecycle tracking - •
isRecurring,recurringScheduleIdfor repeat shifts
If gap detection needs explicit coverage hours, add to agency settings later (not blocking).
UX Constraints
- •Responsive PWA: Mobile-first, consistent with existing Tailwind breakpoints (
<640pxmobile,640-1024pxtablet,>1024pxdesktop) - •No drag-and-drop: All interactions are tap/click. No sortable lists.
- •Bottom sheet pattern: Reuse
MoreMenuDrawerslide-up-from-bottom approach for all forms - •Touch targets: All interactive elements ≥ 44px height/width on mobile
- •Native inputs: Use
<input type="date">and<input type="time">(no custom picker library) - •Dark mode: Support via existing
dark:Tailwind classes - •Offline awareness: Show stale data indicator if offline (existing PWA pattern)
- •Loading states: Skeleton or spinner while querying Firestore
- •Terminology: "Loved One" in user-facing text,
elderId/elderNamein code
Auth & Role Requirements
- •SuperAdmin (
isSuperAdmin(user)): Full access — create, assign, edit, delete shifts - •Caregiver (
isAgencyCaregiver(user)): View own shifts only, request new shifts - •Use
useAuth()from@/contexts/AuthContextfor role detection - •Agency ID from
user.agencyIdoruser.multiAgencyId
Key Imports
import { format, addDays, subDays, startOfWeek, endOfWeek, isSameDay, isToday } from 'date-fns';
import { useAuth } from '@/contexts/AuthContext';
import { isSuperAdmin, isAgencyCaregiver } from '@/lib/firebase/adminUtils';
import { cn } from '@/lib/utils';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';