Anaya Care Docs

Scheduling & Shifts

Part of the Anaya Care product wiki. See 00-overview.md.

Purpose

This module turns a client's care plan into concrete, trackable work. Admins/care managers define schedules (one-time or RRULE-recurring time slots) per client; the system expands schedules into individual shift assignments, assigns them to care providers (caregivers), and tracks each shift through a formal status state machine — assignment, acceptance, GPS-validated clock-in/out (timesheets), task completion, handover to the next caregiver, end-of-shift reflection, and final completion. It also feeds adjacent flows: unfilled shifts can be converted to job postings, and AI-generated shift summaries report on care delivered over a date range.

Backend code lives mainly in apps/backend/src/shifts/ (shift lifecycle), apps/backend/src/shift-handovers/ (handover records), and apps/backend/src/clients/ (schedules, shift summaries). The status state machine is shared with frontends via packages/shared/src/shift-status-transitions.ts.

Entities & Data Model

ClientSchedule — client_schedules

The recurring template a client's shifts are generated from. (apps/backend/src/clients/entities/client-schedule.entity.ts)

FieldTypeNotes
businessObjectId → BusinessTenant scope, required, indexed (L34–40)
clientObjectId → ClientRequired (L42)
tzidstringIANA timezone, required (L45)
dtstartDateFirst occurrence start, required (L48)
durationMinutesnumberRequired, min 1 (L51)
rrulestring?RFC RRULE; absent/empty = one-time schedule (L54)
exdateDate[]Excluded occurrence dates (L57)
priorityenum SchedulePriority LOW/NORMAL/HIGH/URGENT, default NORMALDefined in the same file (L6–11); not consumed in any backend scheduling logic found
notesstring(L67)
endDate (virtual)Date | nullComputed from rrule until/count + duration; null = never ends (L74–111)
isActive (virtual)boolean"Active" includes schedules starting within the next 7 days (L113–154)

Indexes: {business, createdAt}, {client, dtstart}, {rrule} (L156–158).

ShiftAssignment — shift_assignments

One concrete shift occurrence. (apps/backend/src/shifts/entities/shift-assignment.entity.ts)

FieldTypeNotes
businessObjectId → BusinessRequired, indexed (L179)
clientObjectId → ClientRequired, indexed (L187)
caregiverObjectId → User, nullablenull = unassigned (L195–201)
scheduleObjectId → ClientScheduleRequired — every shift points back to a schedule (L203–208)
tzid, dtstart, durationMinutesstring/Date/numberCopied from the schedule at generation time (L210–217)
currentStatusenum ShiftStatus, default GENERATEDIndexed (L219–225)
assignmentMethodenum AssignmentMethodAUTO_GENERATED / MANUALLY_ASSIGNED / REASSIGNED / VOLUNTEER_PICKED (packages/shared/src/enums/domain-status.ts L230–235)
assignedBy, assignedAt, responseDeadlineResponse deadline for accept/decline (L234–242)
timesheetObjectId → TimesheetSet at clock-in (L245)
originalAssignmentObjectId → ShiftAssignmentIf this is a reassignment (L249)
jobPostingIdObjectId → JobPosting, nullableSet when shift converted to a job posting (L253–259)
statusHistoryShiftStatusHistory[]Embedded audit trail: status, timestamp, updatedBy, isSystemAction, notes, method (L21–40)
reflectionembedded ShiftReflection3 required prompts + optional notes (L45–64)
handoverNotesembedded HandoverNotesLegacy single embedded handover; sourceReportId/sourceReportType: 'coc' | 'incident' link to a HealthObservation or IncidentReport (L69–100)
completionRequirementsembedded CompletionRequirementstasksRequired/Completed, reflectionRequired/Completed, handoverNotesRequired/Completed, autoClockoutProcessed, allRequirementsMet (L105–148)
timesheetAutoClockout, reflectionGracePeriodEnds, reflectionReminderSentAtAuto-clock-out + reflection-window bookkeeping (L287–297)
temporalStatus (virtual)PAST/CURRENT/UPCOMING/FUTUREIN_PROGRESS is always CURRENT even past scheduled end (overtime) (L306–337)

Unique index {client, dtstart, durationMinutes} prevents duplicate shifts for the same slot (L352–355). Additional indexes on caregiver/client/status/schedule + dtstart (L339–349).

Timesheet — timesheets

Clock-in/out record, 1:1 with a worked shift. (apps/backend/src/shifts/entities/timesheet.entity.ts)

