Anaya Care Docs

Incident Reports & Emergency Alerts

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

Purpose

This module covers two safety mechanisms:

  1. Incident reports — structured after-the-fact reports (falls, medication errors, injuries, etc.) that a care provider files against a client from the mobile app. Management reviews and resolves them on the web dashboard and can share them with the client's family. Backend: apps/backend/src/incident-reports/.
  2. Emergency alerts (SOS) — a real-time panic action available to the assigned caregiver during an in-progress shift. It immediately dials 911 on the device and, in parallel, records an alert with GPS coordinates and fans out critical notifications to management and family. Backend: apps/backend/src/emergency-alerts/.

Both are tenant-scoped (every record carries a business ref) and both feed the unified notification pipeline (see 12-communication.md).

Entities & Data Model

IncidentReport

  • Collection: incident_reports (apps/backend/src/incident-reports/entities/incident-report.entity.ts:11)
  • Indexes: {business:1, createdAt:-1}, {client:1, createdAt:-1}, {status:1} (entity file, lines 104–107), plus single-field indexes on business and client.
FieldTypeNotes
businessObjectId → Businessrequired, indexed (tenant scope)
clientObjectId → Clientrequired, indexed
reportedByObjectId → Userrequired; the filing care provider
incidentTypeenum IncidentTypeFALL, MEDICATION_ERROR, INJURY, BEHAVIORAL_INCIDENT, SKIN_INTEGRITY_ISSUE, CHOKING, MEDICAL_EMERGENCY (packages/shared/src/enums/domain-status.ts:718-726)
incidentDateDaterequired; when the incident occurred
descriptionStringrequired; min 10 chars (DTO create-incident-report.dto.ts:14-16)
attachmentsString[]file URLs (photo uploads), default []
statusenum IncidentReportStatusSUBMITTEDUNDER_REVIEWRESOLVED (domain-status.ts:709-713); default SUBMITTED
reviewedBy / reviewedAt / reviewNotesObjectId → User / Date / Stringset when status moves to UNDER_REVIEW
resolvedBy / resolvedAt / resolutionNotesObjectId → User / Date / Stringset when status moves to RESOLVED
sharedWithFamily / sharedWithFamilyAtBoolean / Datedefault false; one-way flag
editableUntilDaterequired; creation time + 30 minutes (incident-reports.service.ts:35,63-64)
createdAt / updatedAtDateMongoose timestamps

EmergencyAlert

  • Collection: emergency_alerts (apps/backend/src/emergency-alerts/entities/emergency-alert.entity.ts:6)
  • Indexes: {business:1, triggeredAt:-1}, {client:1, triggeredAt:-1} (entity file, lines 72–73).
FieldTypeNotes
businessObjectId → Businessrequired, indexed
shiftAssignmentObjectId → ShiftAssignmentrequired, indexed; the in-progress shift the SOS was raised from
clientObjectId → Clientrequired, indexed
caregiverObjectId → Userrequired; the triggering caregiver
latitude / longitude / accuracyNumberoptional GPS fix at trigger time
triggeredAtDaterequired; device-supplied timestamp
clientAddressStringdenormalized "street, city, state" snapshot (emergency-alerts.service.ts:88-92)

The EmergencyAlert entity has no status field — it is a write-once event record with no acknowledgment or lifecycle in the schema.

  • LocationBreadcrumb (location_breadcrumbs, owned by the shifts module): when an emergency alert has coordinates and an open timesheet exists, a breadcrumb of type LocationBreadcrumbType.EMERGENCY_CALL is written with metadata.emergencyAlertId, so the SOS appears on the shift's location timeline/map (emergency-alerts.service.ts:115-135).
  • ShiftHandover (shift-handovers module): incident submission enqueues an AI handover decision; if the AI decides a handover is warranted, a ShiftHandover document is created with sourceReportType: 'incident' and sourceReportId = the incident's id (apps/backend/src/shift-handovers/listeners/handover-trigger.listener.ts:51-59, shift-handovers.processor.ts).

