import type { Appointment, Organization, Task } from "fhir/r4"; import { ChevronRight } from "lucide-react"; import { OrgBadge } from "../ui/org-badge.tsx"; import { DueLine, RowStatus, type RowStatusTone } from "../ui/row-meta.tsx"; import { isResumableTaskStatus } from "../../services/TaskService.tsx"; import { organizationNameOf } from "../../utils/patientOrganizations.ts"; import { formatGroupDay, formatGroupVisit } from "../../utils/patientTaskGroups.ts"; import type { PacketSummary, PacketSummaryMember } from "../../services/QuestionnairePacketService.ts"; export { packetOfTask } from "../../utils/packetMembers.ts"; /** * The loaded Task a member opens. A staff reader whose summary carries no task ids (the desk) still reaches the form * it has already loaded, by the member Task name; an unconfirmed form, or any form for a patient, is never matched. */ export const memberTask = (packet: PacketSummary, member: PacketSummaryMember, tasks: Task[], forPatient: boolean): Task | undefined => { if (member.taskId) return tasks.find((t) => t.id === member.taskId); if (forPatient || member.state === "unavailable") return undefined; return tasks.find((t) => t.id === `${packet.id}-m${member.position}`); }; /** The loaded Task a member row actually opens, if any: the one rule for both the package and the rows it replaces. */ export const memberDoor = (packet: PacketSummary, member: PacketSummaryMember, tasks: Task[], forPatient: boolean): Task | undefined => { const task = memberTask(packet, member, tasks, forPatient); return task && memberWord(member, task, forPatient).opens ? task : undefined; }; type MemberWord = { word: string; tone: RowStatusTone; edge: "open" | "done" | "ended" | "plain"; opens: boolean }; // Only a submitted form is done. An unconfirmed one keeps its place and its count, and opens nothing. const memberWord = (member: PacketSummaryMember, task: Task | undefined, forPatient: boolean): MemberWord => { if (member.state === "submitted") return { word: "Done", tone: "done", edge: "done", opens: Boolean(task) }; if (member.state === "withdrawn") return { word: "Withdrawn", tone: "ended", edge: "ended", opens: Boolean(task) }; if (member.state === "unavailable") return { word: "Unavailable", tone: "plain", edge: "plain", opens: false }; if (task && isResumableTaskStatus(task)) { return { word: forPatient ? "Continue" : "In progress", tone: "attention", edge: "open", opens: true }; } return { word: "Not started", tone: "plain", edge: "plain", opens: Boolean(task) }; }; const dueText = (packet: PacketSummary, appointments: Appointment[]): string | undefined => { const visit = packet.due?.appointmentId ? appointments.find((a) => a.id === packet.due?.appointmentId) : undefined; if (visit?.start) return `Due by: your visit on ${formatGroupVisit(visit.start)}`; if (packet.due?.appointmentId) return "Due by: your visit"; if (packet.due?.date) return `Due by: ${formatGroupDay(packet.due.date.slice(0, 10))}`; return undefined; }; const unavailableNote = (count: number) => (count === 1 ? "One form in this packet can't be shown right now and is not counted as submitted." : `${count} forms in this packet can't be shown right now and are not counted as submitted.`); /** * The patient's assigned packets, each named once with its forms in their assigned order and how many are submitted. * A form opens through the same task screen as any other; nothing here decides whether it may be answered. */ const PatientPacketPackages = ({ packets, tasks, appointments, practices, forPatient, onOpenTask }: { packets: PacketSummary[]; tasks: Task[]; appointments: Appointment[]; practices: Organization[]; forPatient: boolean; onOpenTask: (task: Task) => void; }) => ( <> {packets.map((packet) => { const practice = packet.organizationId ? organizationNameOf({ reference: `Organization/${packet.organizationId}` }, practices) : undefined; const due = packet.lifecycle === "open" ? dueText(packet, appointments) : undefined; const members = [...packet.members].sort((a, b) => a.position - b.position); return (

{packet.title || "Forms from your practice"}

{`${packet.progress.submitted} of ${packet.progress.total} submitted`} {(practice || due || packet.lifecycle === "cancelled") && (

{practice && <>{practice}} {packet.lifecycle === "cancelled" ? Cancelled by the practice : due && }

)}
{packet.progress.unavailable > 0 && (

{unavailableNote(packet.progress.unavailable)}

)} {members.map((member) => { const task = memberDoor(packet, member, tasks, forPatient); const shown = memberWord(member, memberTask(packet, member, tasks, forPatient), forPatient); const title = member.title || `Form ${member.position}`; const edge = shown.edge === "plain" ? "" : ` ak-card--${shown.edge}`; return shown.opens && task ? ( ) : (

{title}

{shown.word}
); })}
); })} ); export default PatientPacketPackages;