FieldTypeNotes
businessObjectId → Business(L104)
userObjectId → UserThe caregiver (L118)
shiftObjectId → ShiftAssignmentBack-referenced from the shift (L171)
clockInAt / clockOutAtDate / Date?clockOutAt undefined = active session (L131–143)
clockInLocation / clockOutLocation{lat, lng} embedded(L151–163)
wasAutoClockOutbooleanSystem auto-clocked-out after grace (L188)
isOutOfRange, clockInDistanceMetersGeofence audit at clock-in (L200–211)
isEarlyClockOut, workedPercentFlagged when worked % < business threshold (L220–228)
adminNotestringSet on force clock-out / admin edits (L237)

CaregiverAssignment — caregiver_assignments (scheduling aspects)

Links a caregiver to a client's care team; this is the pool from which shifts can be assigned/picked up. (apps/backend/src/clients/entities/caregiver-assignment.entity.ts)

FieldTypeNotes
business, client, caregiverObjectIdsUnique per {client, caregiver} (L102)
currentStatusenum CaregiverInviteStatus (Invited/Accepted/Declined/Inactive/Assigned)(L75–81; enum in packages/shared/src/enums/domain-status.ts L197–203)
statusHistoryembedded(L83)
prioritynumber | nullUsed to order the auto-assign caregiver pool (shifts.service.ts L934–947 sorts by priority: 1, createdAt: 1)

Scheduling rules tied to this entity: auto-generation only assigns caregivers with status ASSIGNED or ACCEPTED (shifts.service.ts L935–941); shift pickup requires status ACCEPTED (shifts.service.ts L815–823); manual assignment requires ASSIGNED or ACCEPTED (shifts.service.ts L3535–3547).

ShiftHandover — shift_handovers

A handover message for the incoming caregiver, either AI-generated from a Change-of-Condition/Incident report or authored by the outgoing caregiver. (apps/backend/src/shift-handovers/entities/shift-handover.entity.ts)

FieldTypeNotes
business, clientObjectIds(L34–48)
typeenum HandoverType (change_of_condition / incident_report / routine / caregiver_note)(packages/shared/src/types/shift.ts L18–23)
sourceReportId / sourceReportTypestring / 'coc' | 'incident' | 'caregiver_note'Points at HealthObservation or IncidentReport; absent for caregiver notes (L57–64)
shiftObjectId → ShiftAssignmentOriginating shift; used to scope the "pending for next shift" query (L69–74)
authoredByObjectId → User?Null for AI-authored handovers (L78–79)
content, agentReasoningstringReasoning stored for audit (L81–87)
statusenum Pending/Acknowledged/Expired, default Pending(L8–12, L89–95)
acknowledgedBy, acknowledgedAt, expiresAt(L97–107). No code found that actually sets Expired/expiresAt — see Gaps.

ShiftHandoverDecision — shift_handover_decisions

Audit record of every AI handover-decision call, including skips, so managers can ask "why wasn't I alerted?" (apps/backend/src/shift-handovers/entities/shift-handover-decision.entity.ts L10–17). Fields: source report id/type, createHandover boolean, suggestedType, reasoning, handoverId, model, latencyMs.

ShiftSummary — shift_summaries

AI-generated care report over a date range (not per single shift, despite the name). (apps/backend/src/clients/entities/shift-summary.entity.ts)

FieldTypeNotes
business, client, generatedByObjectIds(L23–43)
dateRange{start, end}(L45–52)
audienceenum ExportAudienceDrives content filtering (L54–60)
dataShiftSummaryData objectStructured aggregation of task submissions, medication logs, health observations (L62–63)
aiSummaryAiSummary objectLLM-generated narrative (Anthropic via createAnthropic, shift-summary.service.ts L143–146)
pdfFileObjectId → LockCare, nullable(L68–73)

Entity relationships

Workflows & State Machines

1. Schedule creation

  • Admin/manager creates a ClientSchedule via ClientSchedulesService.createClientSchedule — validates dtstart, positive duration, tzid, RRULE parseability (client-schedules.service.ts L430–470, L561–590). Blocked for clients in OnHold/Closed status (L105–122).
  • Updates go through updateClientSchedule (L595–621); handleScheduleModification can propagate time/duration/tz changes to future shifts in GENERATED/ASSIGNED status, marking them isModified (L127–199; notification fan-out in shifts.service.ts L744–772).
  • Deletion is a cascade: blocked while any shift from the schedule is IN_PROGRESS; otherwise deletes task submissions, care readiness assessments, health observations, timesheets (clearing user.activeTimesheetId), shifts, care plans, care provider tasks, then the schedule (L673–784). A deletion-preview endpoint returns counts first (L626–671).