Workflows & State Machines

Incident report: filing → review → resolution

  1. File (mobile)POST /clients/:clientId/incident-reports requires INCIDENT_REPORTS_MANAGE (incident-reports.controller.ts:36-37). The service validates the client exists, stamps editableUntil = now + 30min, creates the report with status: SUBMITTED, then emits incident-report.submitted and a platform-log activity event (incident-reports.service.ts:52-105).
  2. Photo attachmentsPOST .../:id/upload (reporter-only) uploads images via FilesService.uploadFile({ isPublic: true, type: 'image', folder: 'incident-reports/{id}' }) and appends the URL (incident-reports.service.ts:340-372). The mobile create screen uploads all selected photos right after creation (apps/mobile/app/(client)/[id]/incident-reports/create.tsx:64-83).
  3. Edit windowPATCH .../:id is allowed only for the reporter and only before editableUntil; otherwise 403 (incident-reports.service.ts:210-219). Note: no client UI calls this endpoint today (no incidentReportApi.update usage in apps/mobile/app/(client)/[id]/incident-reports/).
  4. Review / resolve (web)PATCH .../:id/review accepts only UNDER_REVIEW or RESOLVED (review-incident-report.dto.ts:5-6). UNDER_REVIEW stamps reviewedBy/reviewedAt(/reviewNotes); RESOLVED stamps resolvedBy/resolvedAt(/resolutionNotes/reviewNotes). Emits incident-report.status-changed plus a platform-log event (incident-reports.service.ts:230-298).
  5. Share with familyPOST .../:id/share flips sharedWithFamily once (409-style BadRequestException if already shared) and emits incident-report.shared-with-family (incident-reports.service.ts:300-338).

The service performs no current-status transition validationreview() assigns whatever status the DTO carries (incident-reports.service.ts:243). The web UI constrains the flow (hides the action button once RESOLVED, defaults to "Start Review" from SUBMITTED and "Resolve" from UNDER_REVIEW), but the review dialog's status dropdown still lets a manager pick either value (apps/web/.../incident-reports/page.tsx, Detail/Review dialogs). This is a frontend-only rule.

Notification fan-out (incident reports)

All fan-out happens in apps/backend/src/notification/listeners/notification.listener.ts via NotificationService.sendUnifiedNotification → BullMQ unified-notification job → NotificationDispatcherService → per-channel send (in-app persists + emits over the notifications WebSocket via NotificationGateway; push via Expo; email/SMS only when the registry enables them). See 12-communication.md.

EventRecipientsNotification type & channels
incident-report.submittedAll users whose effective role permissions include INCIDENT_REPORTS_VIEW_ALL (admins/care managers), excluding the submitter (notification.listener.ts:306-331; resolution in auth/services/permission.service.ts:119-160)IncidentReportSubmittedhighPriority: in-app + high-priority push, no email/SMS; not user-configurable (notification-registry.ts:818-824)
incident-report.status-changedSame VIEW_ALL set plus the original reporter (notification.listener.ts:334-373)IncidentReportUnderReview / IncidentReportResolvedstandard: in-app + normal push; user-configurable (notification-registry.ts:825-838)
incident-report.shared-with-familyActive RepresentativeProfile users for the client (notification.listener.ts:376-410)IncidentReportSharedWithFamilyhighPriority; not user-configurable (notification-registry.ts:839-845)

Emergency alert: trigger → fan-out

  1. Trigger (mobile) — the SOS button is shown in the client-detail header only while the caregiver has an active shift (apps/mobile/app/(client)/[id]/index.tsx:465-472). On confirm it dials tel:911 immediately, then best-effort grabs GPS and calls POST /shifts/:shiftId/emergency-alert; API failures are swallowed so the 911 call is never blocked (apps/mobile/components/emergency/sos-button.tsx:26-57).
  2. Endpoint — guarded by SHIFTS_EXECUTE and throttled to 3 requests/minute (apps/backend/src/shifts/shifts.controller.ts:708-721).
  3. Validation — shift must exist, must be IN_PROGRESS, and the caller must be the assigned caregiver (emergency-alerts.service.ts:66-81).
  4. Persistence — creates the EmergencyAlert (with denormalized clientAddress) and, if coordinates and a timesheet exist, an EMERGENCY_CALL location breadcrumb on the shift timeline (emergency-alerts.service.ts:96-135).
  5. Fan-out — emits emergency-alert.triggered (events/emergency-alert.events.ts:1).

