// 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<T.SharedCheckIns, "requested"> & { 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<string, "include" | "exclude">;
coverageReview: Record<string, { fingerprint: string; choice?: "not_current" | "not_relevant"; reason: string; confirmedBy: string }>;
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<string, "include" | "exclude">;
coverageReview: Record<string, { choice?: "not_current" | "not_relevant"; reason?: string; fingerprint?: string; confirmedBy?: string }>;
acknowledgedOriginals: boolean;
confirmedHardToRead: string[];
};
// ---------------------------------------------------------------- the check (workspace/checks.ts checkView)
export type CheckRun = Omit<T.CheckRun, "assessment" | "summary"> & {
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<T.CheckInPlanTemplate, "window" | "limits" | "maxQuestions"> & {
/** 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<T.CheckIn["plan"], "linkedTiers"> & { 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 };