2. Shift generation

  • POST /shifts/generate/:clientId (permission SHIFTS_MANAGE) enqueues a BullMQ job on the shifts queue and returns a job id immediately (shifts.controller.ts L387–426); ShiftsProcessor handles generate-shifts and shift-bulk-cancel jobs (shifts.processor.ts L68–88).
  • ShiftsService.generateShiftAssignments walks day-by-day in the client's timezone over the date range derived from the selected schedules, creating one shift per applicable schedule-day (shifts.service.ts L190–428). The range is clamped to a 45-day maximum from the earliest schedule start; unbounded RRULEs default to 45 days (L3956–3963, L3993–4001).
  • Duplicate suppression via lookup on {client, dtstart, durationMinutes} (L293–297) plus the unique index.
  • If autoAssign and the client has an active care team, a caregiver is chosen per shift by strategy: round_robin (default), fair_distribution (least total upcoming hours, L3194–3241), or skill_based (skill-score with fair-distribution fallback, L3246–3314). Assigned shifts start as ASSIGNED with a responseDeadline; otherwise GENERATED (L304–354). Grouped push notifications go to each caregiver (L380–424).
  • Manual single-shift creation from a one-time schedule: POST /shifts/manual (createManualShift, L3500–3739). Recurring schedules are rejected (L3568–3573).

3. Assignment responses & coverage changes

  • Caregiver accept/decline: PUT /shifts/:id/respond — only the assigned caregiver, only from ASSIGNED/GENERATED/REASSIGNED (L433–518). Decline triggers initiateReassignment, which notifies the rest of the care team that the shift is available for pickup (L1020–1084).
  • Manager reassignment: PUT /shifts/:id/reassign (SHIFTS_MANAGE) — validates target is a CareProvider, auto-invites them to the care team if permitted, then runs overlap + daily-hour checks before setting REASSIGNED (L524–666).
  • Unassign: PUT /shifts/:id/unassign reverts to GENERATED, clearing caregiver/deadline (L671–739).
  • Pickup: POST /shifts/:id/pickup — caregiver self-assigns a GENERATED/DECLINED shift if they're an ACCEPTED care-team member and pass overlap + daily-hour checks; status jumps to ACCEPTED with method VOLUNTEER_PICKED (L800–881).
  • Job-posting path: an unfilled shift can be converted to a job posting (convertToJobPosting, L4919–4993; only from GENERATED/DECLINED/ASSIGNED/NO_SHOW, one conversion per shift). When an application is accepted, a job-application.accepted-for-shift event assigns the applicant back onto the shift as ASSIGNED unless it's already filled (apps/backend/src/shifts/listeners/shift-assign-from-job-posting.listener.ts L42–99). See 06-job-postings.md.

4. Clock-in (mobile)

POST /shifts/:id/clock-inclockInForShift (shifts.service.ts L1717–1936) validates, in order:

  1. Caller is the assigned caregiver (L1735–1740).
  2. Status is ASSIGNED or ACCEPTED (L1744–1752) — note clock-in is allowed without explicit acceptance.
  3. No existing timesheet for the shift (L1756–1758).
  4. Geofence: distance to client address vs business-configurable radius (default 30 m). Out-of-range blocks only if clockInGeofenceEnforced (default false); otherwise it's flagged on the timesheet (isOutOfRange) (L1765–1799; defaults in packages/shared/src/shift-status-transitions.ts L173–186).
  5. Time window: not earlier than clockInEarlyWindowMinutes before start (default 480 = 8 h, per-business) and not later than a hardcoded 13-hour late grace (SHIFT_CLOCK_IN.LATE_GRACE_MINUTES, packages/shared/src/constants/common.ts L316) — but also not after the shift's scheduled end (L1801–1837).
  6. No other open timesheet (one active shift at a time) (L1839–1851).
  7. Care Readiness Assessment completed, if required for this client (L1853–1865).

On success: timesheet created with GPS + distance, CLOCK_IN location breadcrumb recorded, shift → IN_PROGRESS, notifications + platform audit log (L1867–1926).