Notification fan-out (emergency alerts)

EmergencyAlertNotificationListener (apps/backend/src/notification/listeners/emergency-alert-notification.listener.ts:31-93) notifies:

  • All active users in the business whose role is in MANAGEMENT_ROLES (= ADMIN_ROLES = Owner, Admin, SuperAdmin, System — packages/shared/src/enums/user-role.ts:41,53; note this is a deprecated role-group check, not a permission check), and
  • All active RepresentativeProfile (family) users for the client,
  • minus the triggering caregiver.

Notification type EmergencyCallInitiated uses the critical channel set: in-app + critical-interruption push + email + SMS (Twilio via SmsChannel/SmsSenderService), not user-configurable (notification-registry.ts:847-855; channels at notification-registry.ts:8-34; SMS at apps/backend/src/notification/channels/sms.channel.ts). The in-app notification deep-links to /dashboard/clients/{clientId}/edit/resident-profile (emergency-alert-notification.listener.ts:79).

There is no acknowledgment workflow. No endpoint reads, lists, or acknowledges emergency_alertsEmergencyAlertsService exposes only triggerEmergencyAlert and no controller exists in the module (emergency-alerts.module.ts registers no controllers). Acknowledgment-shaped permissions exist in the shared package but are wired to nothing (see Gaps).

