// The JSON the MARMAR backend sends and takes under /api/clinician (src/clinician/** in marmar-cds). // These mirror the backend's serializers, field for field; the adapters (adapt*.ts) turn them into // the view types in types.ts. When the backend changes a shape, change it here first. import type { CoverageCandidate } from "../coverage"; import type { Assessment, Draft } from "../types"; import type * as T from "./types"; export type Pronoun = "he" | "she" | "they"; /** 1 to 7, Monday first (src/clinician/profile.ts, check-ins/windows.ts). */ export type WeeklyHours = { day: number; open: string; close: string }[]; // ---------------------------------------------------------------- access (public) /** POST /access-requests (src/clinician/access-requests.ts, strict). */ export type AccessRequestBody = { fullName: string; profession: "doctor" | "pharmacist"; licenceBody: string; licenceNumber: string; countryCode: string; facilityName: string; facilityPhone: string; workEmail: string; mobilePhone: string; heardFrom: "film" | "patient_invite" | "colleague" | "other"; heardFromDetail?: string; acceptTerms: true; termsVersion: string; inviteCode?: string; }; /** 202, whatever is on file. */ export type AccessRequestReceived = { status: "received"; message: string }; /** GET /invite-preview/:token (sharing/invitations.ts previewInvitation). */ export type InvitePreview = { status: T.InvitePreview["status"]; patientName: string; withPerson: { name: string; relation: string } | null; doctorName: string; clinicName: string | null; doctorEmail: string | null; expiresAt: string; }; // ---------------------------------------------------------------- sign-in and profile /** GET /me (profile.ts). Needs a clinician session, not the step-up. */ export type Me = { clinician: Clinician & { lapsed: boolean }; session: { stepUpDone: boolean; locked: boolean; idleLockMinutes: number; startedAt: string; endsAt: string }; secondFactor: { totp: boolean }; features: { realPatients: boolean; invites: boolean; checkIns: boolean }; }; export type Clinician = { id: string; email: string; /** As on the register: "Dr Emeka Okafor". */ fullName: string; title: string | null; /** How Yasmar says it: "Doctor Okafor". */ displayName: string; pronoun: Pronoun | null; profession: "doctor" | "pharmacist"; licenceBody: string; licenceNumber: string; countryCode: string; mobilePhone: string | null; status: "verified" | "suspended" | "closed"; verifiedAt: string | null; verificationExpiresAt: string; idleLockMinutes: number; termsVersion: string | null; termsAcceptedAt: string | null; dpaVersion: string | null; dpaAcceptedAt: string | null; }; /** PATCH /me (strict; needs the step-up). */ export type MeChange = Partial<{ title: string | null; displayName: string; pronoun: Pronoun | null; mobilePhone: string | null; idleLockMinutes: 15 | 30 | 60 }>; /** POST /me/terms. */ export type TermsBody = { termsVersion: string; dpaVersion: string }; /** GET /step-up. */ export type StepUpState = { totpEnrolled: boolean; recoveryCodesLeft: number; stepUpDone: boolean; locked: boolean; pausedUntil: string | null }; /** POST /step-up/totp (201). */ /** qrSvg is drawn by the backend (src/clinician/qr.ts), never by a third party. */ export type TotpStart = { secret: string; otpauthUrl: string; qrSvg?: string }; /** POST /step-up/totp/confirm. */ export type TotpConfirmed = { stepUpDone: true; recoveryCodes: string[] }; /** POST /step-up/verify. */ export type StepUpVerified = { stepUpDone: true; method: "totp" | "recovery_code" }; export type EmergencyNumber = { number: string; label: string; source: "clinic" | "registry"; testDialledAt: string | null; registryRevision?: number | null }; /** GET /clinic, PATCH /clinic: { clinic }. */ export type Clinic = { id: string; name: string; addressLine: string | null; city: string | null; region: string | null; countryCode: string; timezone: string; phone: string | null; openingHours: WeeklyHours | null; alertWatchHours: WeeklyHours | null; backupAlertName: string | null; backupAlertPhone: string | null; backupAlertEmail: string | null; version: number; updatedAt: string; role: "owner" | "member"; emergencyNumbers: EmergencyNumber[]; }; /** PATCH /clinic (strict; owner only; needs the step-up). */ export type ClinicChange = Partial<{ expectedVersion: number; name: string; addressLine: string | null; city: string | null; region: string | null; timezone: string; phone: string | null; openingHours: WeeklyHours; alertWatchHours: WeeklyHours; backupAlertName: string | null; backupAlertPhone: string | null; backupAlertEmail: string | null; emergencyNumbers: { number: string; label: string; testDialledAt: string | null }[] | null; }>; // ---------------------------------------------------------------- patients /** sharedBy and withheldSome came with the tile (SHR-1); older backends left them out. */ export type Sharing = { status: "connected" | "paused" | "stopped"; scopes: T.ShareScope[]; allowsCheckInCalls: boolean; changedAt: string; sharedBy?: string[]; withheldSome?: boolean }; /** GET /patients { items, asOf } and POST /patients/search (workspace/patients.ts listItem). */ export type PatientItem = { id: string; kind: "clinic_added" | "connected"; clinicId: string; displayName: string; dateOfBirth: string | null; birthYear: number | null; ageYears: number | null; ageRecordedOn: string | null; sex: "female" | "male" | "other" | null; shareId: string | null; lastActivityAt: string; createdAt: string; name: string; age: number | null; attention: { urgent: boolean; unreadNotes: number; newSinceLastCheck: boolean }; lastCheckId: string | null; lastCheckAt: string | null; newInformation: { since: string; items: string[] } | null; sharing: Sharing | null; medicines: { count: number } | null; doses: { taken: number; expected: number; onTime: number } | null; lastCheckIn: T.LastCheckIn | null; }; export type PatientList = { items: PatientItem[]; asOf: string }; /** GET /patients/:id { patient } (workspace/patients.ts detailOf). */ export type PatientDetail = PatientItem & { firstName: string; dateOfBirthIso: string | null; identityOnFile: "date_of_birth" | "birth_year" | "none"; timeZone: string; hasOwnAccount: boolean; phone: string | null; pronoun: Pronoun | null; preferredTitle: string | null; activeCheckInId: string | null; }; /** POST /patients (strict). 201 { patient } is the bare row, not the detail. */ export type NewPatientBody = { displayName: string; dateOfBirth?: string; ageYears?: number; sex?: "female" | "male" | "other"; attestation: { careRelationship: true; patientAgreement: true; version: string }; }; export type ActivityList = { events: { id: string; at: string; actor: string; actorKind: "clinician"; action: string }[] }; /** POST /patients/:id/viewed. */ export type Viewed = { lastLookedAt: string | null }; // ---------------------------------------------------------------- shared from MARMAR Health (sharing/shared-data.ts) /** GET /patients/:id/shared: the tile. */ export type SharedSummary = { asOf: string; sharing: { status: "connected"; sharedBy: string[]; scopes: T.ShareScope[]; allowsCheckInCalls: boolean; withheldSome: boolean; changedAt: string }; medicines: { count: number } | null; doses: { taken: number; expected: number; onTime: number } | null; lastCheckIn: T.LastCheckIn | null; phone: string | null; }; export type SharedMedicines = T.SharedMedicines; export type SharedDoses = T.SharedDoses & { complete: boolean }; export type SharedCheckIns = Omit & { requested: CheckInListItem[] }; export type SharedRecords = T.SharedRecords; // ---------------------------------------------------------------- invitations (sharing/invitations.ts) export type Invitation = { id: string; patientName: string; patientAge: number | null; withPerson: { name: string; relation: string } | null; scopes: T.ShareScope[]; allowsCheckInCalls: boolean; withheldSome: boolean; sentAt: string; expiresAt: string; possibleMatch: { patientId: string; name: string; dateOfBirth: string | null } | null; }; export type InvitationList = { invitations: Invitation[] }; export type Accepted = { patientId: string; shareId: string }; /** POST /invitations/:id/decline (strict): not_for_context is stored as not_using_context. */ export type DeclineBody = { reason?: "not_my_patient" | "not_for_context" | "other" | null; note?: string | null }; // ---------------------------------------------------------------- records (workspace/records.ts) export type Record_ = { id: string; patientId: string; title: string; type: T.RecordType; place: string | null; recordDate: string | null; addedBy: string; addedAt: string; /** "clarification" records read as "pasted". */ origin: T.RecordOrigin; fileName: null; contentType: string | null; sizeBytes: number | null; pages: number | null; reading: "queued" | "reading" | "read" | "failed"; readingError: string | null; readingErrorCode: string | null; lineCount: number; heldBack: { reason: string } | null; clarification: { clarificationId: string } | null; }; export type Records = { records: Record_[] }; export type UploadTicket = T.UploadTicket; /** POST /patients/:id/records (strict). */ export type NewRecordBody = { text?: string | null; uploadId?: string | null; title?: string; type: T.RecordType; place?: string | null; recordDate?: string | null; origin: "upload" | "pasted" | "camera"; }; // ---------------------------------------------------------------- reading and review (workspace/review.ts extractionView) export type Extraction = Draft & { id: string; patientId: string; revision: number; createdAt: string; records: { recordId: string; status: "queued" | "reading" | "read" | "failed" | "held_back"; error: string | null; inReading: boolean }[]; decisions: Record; coverageReview: Record; acknowledgedOriginals: boolean; confirmedHardToRead: string[]; stale: boolean; recordsNotReady: { recordId: string; title: string; status: string }[]; coverage?: (CoverageCandidate & { kind: "medicine" | "allergy" | "kidney" })[]; }; /** PUT /extractions/:id/decisions (strict). */ export type DecisionsBody = { revision: number; decisions: Record; coverageReview: Record; acknowledgedOriginals: boolean; confirmedHardToRead: string[]; }; // ---------------------------------------------------------------- the check (workspace/checks.ts checkView) export type CheckRun = Omit & { assessment: Assessment; summary: { before: number; after: number; added: number; headline: string; subline: string }; profile: unknown[]; medicines: unknown[]; sourceLines: { recordId: string; recordTitle: string; page: number | null; line: number; text: string; hardToRead: boolean }[]; }; export type CheckRunList = T.CheckRunList; export type FindingMark = T.FindingMark; // ---------------------------------------------------------------- check-ins (check-ins/*) /** GET /patients/:id/check-ins/plan (check-in-plan.ts planTemplate). */ export type PlanTemplate = Omit & { /** Older servers leave it out; 10. */ maxQuestions?: number; window: { patientTimeZone: string | null; clinicTimeZone: string; clinicHours?: WeeklyHours; alertWatchHours?: WeeklyHours; dayStart?: string; dayEnd?: string; /** null: the patient turned quiet hours off. */ quietHours: { start: string; end: string } | null; blockedTimes?: { day: number | null; start: string; end: string; label?: string }[]; defaultExpiryHours: number; maxExpiryHours: number; }; limits: T.CheckInLimits | null; }; export type CheckInStatus = T.CheckInStatus; export type CheckInNote = T.CheckInNote & { validation: "pending" | "matched" | "unmatched" }; /** GET /check-ins/:id (check-in-clinician.ts checkInForClinician). */ export type CheckIn = { id: string; patientId: string; patientName: string; requestedBy: string; requestedAt: string; /** linkedTiers: the tiers of the linked findings (older servers leave it out). */ plan: Omit & { linkedTiers?: string[] }; script: { opening: string; identity: string[]; consent: string; feeling: string; alwaysLast: string; closing: string[]; protocolVersion: string }; status: CheckInStatus; statusAt: string; history: T.StatusChange[]; window: { start: string; end: string }; later: { start: string; end: string } | null; urgent: boolean; urgentAcknowledgedAt: string | null; notes: CheckInNote[]; summary: T.CheckInSummary | null; transcript: T.TranscriptLine[]; /** The alerts already sent (urgent sent, escalated, not reached), so a reload keeps them. */ alerts?: { id: number; at: string; kind: string | null; message: string | null }[]; lastEventId: number; }; export type CheckInListItem = T.CheckInListItem; export type CheckInList = { checkIns: CheckInListItem[] }; export type CheckInEvent = | { id: number; type: "status"; at: string; status: CheckInStatus; detail: string | null; later: { start: string; end: string } | null } | { id: number; type: "note" | "note_updated"; at: string; note: CheckInNote } | { id: number; type: "transcript"; at: string; line: T.TranscriptLine } | { id: number; type: "summary"; at: string; summary: T.CheckInSummary } | { id: number; type: "urgent"; at: string; noteId: string; reason: string } | { id: number; type: "alert"; at: string; kind: string | null; message: string | null }; export type CheckInUpdates = { events: CheckInEvent[]; lastEventId: number }; /** POST /check-ins/:id/notes/:noteId/add-to-check. */ export type AddedToCheck = { note: CheckInNote; recordId: string }; /** POST /check-ins/:id/add-to-check { noteIds }: one record from the call holds every chosen note. */ export type AddedNotesToCheck = { recordId: string; notes: CheckInNote[]; note: CheckInNote }; export type AlertList = { alerts: T.UrgentAlert[] }; export type QuestionCheck = T.QuestionCheck; // ---------------------------------------------------------------- errors /** Every error: { code, message }, with details on 400 VALIDATION_ERROR and extra fields on some sharing errors. */ export type ErrorBody = { code: string; message: string; details?: { path: string; message: string }[]; eligibility?: string; phone?: string | null };