5. Clock-out, handover, reflection, completion (mobile)

  • POST /shifts/:id/clock-outclockOutFromShift (L1999–2265): must be IN_PROGRESS with an open timesheet. Clock-out geofence (100 m, GEOFENCE.CLOCK_OUT_RADIUS) is a soft check — warning only (L2087–2114). Required scheduled tasks must be submitted unless ≥ 24 h since shift start (SHIFT_CLOCK_OUT.FORCE_CLOCK_OUT_HOURS, L2116–2144); any IN_PROGRESS task submissions are auto-completed (L2146–2148). Early clock-out below the business threshold % (default 50) flags the timesheet (L2159–2173). Shift → SHIFT_ENDED and a reflection grace period is set (default 24 h, per-business) (L2191–2225).
  • Handover notes: after clock-out the mobile app routes to an optional handover-notes screen (apps/mobile/app/(shifts)/time-out.tsx L150; apps/mobile/app/(shifts)/handover-notes.tsx L31 — "Optional step between clock-out and daily reflection"), which creates 0..N ShiftHandover records via POST /handovers (shift-handovers.controller.ts L48–60). Handover is not a completion gate: "Handover creation is no longer a caregiver gate — Anaya generates handovers from COC/incident submissions and the incoming caregiver acknowledges them at login" (shifts.service.ts L4076–4078).
  • Reflection: POST /shifts/:id/reflectionsubmitShiftReflection (L4009–4128). Allowed from SHIFT_ENDED / PENDING_COMPLETION / COMPLETED_WITHOUT_REFLECTION, once, within the grace period. If tasks are also done → COMPLETED and a SHIFT_COMPLETED_EVENT is emitted; else → PENDING_COMPLETION.
  • Background automations (all cron-driven):
    • No-show: every minute, shifts still in ASSIGNED/REASSIGNED/ACCEPTED (GENERATED excluded) with no timesheet past the per-business noShowThresholdMinutes (default 30) are batch-marked NO_SHOW (shift-monitoring.service.ts L150–251). Managers can also mark/revert no-show manually (shifts.service.ts L3745–3827).
    • Reminders/alerts: every-minute monitoring cron sends pre-shift reminders and overdue/critical alerts (escalating to managers) using Redis SET NX dedup (shift-monitoring.service.ts L110–140).
    • Auto clock-out: every minute, open timesheets past shift end + grace (default 30 min) are clocked out with wasAutoClockOut: true, then handleAutoClockoutSync sets SHIFT_ENDED, auto-completes running tasks, and starts the reflection window (services/auto-clock-out.service.ts L37–209; shifts.service.ts L4237–4313).
    • Reflection lifecycle: every 5 minutes, midpoint reminders are pushed, and expired grace periods transition the shift to COMPLETED_WITHOUT_REFLECTION (services/auto-complete-reflection.service.ts L38–177).

6. Handover decision flow (AI)

  • health-observation.submitted and incident-report.submitted events enqueue an evaluate job on the handover queue (shift-handovers/listeners/handover-trigger.listener.ts L31–59). COC events missing businessId are skipped with a warning (L33–42).
  • The processor/AI decides whether a handover is warranted; recordAgentDecision persists the decision (always) and the handover (when created), deduping AI handovers per source report and attaching them to the in-progress shift or the most recent shift started within 24 h (shift-handovers.service.ts L204–281).
  • The incoming caregiver sees pending handovers via GET /handovers/pending?clientId&shiftId — scoped to handovers attached to their upcoming shift, the immediately prior shift for the same client, or legacy shift-less AI handovers (L283–325) — and acknowledges via POST /handovers/:id/acknowledge (idempotent, L343–364).
  • Late-write contract: a caregiver note written after the next shift already clocked in attaches to that active shift and push-notifies the incoming caregiver (L68–158).

7. Shift summary

POST /clients/:clientId/shift-summaries/generate?startDate&endDate&audience (permission CLIENTS_VIEW_ASSIGNED, clients/controllers/shift-summary.controller.ts L30–56) aggregates SUBMITTED task submissions, medication logs, and submitted/reviewed/acknowledged health observations for the range (max 365 days), builds structured data, generates an audience-tailored AI narrative, and persists (shift-summary.service.ts L154–282). Same range+audience returns the cached summary unless force=true (which deletes all summaries for the range across audiences, L188–202). PDF export re-filters the saved data by audience via ShiftSummaryContentFilterService, which maps the shared CONTENT_MATRIX (include/consider/exclude per ExportAudience) onto data sections (shift-summary-content-filter.service.ts L17–64; export at shift-summary.service.ts L377+).

Shift status state machine

Source of truth: packages/shared/src/shift-status-transitions.ts (L14–69). Backend writes go through service-level guards; markShiftAsNoShow validates with isValidShiftTransition (shifts.service.ts L3759), other paths enforce explicit status allow-lists.