Business Rules & Constraints

  • Tenant scoping: every read/write filters by business: businessId from the JWT (incident-reports.service.ts:121-124,186,201,236,305,346).
  • Minimum description length: 10 characters, enforced by DTO validation (create-incident-report.dto.ts:14-16) and duplicated client-side on mobile (create.tsx:61-62).
  • 30-minute edit window: only the reporter may PATCH a report, and only until editableUntil (incident-reports.service.ts:35,210-219). Attachment uploads are reporter-only but have no time-window check (incident-reports.service.ts:354-357).
  • Attachments are uploaded as public files (isPublic: true, incident-reports.service.ts:360-363) and rendered on the web via direct <img src={url}>.
  • Review statuses limited by DTO: only UNDER_REVIEW or RESOLVED can be set via /review; there is no way to return a report to SUBMITTED (review-incident-report.dto.ts:5-6). No current-status check is performed server-side (incident-reports.service.ts:243).
  • Share-with-family is one-way and idempotent-guarded: re-sharing throws (incident-reports.service.ts:309-313). There is no "unshare".
  • PHI access auditing: both GET endpoints carry @AuditPhiAccess(PlatformLogEntityType.INCIDENT_REPORT), which the platform-log interceptor turns into access-audit entries (incident-reports.controller.ts:52,66; apps/backend/src/common/decorators/audit-phi-access.decorator.ts).
  • Platform activity logs: submission, review, and resolution each emit PLATFORM_LOG_ACTIVITY_EVENT with INCIDENT_REPORT_SUBMITTED / INCIDENT_REPORT_REVIEWED / INCIDENT_REPORT_RESOLVED actions (incident-reports.service.ts:92-104,281-297).
  • Emergency alert preconditions: shift IN_PROGRESS + caller is the assigned caregiver (emergency-alerts.service.ts:72-81); endpoint rate-limited to 3/minute (shifts.controller.ts:710).
  • Emergency alerts never block 911: the client dials first and treats the API call as best-effort (sos-button.tsx:29-56).
  • Permissions (defaults in packages/shared/src/constants/default-role-permissions.ts; businesses can override per-role and custom roles carry their own sets — permission.service.ts:43-84):
    • Admin: INCIDENT_REPORTS_VIEW_ALL + MANAGE (lines 110–111); EMERGENCY_ALERTS_VIEW_ALL + ACKNOWLEDGE + CONFIGURE (114–116).
    • CareProvider: INCIDENT_REPORTS_VIEW_OWN + MANAGE (224–225); EMERGENCY_ALERTS_VIEW_ASSIGNED + TRIGGER (228–229).
    • Representative (Family): no incident-report permissions; EMERGENCY_ALERTS_VIEW_ASSIGNED + TRIGGER (292–293).
    • MedicalProfessional: INCIDENT_REPORTS_VIEW_ASSIGNED (342); EMERGENCY_ALERTS_VIEW_ASSIGNED + ACKNOWLEDGE (345–346).
    • CareManager migration set: INCIDENT_REPORTS_VIEW_ALL + MANAGE (421–422).
  • Scope hierarchy: :all:assigned:own (packages/shared/src/enums/business-permission.ts:286-336). GET endpoints require INCIDENT_REPORTS_VIEW_ASSIGNED (incident-reports.controller.ts:53,67) — VIEW_OWN does not satisfy it (see Gaps #1).
  • "Assigned"/"own" scoping is not enforced in queries: findAll/findOne filter only by business + client; any caller who passes the guard sees every report for that client, regardless of assignment or authorship (incident-reports.service.ts:121-124,181-194).
  • Emergency alert permissions are not used by the endpoint: the trigger route checks SHIFTS_EXECUTE, not EMERGENCY_ALERTS_TRIGGER (shifts.controller.ts:709).

Surfaces (Web & Mobile)

Mobile (care providers)

  • Entry point: client detail screen → "Incident Report" care item (no role gating visible in the menu itself; backend permissions gate the API) (apps/mobile/app/(client)/[id]/index.tsx:279-286).
  • List: apps/mobile/app/(client)/[id]/incident-reports/index.tsx — paginated list with a create FAB.
  • Create: apps/mobile/app/(client)/[id]/incident-reports/create.tsx — incident type picker (from INCIDENT_TYPE_LABELS), date/time, description (client-side ≥10 chars), camera/gallery photos (gallery gated by upload policy, lib/upload-policy.ts import at top) uploaded after creation.
  • Detail: apps/mobile/app/(client)/[id]/incident-reports/[reportId].tsx — read-only view with attachments; no edit UI despite the backend's 30-minute edit window.
  • SOS button: apps/mobile/components/emergency/sos-button.tsx, rendered in the client header only when activeShiftId is set ((client)/[id]/index.tsx:465-472). Confirmation sheet → 911 dial + alert API.
  • API clients: apps/mobile/lib/incident-report-api.ts, apps/mobile/lib/emergency-alert-api.ts.

Web (admins / care managers)

  • Incident review page: apps/web/app/(app)/(admin)/dashboard/clients/[id]/edit/incident-reports/page.tsx — status-tab filters with counts (from the API's statusCounts aggregation), report cards, a detail dialog (description, reporter, photos, review/resolution notes), "Share with Family", and a review dialog ("Start Review" / "Resolve" with notes). The empty state says "Incident reports created from the mobile app will appear here" — web has no create UI. Resolving also invalidates the client tracking summary (useInvalidateTrackingSummary).
  • Forms placeholder: apps/web/app/(app)/(admin)/dashboard/forms/incident-report/page.tsx renders a ComingSoon component — a dead placeholder.
  • Emergency alerts on web: there is no alert list/inbox. Alerts surface only as (a) the EmergencyCallInitiated notification (toast/notification center, deep-link to the client's resident profile) and (b) an "Emergency 911 Call" pin on the shift location map/timeline via the EMERGENCY_CALL breadcrumb (apps/web/.../shifts/_components/location-breadcrumb-utils.ts:30,40).
  • API client: apps/web/lib/incident-report-api.ts (list/get/review/share; no create/update/upload on web).

Frontend-only rules

  • Web hides review actions once RESOLVED and sequences SUBMITTED→UNDER_REVIEW→RESOLVED; the API allows any of the two DTO statuses at any time.
  • Mobile enforces the ≥10-char description before submitting (also enforced server-side).
  • Mobile decides SOS availability purely from the local activeShiftStore; the server re-validates shift status and assignment.

Cross-Module Dependencies

  • incidents → shift-handovers (AI): incident-report.submitted is consumed by HandoverTriggerListener, which queues a BullMQ shift-handovers job; an Anthropic-backed agent (ai-handover-decision.service.ts:1,82) decides whether to create a ShiftHandover attached to the client's in-progress (or ≤24h-old) shift, which the incoming caregiver acknowledges (handover-trigger.listener.ts:51-59, shift-handovers.processor.ts, shift-handovers.service.ts:204-260, shift-handover.entity.ts:8-11,95-101). See 05-scheduling-and-shifts.md.
  • incidents → notifications: three events handled in notification.listener.ts:306-410; delivery via the unified dispatcher (in-app/WebSocket, Expo push; registry notification-registry.ts:817-845). See 12-communication.md.
  • incidents → platform-logs: activity events on submit/review/resolve plus PHI read audits (incident-reports.service.ts:92-104,281-297; controller decorators).
  • incidents → client tracking summary: ClientTrackingSummaryService counts open (status != RESOLVED) and unreviewed (status == SUBMITTED) reports per client for the dashboard summary (apps/backend/src/clients/services/client-tracking-summary.service.ts:261-276).
  • incidents → shift summary exports (PDF): ReportDataEnricherService.buildIncidentReports aggregates incidents in a date range (including per-type counts and sharedWithFamily) into welfare/family PDF reports; visibility per audience is governed by the INCIDENT_REPORTS content item matrix (apps/backend/src/clients/services/report-data-enricher.service.ts:209-237, packages/shared/src/types/shift-summary-export.ts:307,374,473, shift-summary-content-filter.service.ts:161). EMERGENCY_ALERTS is also a content item consumed by the PDF layouts (apps/backend/src/pdf/pdf-layout/report-formal-welfare.pdf.tsx:267).
  • emergency-alerts → shifts: alerts are created from a shift, validate against ShiftAssignment/Timesheet, and write LocationBreadcrumb records (emergency-alerts.module.ts, emergency-alerts.service.ts:96-135). See 05-scheduling-and-shifts.md.
  • emergency-alerts → notifications: dedicated listener with email + SMS escalation (emergency-alert-notification.listener.ts). See 12-communication.md.

Pain point: connection to schedule and care plan

Filing an incident report does not touch the client's care plan, schedule, assessments, or any change-of-conditions record. Verified by tracing every emitter/listener out of incident-reports/:

  • The module emits exactly four things: incident-report.submitted, incident-report.status-changed, incident-report.shared-with-family, and platform-log events (incident-reports.service.ts). Their only listeners are the notification listeners (notification.listener.ts:306-410) and the handover trigger (handover-trigger.listener.ts:51). No listener exists in clients/ (care plans), client-medications/, initial-assessments/, or scheduling services — a repo-wide grep for these event names finds no other consumers.
  • The single behavioral cascade is the AI shift-handover: an incident may generate a ShiftHandover note that the next caregiver must acknowledge at login (shift-handovers.processor.ts, shift-handover.entity.ts). This informs the next shift but does not modify the schedule, shift tasks, or the care plan document (apps/backend/src/clients/entities/client-care-plan.entity.ts has no incident linkage).
  • The change-of-conditions flow (health-observations module, sourceReportType: 'coc') is a parallel sibling input to the same handover queue — incidents do not create or escalate into a COC record, and nothing converts a resolved incident into a care-plan revision or reassessment. See 02-assessments.md and 04-care-plans-and-tasks.md.
  • Remaining surfaces are read-only roll-ups: dashboard counters (client-tracking-summary.service.ts:261-276) and PDF report sections (report-data-enricher.service.ts:209+).

Bottom line: after review/resolution, an incident report is a dead end. The handover banner is the only operational ripple; care plans, schedules, and assessments are never updated or even flagged for follow-up from this module. This is a key product gap (Gaps #7).

Open Questions & Gaps

  1. Default care providers may be unable to read incident reports. GET list/detail require INCIDENT_REPORTS_VIEW_ASSIGNED (incident-reports.controller.ts:53,67), but the CareProvider default set contains only VIEW_OWN + MANAGE (default-role-permissions.ts:224-225), and own does not satisfy assigned in the scope hierarchy (business-permission.ts:317-336). As written, a default-permission care provider can create reports but gets 403 listing/viewing them — yet the mobile app ships full list/detail screens for them. Whether production businesses rely on baseRolePermissionOverrides to fix this cannot be determined from code.
  2. INCIDENT_REPORTS_VIEW_OWN is enforced nowhere in the backend, and the findAll query implements no own/assigned scoping — anyone passing the guard sees all reports for the client (incident-reports.service.ts:121-124). The scope labels in permission-groups.ts:160-162 over-promise relative to actual behavior.
  3. No status-transition validation. review() writes the DTO status directly (incident-reports.service.ts:243), allowing SUBMITTED→RESOLVED in one step (intentional, per web UI) and RESOLVED→UNDER_REVIEW re-opening (web hides it; the API does not). A re-opened report keeps its stale resolvedBy/resolvedAt fields.
  4. Family cannot actually view a shared report. "Share with family" sets a flag and notifies active representatives with the report id in metadata (notification.listener.ts:376-410), but Representatives have no incident-report permission by default (default-role-permissions.ts:260-322 contains none) and no family-facing endpoint filters by sharedWithFamily. The only place the flag is consumed is the PDF export enricher (report-data-enricher.service.ts:237). What the family is supposed to see after tapping the notification cannot be determined from code.
  5. Emergency alerts are write-only. No list/read/acknowledge/configure API exists despite EMERGENCY_ALERTS_VIEW_ASSIGNED/VIEW_ALL/ACKNOWLEDGE/CONFIGURE being defined, granted by default, and even surfaced in the permission-management UI strings ("Acknowledge and respond to emergency alerts", permission-groups.ts:404-406). The trigger endpoint also checks SHIFTS_EXECUTE instead of EMERGENCY_ALERTS_TRIGGER (shifts.controller.ts:709), so the granted TRIGGER permission (including the Representative's, line 292–293) is inert. Admins can never mark an alert handled, and there is no escalation if nobody responds.
  6. Incident photo attachments are uploaded as public files (isPublic: true, incident-reports.service.ts:361) even though incident GETs are treated as PHI access (audit decorator). Anyone with the URL can fetch the image. Also, attachment upload has no edit-window or count/size limit, unlike report edits.
  7. Incidents are operationally a dead end (the cited pain point): no care-plan revision, no reassessment trigger, no schedule adjustment, no COC escalation follows from filing or resolving an incident — only notifications, an optional AI handover note, dashboard counters, and PDF roll-ups. See the pain-point subsection above.
  8. The 30-minute edit window is unused by clients: no web or mobile UI calls PATCH .../:id (no incidentReportApi.update callers), so reporters have no practical way to correct a report; and because the service restricts update to the reporter, admins holding MANAGE cannot correct one either. There is also no delete endpoint.
  9. Inconsistent recipient selection between the two features: incident fan-out is permission-based (getUserIdsWithPermission with VIEW_ALL), while emergency fan-out uses the deprecated MANAGEMENT_ROLES role-group (emergency-alert-notification.listener.ts:42-46; deprecation note at user-role.ts:46-53) — custom roles with full emergency permissions would receive incident notifications but not emergency alerts.
  10. Web "Forms → Incident Report" page is a ComingSoon stub (apps/web/app/(app)/(admin)/dashboard/forms/incident-report/page.tsx) — intended scope cannot be determined from code.
  11. Emergency alert timestamps are device-supplied (triggeredAt comes from the client DTO with no server-side sanity check, emergency-alerts.service.ts:93), so a skewed device clock produces misleading alert/breadcrumb timelines.

On this page