Helper sets: PRE_CLOCK_IN_STATUSES (ASSIGNED/REASSIGNED/GENERATED/ACCEPTED), NEEDS_COVERAGE_STATUSES (GENERATED/DECLINED/NO_SHOW), TERMINAL_STATUSES (COMPLETED/COMPLETED_WITHOUT_REFLECTION/CANCELLED) (shift-status-transitions.ts L99–122).

Business Rules & Constraints

  • Tenant scoping: every entity carries business; handover lookups treat cross-tenant shift ids as not-found (shift-handovers.service.ts L75–84).
  • Client lifecycle gate: schedules and shifts cannot be created for OnHold/Closed clients (client-schedules.service.ts L100–122; shifts.service.ts L171–184).
  • No duplicate shifts per {client, dtstart, durationMinutes} — unique index (shift-assignment.entity.ts L351–355).
  • Generation horizon: max 45 days per generation run (shifts.service.ts L3993–4001).
  • Overlap check (checkSchedulingConflict): any time-overlap against the caregiver's ASSIGNED/ACCEPTED/IN_PROGRESS shifts in a ±24 h window blocks reassign, pickup, manual assign, job-application acceptance, and job apply (shifts.service.ts L977–1018; callers listed under "16-hour rule" below).
  • Daily-hour caps: 12 h/day across different clients, 24 h/day for the same client (packages/shared/src/constants/common.ts L294–306). Computed timezone-aware with midnight-spanning proration and a two-day evaluation for overnight shifts (shifts.service.ts L5155–5286).
  • One active timesheet per caregiver at a time (shifts.service.ts L1839–1851).
  • Clock-in window: per-business early window (default 8 h before start), hardcoded 13 h late grace, never after scheduled shift end (shifts.service.ts L1801–1837; common.ts L312–325).
  • Clock-in geofence: default 30 m radius; soft-flag by default, hard-block only when business sets clockInGeofenceEnforced (shift-status-transitions.ts L140–147; shifts.service.ts L1765–1799).
  • Clock-out geofence is advisory only (100 m, warning log) (shifts.service.ts L2087–2114).
  • Tasks gate clock-out until 24 h after shift start, then incomplete clock-out is allowed with a warning (shifts.service.ts L2128–2144).
  • Reflection window: business-configurable grace (default 24 h); expiry auto-completes as COMPLETED_WITHOUT_REFLECTION (auto-complete-reflection.service.ts L120–176).
  • No-show: auto-marked after per-business threshold (default 30 min) only for shifts that had a caregiver (GENERATED excluded) (shift-monitoring.service.ts L150–251).
  • Pickup eligibility: only ACCEPTED care-team members; reassignment can auto-invite a new caregiver only for users with SHIFTS_MANAGE or bypass roles (shifts.service.ts L815–823, L552–583).
  • Job-posting conversion: once per shift, only from GENERATED/DECLINED/ASSIGNED/NO_SHOW; rollback deletes the orphaned posting if linking fails (shifts.service.ts L4930–4990).
  • Per-business shift settings (Business.settings.shift): no-show threshold, clock-in early window, auto-clock-out grace, geofence enforce/radius, early-clock-out %, reflection grace, reminder/alert timings — all defaulted in DEFAULT_SHIFT_SETTINGS (shift-status-transitions.ts L132–186), managed at GET/PATCH /shifts/settings/business (permission SHIFTS_SETTINGS, shifts.controller.ts L111–137).
  • Shift summary caching: one summary per (client, range, audience); regeneration requires force (shift-summary.service.ts L185–202).

The 16-hour rule (pain point)

Stated requirement: "A care provider must NOT have a schedule exceeding 16 hours in a single day; when they apply to a job posting there should be an indicator whether they can be accepted."

Finding: no 16-hour rule exists anywhere in the repo. Searches for 16, sixteen, MAX_HOURS, maxHours, hoursPerDay in scheduling/job contexts find no 16-hour daily cap. What exists instead is a dual cap of 12 h/day cross-client and 24 h/day same-client:

  • Constants: SHIFT_LIMITS.MAX_DAILY_HOURS = 12 and SHIFT_LIMITS.MAX_DAILY_HOURS_SAME_CLIENT = 24 (packages/shared/src/constants/common.ts L294–306). The comments explicitly allow same-client work above 12 h "up to this cap (e.g. a single client requiring around-the-clock care)".
  • Engine: ShiftsService.getCaregiverDailyHours + checkDailyHourLimit partition a caregiver's committed hours (ASSIGNED/ACCEPTED/IN_PROGRESS shifts) per calendar day in the shift's timezone, handle midnight-spanning shifts, and evaluate both days of an overnight slot (shifts.service.ts L5155–5286).

Where it IS enforced (server-side, hard block):

FlowCode
Manager reassigns a shiftshifts.service.ts L586–608
Caregiver picks up an open shiftshifts.service.ts L826–848
Manual shift creation (direct caregiver assignment only)shifts.service.ts L3644–3674
Caregiver applies to a job posting (POST /job-postings/:id/apply) → 403job-postings.controller.ts L506–526
Admin accepts a job application → 409/400job-applications.service.ts L332–364

Where it surfaces as an indicator (the "can they be accepted" UX):

  • Job feed filtering: featured/search listings hide postings the requesting CareProvider can't take (overlap or daily cap) (job-postings.controller.ts L358–365, L419–426).
  • Job detail: GET /job-postings/:id returns a structured unavailabilityReason (shift-conflict, daily-limit-cross-client, daily-limit-same-client, with the hour counts) (job-postings.controller.ts L76–121, L486–500). Mobile disables the Apply button and shows the reason (apps/mobile/app/(jobs)/[id].tsx L175–176, L551–567).
  • Web hiring UIs: GET /shifts/availability/check and the caregiver-finder enrichment return {hasConflict, dailyHoursOnDate, wouldExceedDailyLimit, projectedHours, isEligible} (shifts.controller.ts L228–264; client-caregiver-assignments.service.ts L688–720). Rendered as Available / Conflict / "Exceeds {12}h limit" badges in apps/web/.../care-team/_components/caregiver-card.tsx (L68–78) and apps/web/.../job-postings/_components/caregiver-availability-calendar.tsx (L244–275), plus the quick-assign tab (care-team/find/_components/quick-assign-tab.tsx L83). These web labels read the shared constant, so they say "12h", not 16.

Where it is NOT enforced:

  • Bulk shift generation with auto-assign: selectCaregiver strategies (round-robin / fair-distribution / skill-based) never call checkSchedulingConflict or checkDailyHourLimit (shifts.service.ts L949–975, L3194–3314). Auto-generation can therefore stack a caregiver past any daily cap and even create overlapping shifts.
  • Caregiver accept (respondToShift): no re-check at acceptance time (shifts.service.ts L433–518).
  • Event-driven assignment from job posting: the listener that writes the caregiver onto the shift doesn't re-check (the check happened at application-acceptance time) (shift-assign-from-job-posting.listener.ts L42–99).
  • Schedule creation itself: schedules have no caregiver, so no hour validation applies there.

Whether the product intent is 16 h (per the pain-point statement) or the implemented 12/24 split cannot be determined from code — the code is internally consistent at 12/24, and no 16-hour constant, comment, or migration was found. Cross-link: 06-job-postings.md.

Surfaces (Web & Mobile)

Web (admins / care managers) — apps/web

  • Business calendar: app/(app)/(admin)/dashboard/calendar/ — combined calendar with shift + appointment detail dialogs (_components/combined-calendar.tsx, shift-detail-dialog.tsx). Backed by GET /clients/schedules/business date-range filtering (client-schedules.service.ts L480–529 — conservative recurring filter; frontend does the precise RRULE expansion per the service comment L472–478).
  • All-shifts dashboard: app/(app)/(admin)/dashboard/shifts/page.tsx with filters/list/stats components.
  • Per-client schedules: dashboard/clients/[id]/edit/schedules/ — create/edit dialog, time-range picker, delete dialog (which consumes the deletion preview).
  • Per-client shifts: dashboard/clients/[id]/edit/shifts/ — generation form + generation status (polls the BullMQ job), shift list/table, assignment dialog, report sheet (shift-report-sheet.tsx shows handovers via apps/web/lib/handover-api.ts against the management-only GET /handovers?shiftId, shift-handovers.controller.ts L69–77), location map/timeline, task gantt.
  • Care-team hiring: dashboard/clients/[id]/edit/care-team/ — caregiver cards with availability badges; dashboard/job-postings/_components/caregiver-availability-calendar.tsx shows an applicant's existing shifts and eligibility per day.
  • Shift settings: dashboard/settings/shifts/page.tsxGET/PATCH /shifts/settings/business.
  • Access is permission-based, not hardcoded by role: SHIFTS_MANAGE (generate, manual, reassign, unassign, cancel, no-show, force clock-out), SHIFTS_MONITOR (overdue monitoring), SHIFTS_SETTINGS, SHIFTS_VIEW_ASSIGNED, JOB_POSTINGS_CONVERT (shifts.controller.ts L112–1463).

Mobile (care providers) — apps/mobile

  • My schedule: app/(schedule)/index.tsx — upcoming shifts with accept/decline mutations (L128, L183). app/(schedule)/available.tsx — open-shift marketplace with pickup (L45–71). app/(schedule)/[id]/details.tsx — accept/decline/pickup from detail view.
  • Clock-in: app/(shifts)/time-in.tsx — requires device location; checks Care Readiness Assessment status first and routes to (assessments) if required/not passed (L399–450); computes distance against the backend-resolved per-business geofence (shift.effectiveClockInGeofence) and warns or blocks per the enforced flag (L420–440); confirmation via bottom sheet. Frontend-only nuance: the distance warning UI duplicates the backend check with a shared-constant fallback when settings haven't loaded (L427–430).
  • Clock-out → handover → reflection: app/(shifts)/time-out.tsx clock-out confirmation, then routes to handover-notes.tsx (L150) — an optional, skippable screen that posts 0..N caregiver handover notes — then daily-reflection.tsx (3 required prompts; client-side grace-period countdown using shift.reflectionGracePeriodEnds, L137–142, with an expired state at L183–184). shift-completion.tsx shows the requirements checklist (reflection submitted, 24 h reminder copy at L248).
  • Pending-handover banner/bottom sheet: components/handovers/pending-handover-banner.tsx — non-blocking banner on the client screen (app/(client)/[id]/index.tsx L117–120, passing the active shift id to scope to current + prior shift). Tapping opens a BottomSheetModal listing each pending handover (type-themed: Incident = red "acknowledge before shift", CoC = amber, caregiver note = neutral) with the AI's reasoning and an Acknowledge button; polls every 60 s (L60–64). Frontend-only rule: the banner hides entirely while loading or when empty (L93–95); acknowledgment is optional — nothing blocks clock-in on unacknowledged handovers (no such check exists in clockInForShift).
  • Jobs: app/(jobs)/[id].tsx disables Apply and explains why via unavailabilityReason (see 16-hour-rule section).

Family / MedicalProfessional

No scheduling surfaces found for these roles; families receive shift notifications (clock-in/out events fan out via sendShiftNotification, shifts.service.ts L1909, L2232) but cannot manage schedules. Cannot determine from code whether any family-facing schedule view is planned.

Cross-Module Dependencies

  • Job Postings (06-job-postings.md): shift → posting conversion (shifts.service.ts L4805–4993); acceptance → shift assignment event (shift-assign-from-job-posting.listener.ts); availability/daily-hour checks consumed by job-postings.controller.ts L76–121 and job-applications.service.ts L332–364.
  • Care Provider Tasks (04-care-provider-tasks.md): clock-out requires submitted SCHEDULED+required tasks for the shift (shifts.service.ts L4133–4195 via ClientCareProviderTasksService.getCareProviderTasksDocumentForShift); task submissions reference shiftAssignment and are auto-completed at shift end (L4202–4231); schedule deletion cascades them (client-schedules.service.ts L709–715).
  • Health Observations / Change of Conditions (02-...) and Incident Reports (09-incident-reports.md): submission events trigger the AI handover queue (shift-handovers/listeners/handover-trigger.listener.ts); ShiftHandover.sourceReportId/Type and the legacy embedded HandoverNotes.sourceReportId point back at those records (shift-handover.entity.ts L57–64; shift-assignment.entity.ts L83–90). Health observations also carry a shiftAssignment ref (cascade in client-schedules.service.ts L725–731). Schedules themselves have no direct link to incidents/CoCs — the connection is shift-mediated.
  • Notifications: shift assignment/response/clock events, pickup broadcasts, reflection reminders, and manager alerts via ShiftNotificationService / NotificationService (e.g., shifts.service.ts L3007+, shift-handovers.service.ts L131–155).
  • Clients module: client status gates, client address for geofence, client timezone for generation; care readiness assessments gate clock-in (shifts/care-readiness-assessment.service.ts, checked at shifts.service.ts L1853–1865).
  • Business module: per-business settings.shift overrides (shifts.service.ts L163–169).
  • Platform logs: every lifecycle action emits PLATFORM_LOG_ACTIVITY_EVENT audit entries (e.g., shifts.service.ts L503–516, L1912–1926).
  • AI: handover decisions (shift-handovers/ai-handover-decision.service.ts), shift-summary narratives (Anthropic, shift-summary.service.ts L143–146), job-posting preview generation (shifts.service.ts L4836–4846).

Open Questions & Gaps

  1. 16-hour rule absent: the stated business rule (≤ 16 h/day) is implemented as 12 h cross-client / 24 h same-client (packages/shared/src/constants/common.ts L294–306). Whether 12/24 is a deliberate refinement or a deviation from the 16 h requirement cannot be determined from code.
  2. Auto-generation bypasses all availability checks: generateShiftAssignments with autoAssign never runs checkSchedulingConflict or checkDailyHourLimit (shifts.service.ts L304–319, L949–975), so bulk generation can create overlapping shifts and exceed daily caps that every manual path enforces.
  3. Daily-hour check ignores GENERATED-with-caregiver and REASSIGNED shifts: getCaregiverDailyHours counts only ASSIGNED/ACCEPTED/IN_PROGRESS (shifts.service.ts L5164–5172), but respondToShift accepts responses on GENERATED/REASSIGNED shifts that have a caregiver (L461–466) — those committed-but-pending hours are invisible to the cap.
  4. Job-feed hiding vs indicator inconsistency: list endpoints silently hide conflicting postings (job-postings.controller.ts L358–365, L419–426) while the detail endpoint shows a reason — a caregiver following a deep link sees "why", but the same job never appears in search. Sequential per-posting conflict checks in the list loop are also an N+1 performance concern.
  5. createManualShift duplicate check queries non-existent fields: it filters on shiftDate, startTime, endTime (shifts.service.ts L3584–3590), none of which exist on ShiftAssignment (which uses dtstart/durationMinutes). The query can never match, so duplicate suppression rests solely on the unique index (which throws instead of skipping).
  6. createManualShift auto-assign skips checks and always sets ASSIGNED: the autoAssign ? ASSIGNED : ASSIGNED ternary is degenerate (L3686), round-robin always picks index 0 (L3633–3639), and no conflict/daily checks run on the auto path (only the manual-caregiver path, L3642–3675).
  7. Handover expiry is dead code: ShiftHandoverStatus.Expired and expiresAt exist (shift-handover.entity.ts L8–12, L100–107) with a comment that "older handovers get marked Expired", but no cron/job that sets either was found. Pending handovers persist until acknowledged.
  8. Handover acknowledgment is unscoped: acknowledge() looks up by id only — no business/tenant or caregiver-assignment check on who may acknowledge (shift-handovers.service.ts L343–364), unlike the tenancy-guarded note creation.
  9. CoC events without businessId skip handover evaluation entirely (acknowledged TODO in handover-trigger.listener.ts L33–42) — those submissions never get an AI handover decision or audit record.
  10. Clock-in allowed from ASSIGNED (unaccepted) shifts (shifts.service.ts L1744–1752) while the state machine's ASSIGNED→IN_PROGRESS transition is not in SHIFT_STATUS_TRANSITIONS (ASSIGNED lists ACCEPTED/DECLINED/REASSIGNED/NO_SHOW/GENERATED/CANCELLED only) — the service path and the shared map disagree.
  11. Late clock-in grace (13 h) is hardcoded (common.ts L316) and not part of BusinessShiftSettings, while the early window is configurable — and the "shift already ended" check (L1830–1837) makes the 13 h grace moot for shifts shorter than 13 h.
  12. ClientSchedule.priority appears unused: stored and selected for the calendar (client-schedules.service.ts L523) but no backend logic branches on it; cannot determine intended behavior from code.
  13. CompletionRequirements.handoverNotesRequired/Completed are vestigial: defaults false, never set true anywhere; completion is computed from tasks + reflection only (shifts.service.ts L4079–4082).
  14. Schedule updates skip both guards that creation enforces: updateClientSchedule (client-schedules.service.ts L595–621) calls neither assertClientCanReceiveOperations (client OnHold/Closed gate) nor validateScheduleData (RRULE/duration validation) — only createClientSchedule does (L561–590). An invalid RRULE or an edit on a closed client's schedule passes straight through findByIdAndUpdate.
  15. ShiftSummary naming vs scope: summaries are date-range client reports, not per-shift; the aggregation keys off TaskSubmission.createdAt/MedicationLog.occurredAt, not shift boundaries (shift-summary.service.ts L220–246) — partial-day boundary behavior at range edges is timezone-naive (new Date(startDate) server-local, L162–164).

On this page