import { EmptyState } from "../../../../../components/ui/empty-state"; import { useNavigate } from "react-router-dom"; import type { Organization, Task } from "fhir/r4"; import { DUE_BY_NEXT_APPOINTMENT, DUE_BY_UNSCHEDULED_NEXT_APPOINTMENT, getAkashicTaskKind, getAppointmentOrganizationDisplay, getAppointmentPractitionerDisplay, getCustomTaskDueDisplay, getCustomTaskDueMode, hasPatientFacingTaskScreen, isClosedTask, isCustomTask, isExceptionalTaskStatus, isOutstandingTask, isPatientsOwnTask, isResumableTaskStatus, resolveTaskAppointment, useAuthServiceContext, usePatientServiceContext, useTaskServiceContext, } from "../../../../../services"; import { CustomTaskAssignmentModal, CustomTaskDetails, PacketAssignmentModal, QuestionnaireTaskAssignmentModal, FhirTaskDisplay } from "../../../../../components"; import { useFhirTaskBroadcast, useFhirTasks, useFhirTaskTargetedBroadcast, useFhirAppointments, useFhirPractitioners, } from "../../../../../hooks"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import PatientSetupTasks from "./PatientSetupTasks.tsx"; import PatientTaskDialog from "../../../../../components/tasks/PatientTaskDialog.tsx"; import PatientReferralDialog from "../../../../../components/tasks/PatientReferralDialog.tsx"; import { usePatientReferrals } from "../../../../../hooks/usePatientReferrals.ts"; import { referralDay, type PatientReferral } from "../../../../../utils/patientReferrals.ts"; import { isStandingPlanInstruction } from "../../../../../utils/planActivity.ts"; import PatientPacketPackages, { memberDoor, packetOfTask } from "../../../../../components/tasks/PatientPacketPackages.tsx"; import { NO_PACKETS, PacketSummaryReader, type PatientPacketSummaries } from "../../../../../hooks/usePatientPacketSummaries.ts"; import { Check, ChevronRight, Edit, FileCheck, FileText, Plus } from "lucide-react"; import "./PatientTasks.css"; import { DueLine, RowPriority, RowStatus, priorityWord } from "../../../../../components/ui/row-meta.tsx"; import { Skeleton } from "../../../../../components/ui/skeleton.tsx"; import { OrgBadge } from "../../../../../components/ui/org-badge.tsx"; import { useMyOrganizations } from "../../../../../hooks/useMyOrganizations.tsx"; import { appointmentOrgRef, organizationNameOf } from "../../../../../utils/patientOrganizations.ts"; import type { PractitionerWithRoles } from "../../../../../services/PractitionerService.tsx"; // `taskDisplayTitle` - ONE title function, shared with the appointment card's task list. Two surfaces naming // the same task differently is the defect; a shared resolver is the only thing that stops it recurring. import { taskDisplayTitle } from "../../../../../utils/appointmentTasks.ts"; // WHERE the visit is, from the helper that already answers it. It anchors on the `Location/` // prefix rather than a substring, and it already falls back to the organization when an // appointment names no site - which is exactly the second choice this subline wants, and a // second implementation would be a second answer to one question. import { getAppointmentLocationName } from "../../../../../utils/appointmentLocation.ts"; import { formatGroupDay, formatGroupVisit, groupPatientTasks, isAbandonedVisit, type PatientTaskGroup, } from "../../../../../utils/patientTaskGroups.ts"; import { readsForAnother, visitListProvesNoneBooked } from "../../../../../utils/visitListProvesNoneBooked.ts"; import { Button } from "../../../../../components/ui/button.tsx"; import { Sheet, SheetContent } from "../../../../../components/ui/sheet.tsx"; import { Tabs, TabsList, TabsTrigger, } from "../../../../../components/ui/tabs.tsx"; import { DropdownMenu, DropdownMenuItem, } from "../../../../../components/ui/dropdown-menu.tsx"; export type TaskAssignmentKind = "questionnaire" | "packet" | "custom"; // The patient's practices and the clinicians' roles, read only where a patient is reading, so the // staff chart never asks. const PatientPracticesReader = ({ onRead }: { onRead: (read: { organizations: Organization[]; practitioners: readonly PractitionerWithRoles[] }) => void; }) => { const { organizations } = useMyOrganizations(); // LOADED HERE, not borrowed: on a cold direct entry nothing else has read the clinicians' roles, and a // visit naming only its clinician lost its mark. The provider keeps what it read, so this asks once. // Pending or failed leaves the list empty, and an empty list marks nothing rather than guessing. const { practitionersList } = useFhirPractitioners(); useEffect(() => { onRead({ organizations, practitioners: practitionersList ?? [] }); }, [organizations, practitionersList, onRead]); return null; }; const PatientTasks = ({ isHomePage = false, embedded = false, onOpenTask, onStartAssignment, }: { isHomePage?: boolean; elevated?: boolean; // When embedded in the workspace modal: drill into a task in-place via onOpenTask (no nested Sheet/overlay), // and hand assignment up via onStartAssignment so the host drills into the form the same way. // The WP-shaped assignment MODALS still cannot portal correctly inside the Dialog - that is why the host // renders the same forms inline, and why this component never opens one while embedded. embedded?: boolean; onOpenTask?: (task: Task) => void; onStartAssignment?: (kind: TaskAssignmentKind) => void; }) => { const navigate = useNavigate(); const { userRole, USER_ROLES, actingAs } = useAuthServiceContext(); const { patientState } = usePatientServiceContext(); const { getTaskStatusDisplay } = useTaskServiceContext(); const { loading, error, tasksList, mayBeIncomplete, recheck } = useFhirTasks(); const appointmentRead = useFhirAppointments(); const { appointmentsList } = appointmentRead; const [selectedTask, setSelectedTask] = useState(null); const [showDisplayModal, setShowDisplayModal] = useState(false); // Each opening of the sheet is numbered; a close kept by an earlier opening never closes a later one. const openingRef = useRef(0); const [opening, setOpening] = useState(0); // The patient's own task, open over the list; a long form hands off from there to its page. const [dialogTaskId, setDialogTaskId] = useState(); const [showCustomTaskModal, setShowCustomTaskModal] = useState(false); const [showQuestionnaireTaskModal, setShowQuestionnaireTaskModal] = useState(false); const [showPacketModal, setShowPacketModal] = useState(false); const [taskTab, setTaskTab] = useState<"outstanding" | "completed">( "outstanding", ); /** * WHAT THE SETUP GROUP BELOW IS ALREADY SHOWING, so neither tab's empty state can deny it. * * Both halves, because both tabs speak: Outstanding says nothing needs the patient, Completed says * nothing has been finished, and the group below can contradict either one. * * Undefined until the derived reads answer. The host never computes these itself - one computation, * asked by whoever needs it, is the whole reason `usePatientSetupSummary` exists. */ const [setupOutstanding, setSetupOutstanding] = useState(); const [setupCompleted, setSetupCompleted] = useState(); const handleSetupCounts = useCallback( ({ outstanding, completed }: { outstanding: number; completed: number }) => { setSetupOutstanding(outstanding); setSetupCompleted(completed); }, [], ); useFhirTaskBroadcast(); useFhirTaskTargetedBroadcast({ task: selectedTask || undefined, isOpen: showDisplayModal, onUpdate: (updatedTask) => { setSelectedTask(updatedTask); }, onDelete: () => { setShowDisplayModal(false); setSelectedTask(null); }, }); const isPatientOrProxy = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes( userRole as string, ); const noneBookedIsKnown = visitListProvesNoneBooked(appointmentRead, readsForAnother({ userRole, USER_ROLES, actingAs })); // Spencer, 2026-08-25, reading his own Tasks page on a phone: the shell one line above already // says "My Tasks", so a second heading here names the page twice - and it was the doubling that made // him think he was on a different screen entirely. A patient reaches this component only through that // shell (the routed `/dashboard/patient/tasks`), so on that path the heading is pure repetition. // Staff keep theirs: the chart's shell names the PATIENT, not the section, so "Patient Tasks" is // the only thing naming this panel there. And a patient is never authorized to assign, so hiding the // heading empties this row rather than leaving a control stranded in it. const namedByShell = isPatientOrProxy && !embedded; // Outstanding/Completed tabs on every patient-task view - the patient's own Tasks page, the // practitioner chart, and the embedded receptionist-workspace modal. const showTaskTabs = true; // THE AUDIENCE FILTER, and it is the only one of the four that is conditional. This component is // mounted for BOTH the patient's own Tasks page and the staff chart's Tasks section, so the referral // tasks that must not reach a patient are exactly the ones a coordinator needs to see - the chart is // where they are worked. Gated on the reader, not on the task. // A packet's forms are listed once, under the packet, after the server has said what it holds and how far it is. // Until then, or if that read fails, they stay in the list as ordinary forms, so none goes missing. With no packet // form in the list there is nothing to group, and nothing is asked. const packetRefresh = useMemo( () => tasksList.filter((task: Task) => packetOfTask(task)) .map((task: Task) => `${task.id}:${task.status}:${task.meta?.versionId ?? ""}`).join(","), [tasksList], ); const [packetRead, setPacketRead] = useState(NO_PACKETS); const summarisedPackets = useMemo( () => new Set((packetRead.packets ?? []).map((packet) => packet.id)), [packetRead.packets], ); const packetsShown = (packetRead.packets ?? []).filter((packet) => (taskTab === "completed" ? packet.lifecycle !== "open" : packet.lifecycle === "open")); const openPacketDueAt = (appointmentId: string | undefined) => Boolean(appointmentId) && (packetRead.packets ?? []).some((packet) => packet.lifecycle === "open" && packet.due?.appointmentId === appointmentId); // A form its package opens is listed only there. A staff reader keeps the row of any form the package cannot open, // so no door they had is lost; a patient sees an unconfirmed form only as its package says, never as done. const packagedTaskIds = useMemo(() => new Set((packetRead.packets ?? []).flatMap((packet) => packet.members .map((member) => memberDoor(packet, member, tasksList, isPatientOrProxy)?.id) .filter((id): id is string => Boolean(id)))), [packetRead.packets, tasksList, isPatientOrProxy]); const audienceTasks = useMemo( () => (isPatientOrProxy ? tasksList.filter((task: Task) => isPatientsOwnTask(task) && !isStandingPlanInstruction(task)) : tasksList).filter((task: Task) => !summarisedPackets.has(packetOfTask(task) ?? "") || (!isPatientOrProxy && !packagedTaskIds.has(task.id ?? ""))), [isPatientOrProxy, tasksList, summarisedPackets, packagedTaskIds], ); // A plan's standing instruction is how the patient lives, not a job with an end, so it is listed // apart from the tasks and counted in none of their groups (planActivity). const standingInstructions = useMemo( () => (isPatientOrProxy ? tasksList.filter((task: Task) => isPatientsOwnTask(task) && isStandingPlanInstruction(task) && isOutstandingTask(task)) : []), [isPatientOrProxy, tasksList], ); // The patient's referrals come from their orders, not from the coordinator's referral Task above. const { referrals, markBooked, declineInvite, failed: referralsFailed } = usePatientReferrals(patientState?.id, isPatientOrProxy && !embedded); const [openReferralId, setOpenReferralId] = useState(); const [practices, setPractices] = useState([]); const [clinicians, setClinicians] = useState([]); const readPractices = useCallback( ({ organizations, practitioners }: { organizations: Organization[]; practitioners: readonly PractitionerWithRoles[] }) => { setPractices(organizations); setClinicians(practitioners); }, [], ); const referralRows = referrals.filter((referral) => taskTab === "completed" ? referral.stage === "booked" : referral.stage !== "booked"); const referralsDone = referrals.filter((referral) => referral.stage === "booked").length; const referralSubline = (referral: PatientReferral) => { if (referral.stage === "booked") return "Booked"; const day = referralDay(referral.authoredOn); return [referral.referredBy && `Referred by ${referral.referredBy}`, day].filter(Boolean).join(", "); }; const filteredTasks = useMemo(() => { if (showTaskTabs) { return audienceTasks.filter( taskTab === "completed" ? isClosedTask : isOutstandingTask, ); } return isHomePage ? audienceTasks.filter((task: Task) => task.status !== "completed") : audienceTasks; }, [showTaskTabs, taskTab, audienceTasks, isHomePage]); // ORDER MATTERS HERE AND IT IS NOT A STYLE POINT. These were written above `filteredTasks` and // that threw "Cannot access 'O' before initialization" on the patient's own Tasks page - a blank // screen where their list should be. A useMemo CALLBACK is lazy, but its DEPENDENCY ARRAY is // evaluated at the declaration, so naming `filteredTasks` there reaches into the temporal dead // zone. `tsc` cannot see it and neither can a suite that never renders this component as a // patient. Anything deriving from `filteredTasks` belongs after it. /** * THE GROUPS THE PATIENT'S LIST IS DRAWN IN, and the counts that go beside them. * * `taskGroups` arranges the CURRENT TAB. `groupTotals` counts over every task in the audience, * both tabs - the deck's headings read "1 of 3 done", and a count taken from the filtered list * could only ever say "0 of 2" on the Outstanding tab. The count is the thing that stops a group * hiding work: it is a statement about the visit, not about what is currently on screen. */ /** * A VISIT YOU HAVE FINISHED SAYS SO, INSTEAD OF DISAPPEARING. * * Groups are built from the tasks the current tab shows, so a visit whose items are all done had no * group at all - which makes "you have finished everything for this visit" and "there is no such * visit" the same state on screen, at the one moment a patient most wants to be told they are done. * * So on Outstanding the groups are built from the whole audience and the finished ones are kept, * rendering their heading with a ready line in place of rows. * * ONLY FUTURE VISITS, and only visits. A past visit's heading already says "still needed from your * visit on ...", so a past visit with nothing still needed has nothing to say and goes. A date group * is a deadline rather than an occasion - there is no event to be ready for - so an empty one is * simply absent, exactly as before. * * AND ONLY VISITS THAT ARE STILL GOING TO HAPPEN. Measured on the re-walk: a cancelled * appointment, its tasks correctly voided, was drawn with nothing left and therefore said "You're * ready for this visit" - directly above the identical heading for the visit that IS happening, * which had four things outstanding. See `isAbandonedVisit`. */ const taskGroups = useMemo(() => { const shown = groupPatientTasks(filteredTasks, appointmentsList ?? []); if (taskTab !== "outstanding") return shown; const now = Date.now(); return groupPatientTasks(audienceTasks, appointmentsList ?? []) .map((group) => shown.find((s) => s.key === group.key) ?? { ...group, tasks: [] }) .filter((group) => group.tasks.length > 0 || (group.kind === "appointment" && Boolean(group.appointment?.start) && !isAbandonedVisit(group.appointment) && new Date(group.appointment!.start as string).getTime() >= now), ); }, [filteredTasks, audienceTasks, appointmentsList, taskTab]); const groupTotals = useMemo(() => { const totals = new Map(); for (const group of groupPatientTasks(audienceTasks, appointmentsList ?? [])) { const countable = group.tasks.filter((task) => task.status !== "cancelled"); totals.set(group.key, { total: countable.length, done: countable.filter(isClosedTask).length, cancelledOnly: group.tasks.length > 0 && countable.length === 0, }); } return totals; }, [audienceTasks, appointmentsList]); /** * WHAT A ROW IS, IN THE THREE STATES THIS SCREEN ACTUALLY HAS. * * It had two, and `isClosedTask` was doing the work of both - a predicate answering a DIFFERENT * question stretched over this one. It means *not actionable*, so a CANCELLED task took the done * arm and wore the success green. Walked on the mount: thirteen of the fifteen rows on the * Completed tab were cancelled, every one edged in success green with a filled red pill beside it, * so a tab of things that are over read as a wall of alarm. * * THE THIRD STATE IS THE ONE THE DECK NAMES AND EVERY READING OF IT MISSED: a cancelled task * RECEDES. Red, a coloured edge and a place at the top are all ways of saying LOOK AT THIS, and * this is the one row that wants none of them - it is over, and it needs to stay findable without * competing with the things that are not. */ const rowState = (task: Task): "ended" | "done" | "open" => { if (isExceptionalTaskStatus(task)) return "ended"; return isClosedTask(task) ? "done" : "open"; }; // The edge follows the word the row shows (Spencer, 2026-09-01): a plain outstanding row has // no word, so it keeps the transparent edge the deck draws. const rowEdge = (task: Task): "ended" | "done" | "open" | "plain" => { const state = rowState(task); if (state !== "open") return state; return isResumableTaskStatus(task) ? "open" : "plain"; }; // A row outside a visit group names its practice, as the grouped deck draws it; unknown names nobody. const rowPractice = (task: Task) => { const practice = organizationNameOf(task.owner, practices); if (!practice) return undefined; return (

{practice}

); }; // What a row's due line adds to its heading. A date group's heading is the date and "No deadline" // says there is none, so the patient's row keeps only what the heading does not: the repeat, or a // visit it is tied to. Staff read one flat list and keep the whole line. const dueLineOf = (task: Task) => getCustomTaskDueDisplay(task, resolveTaskAppointment(task, appointmentsList), Date.now(), noneBookedIsKnown); // The subline claims no visit is booked only when every row beneath it would say so itself. const groupIsUnscheduled = (group: PatientTaskGroup) => group.kind === "next-appointment" && group.tasks.every((task) => dueLineOf(task) === `Due by: ${DUE_BY_UNSCHEDULED_NEXT_APPOINTMENT}`); const rowDueText = (task: Task, group: PatientTaskGroup): string | undefined => { if (task.status === "cancelled") { const when = task.lastModified ?? task.meta?.lastUpdated; const date = when ? new Date(when) : undefined; const day = date && !Number.isNaN(date.getTime()) ? new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric" }).format(date) : undefined; return day ? `Cancelled on: ${day}` : undefined; } const text = dueLineOf(task); // A closed task waits for no visit, so it drops the line that looks ahead to one. if (isClosedTask(task) && (text === `Due by: ${DUE_BY_UNSCHEDULED_NEXT_APPOINTMENT}` || text === "Due by: " + DUE_BY_NEXT_APPOINTMENT)) return undefined; if (!isPatientOrProxy) return text; // The heading and subline already say what this line would. if (group.kind === "next-appointment" && (groupIsUnscheduled(group) || text === "Due by: " + DUE_BY_NEXT_APPOINTMENT)) return undefined; const mode = getCustomTaskDueMode(task); if (mode === "recurring") { const repeat = text.match(/repeats.*$/)?.[0]; return repeat ? repeat.charAt(0).toUpperCase() + repeat.slice(1) : text; } if (group.kind === "date" && mode === "date") return undefined; if (group.kind === "undated" && text === "No due date") return undefined; return text; }; const groupCount = (group: PatientTaskGroup) => { const totals = groupTotals.get(group.key); if (!totals || totals.cancelledOnly) return ""; return `${totals.done} of ${totals.total} done`; }; /** * THE HEADING SAYS WHAT THE GROUP IS; THE SUBLINE SAYS WHICH ONE. * * Copy taken verbatim from the approved deck (`mockups/patient-portal.html`, "Grouped by what the * items share") rather than written here: these are three sentences a patient reads about their own * care, and the drawing is the place that decides them. * * "No deadline" rather than "No due date" is the one worth not tidying. It names the situation - * nobody has put a deadline on these - instead of naming an empty field, and those tasks are the * ones most likely to be forgotten. */ const groupTitle = (group: PatientTaskGroup) => { if (group.kind === "appointment" && group.appointment?.start) { const when = formatGroupVisit(group.appointment.start); // A PAST VISIT MUST NOT KEEP A FUTURE-SHAPED HEADING. "Everything for this visit" was vague // enough to survive a date that had passed; a bare date is not - a group headed "Sun, Sep 6" // sitting above Sep 9 tells a patient to prepare for something that already happened. // The item is still real (a cancelled visit VOIDS its tasks instead), so the group stays and // stops being preparation: it says the practice has not stopped wanting this, and where it // came from. return new Date(group.appointment.start).getTime() < Date.now() ? `Still needed from your visit on ${when}` : when; } if (group.kind === "appointment") return "This visit"; if (group.kind === "date" && group.dueDate) return `Due ${formatGroupDay(group.dueDate)}`; if (group.kind === "next-appointment") return "Due by your next appointment"; return "No deadline"; }; // A GROUP'S HEADING NAMES THE THING THAT MAKES IT A DIFFERENT GROUP FROM ITS NEIGHBOURS. // Everything else is subline. // // The heading used to read "Everything for this visit" on every visit group, which was right in // the drawing - where there is ONE - and says nothing on a real patient, where there were five. // Measured: five identical bold lines with every distinguishing fact underneath them. The date // and time are what tell one visit from the next, so they are the heading; the practitioner and // the appointment type describe it and go beneath. // // The rejected alternative is worth recording because it looks reasonable: keep the phrase when // there is only one group and use a date when there are several. That gives one group two // different names depending on how many siblings it happens to have that week - a patient learns // the phrase in October and meets a date in November. A heading that moves under the reader is // worse than one that is merely generic. const groupSubline = (group: PatientTaskGroup) => { if (group.kind === "next-appointment") { return groupIsUnscheduled(group) ?

Not scheduled yet

: undefined; } if (group.kind !== "appointment" || !group.appointment) return undefined; // WHERE, THEN WHO - AND NO APPOINTMENT TYPE. "Routine" is the practice's word for its own // scheduling and a patient does not prepare differently for one, so it earns no space here. // // THE PLACE RATHER THAN THE PRACTICE, WHICH REVERSES AN EARLIER RULING AND SHOULD. The ruling // was "practice then practitioner", and it asked for something the resource does not carry: // measured across 57 of one patient's appointments, NOT ONE has an Organization participant, // so the practice rendered as nothing at all and the subline was the practitioner alone. // The location is on the resource today, it is the question a visit line answers - which // building do I drive to - and it is the ONLY drawn field that differs between two visits at // the same minute with the same practitioner, which this patient has two pairs of. The // practice is not lost: the mark carries it, in colour, without spending a word. // // THE PRACTICE IS STILL THE SECOND CHOICE, never nothing. A subline that disappears takes the // group's only distinguishing fact with it. const organization = getAppointmentOrganizationDisplay(group.appointment); const where = getAppointmentLocationName(group.appointment, organization); // The mark comes from the visit's own practice or its clinician's role, never from the portal. const practice = organization ?? organizationNameOf(appointmentOrgRef(group.appointment, clinicians), practices); const practitioner = getAppointmentPractitionerDisplay(group.appointment); if (!where && !practitioner) return undefined; // AND THE PRACTICE'S MARK IN FRONT OF IT, the same square the patient's home queue puts beside // the same names. Shared rather than reimplemented: two tone functions would eventually // disagree, and one practice wearing two colours on two screens is the confusion the mark // exists to prevent. // // ONLY WHEN THE APPOINTMENT ACTUALLY NAMES ITS PRACTICE. Deriving one from the portal the // patient happens to be signed into would put a practice's colour on a visit that may belong // to another, which is worse than no mark: the mark's whole job is telling two practices // apart, and a wrong one does the opposite confidently. return (

{practice && } {[where, practitioner].filter(Boolean).join(" \u00b7 ")}

); }; const viewTask = (task: Task) => { // Embedded in the workspace modal: drill in-place (no nested Sheet/overlay). if (embedded && onOpenTask && task.id) { onOpenTask(task); return; } // The SECOND place that made this decision, and it got it wrong independently of the home queue. // Asking `isCustomTask` alone meant a patient opening an assigned QUESTIONNAIRE fell through to the // read-only Task Details dialog below - Task Status, Business Status, Owner - with no questions on it // and nothing to press to finish. `TaskPage` renders the questions and submits them; it always could. // Two routers, one shared predicate now, because the duplicated condition is what let the two // answers drift apart. if (isPatientOrProxy && hasPatientFacingTaskScreen(task) && task.id) { setDialogTaskId(task.id); return; } openingRef.current += 1; setOpening(openingRef.current); setSelectedTask(task); setShowDisplayModal(true); }; const closeTaskDetails = (closing?: number) => { if (closing !== undefined && closing !== openingRef.current) return; setShowDisplayModal(false); setSelectedTask(null); }; const editTask = (event: React.MouseEvent, task: Task) => { event.stopPropagation(); navigate(isHomePage ? `task/${task.id}` : (task.id as string), { relative: "path", }); }; const isAuthorizedToAssign = () => Boolean( userRole && [USER_ROLES.PRACTITIONER, USER_ROLES.RECEPTIONIST].includes( userRole as string, ), ); const isAuthorizedToEdit = () => Boolean( userRole && [USER_ROLES.PRACTITIONER].includes(userRole as string), ); const startAssignment = (kind: TaskAssignmentKind) => { // Embedded: the host owns the surface, so it drills into an inline form and keeps its own Back. if (embedded) { onStartAssignment?.(kind); return; } if (kind === "questionnaire") { setShowQuestionnaireTaskModal(true); return; } if (kind === "packet") { setShowPacketModal(true); return; } setShowCustomTaskModal(true); }; const renderContent = () => { return ( <> {/* An unread referral list is not an empty one: a patient told there is nothing to do would not book it. */} {!error && !loading && referralsFailed && (

We couldn't check your referrals just now. If a practice has referred you, it may not show here. Reload the page to try again.

)} {!error && !loading && referralRows.length > 0 && (

Referrals

{`${referralsDone} of ${referrals.length} done`}
{referralRows.map((referral) => ( ))}
)} {!error && !loading && packetRead.failed && (

We couldn't check packets just now, so their forms are listed one by one below. Reload the page to try again.

)} {!error && !loading && !packetRead.failed && !packetRead.complete && (

Some packets may not be listed here. Reload the page to check again.

)} {!error && !loading && packetsShown.length > 0 && ( )} {/* The list may be missing a task just assigned: its rows stay, and nothing below says it is empty or done. */} {!error && mayBeIncomplete && (
This list may not show a recent change yet.
)} {error ? ( window.location.reload()}> Retry } /> ) : !loading && !mayBeIncomplete && filteredTasks.length === 0 && referralRows.length === 0 && !referralsFailed && packetsShown.length === 0 ? ( /* THE SHARED EmptyState. "No Tasks found" is the sentence a failed search returns, and the capital T was a resource type showing through on a patient's own screen. THE TWO AUDIENCES STAY SPLIT, which is why the branch survives the conversion: the patient is told what a task IS and where they come from, because a new patient has never seen one. A clinician reading the same chart already knows, and needs the fact about THIS patient. */ ) : loading ? (
{[...Array(4)].map((_, index) => (
))}
) : ( filteredTasks.length !== 0 && (
{/* GROUPED FOR THE PATIENT, FLAT FOR STAFF, on the gate this file already uses a few lines above for the audience filter. The grouping is a ruling about the PATIENT's list; the chart is a working surface where a coordinator wants every row in one run. Same component, two readers, one discriminator. */} {(isPatientOrProxy ? taskGroups : [{ key: "all", kind: "undated" as const, tasks: filteredTasks }]).map((group) => (
{isPatientOrProxy && ( /* NO FILL BEHIND A GROUP HEADING. The deck has none, and a filled band on this screen reads as a control rather than a heading - the rows beneath it are pressable and it is not. */

{groupTitle(group)}

{/* THE COUNT IS OVER THE WHOLE GROUP AND NOT OVER THE TAB, which is why it is computed from the unfiltered list. "1 of 3 done" on the Outstanding tab is the point: it tells a patient that this visit has three jobs and one is already behind them, which a tab showing only the outstanding two cannot. */} {groupCount(group) !== "" && ( {groupCount(group)} )} {groupSubline(group)}
)} {group.tasks.length === 0 && !openPacketDueAt(group.appointment?.id) && ( /* The promise "everything for this visit" used to make, cashed at the moment it comes true rather than announced on every group that had not yet. */

)} {group.tasks.map((task: Task) => (
viewTask(task)} onKeyDown={(event: React.KeyboardEvent) => { // A key pressed on a control inside the row (Edit) belongs to that control. if (event.target !== event.currentTarget) return; if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); viewTask(task); }} >

{/* 🏷️ CARD 715 β€” this read `task.description || "Task name not available"`, so a questionnaire task with no description listed as that sentence and then opened as "Blood Pressure Panel". ⭐ The name was already on the task the whole time: `focus.display` carries the questionnaire's title, written at assignment. β›” Shared rather than a second fallback chain here β€” the appointment card's task list had one and this screen had another, which is how the same task could read two different ways on two screens of the same product. */} {/* A questionnaire is marked by its form icon; no task carries a type pill or a Custom label, and the type comes from the task's coding, never its title. */} {taskDisplayTitle(task)} {getAkashicTaskKind(task) === "questionnaire" && ( )}

{/* πŸ”» THE ORDINARY STATUS IS GONE. πŸ—£οΈ Spencer, 2026-08-25: *"The requested pill isn't consistently placed, isn't everything requested? I'm thinking that has zero value and can be dropped."* He is right on both counts β€” it sat in a wrapping flex beside the title, so a long name pushed it onto its own line, and the Outstanding tab already says every row in it is outstanding. ⭐ WHAT SURVIVES IS THE EXCEPTIONAL STATE. "Cancelled" and "Could not be completed" are facts the tab does NOT give, and a patient looking for a task that has gone away needs to see why. β›” "Requested", "Received", "Accepted", "In progress" and "Completed" all say what their tab already said. */} {/* THREE STATES WORTH A WORD, AND THE COLOUR IS WHICH ONE. INERT (red) is what the tab cannot say: a row the patient is scanning for things to DO that will never respond, and the reason. RESUMABLE (amber) is the one ordinary status the tab genuinely cannot say either - Outstanding is not a synonym for in-progress, it is its superset, and a half-answered questionnaire is picked up where a never-opened one is started. DONE (green) is a case the flat list never had: every heading on this screen claims "1 of 3 done", and the row is where the patient sees WHICH one. Without it the count is a number they cannot check. IT WAS SCOPED TO A VISIT GROUP AND THAT WAS TOO NARROW. Walked at 390 on the Completed tab: a finished task under "Due Monday, Jun 22" showed no word while a finished task under "Still needed from your visit on Mon, Jun 29" showed "Completed" - one state wearing a word and no word, three rows apart, on one screen. THE ARGUMENT WAS NEVER ABOUT APPOINTMENTS, IT WAS ABOUT A HEADING THAT COUNTS, and all three kinds count: a visit group, a due-date group and "No deadline" each print "N of M done". Scoping it to one of the three was reading the case it was found in as the rule. The 2026-08-25 ruling is untouched by this and still does its work one tab away: on Outstanding a closed row does not exist, so nothing here puts a word true of every row onto every row. THE WORD COMES FROM THE ONE VOCABULARY, never from a label written here. The deck draws "Submitted"; `statusLabel` says what this product calls a completed task, and `oneStatusVocabularyNoSecondMap` fails on the first surface that disagrees with itself. The deck says WHICH states are worth showing; it does not get to name them twice. AND IT IS TEXT, NEVER A FILLED PILL. The deck's rule above its status block: the status is text, not a token - filled pills made the state the loudest thing on a row, louder than the task itself, and three stacked down a list read as a column of buttons. Measured at thirteen stacked red pills on one tab, four times the count that rule was written against. THE COLOUR IS THE STATE - except on an ENDED row, where the word goes plain. That is the row meant to recede, and a coloured word on it is the edge coming back through the text. */} {(isExceptionalTaskStatus(task) || isResumableTaskStatus(task) || (isPatientOrProxy && isClosedTask(task))) && ( {getTaskStatusDisplay(task)} )}
{/* πŸ—“οΈ ONE DUE VOCABULARY FOR EVERY KIND OF TASK β€” Spencer's own ruling, and the questionnaire branch was breaking it twice over. It printed `formatDateForDisplay(task.authoredOn)`, which is the date the task was CREATED, with no "Due by:" in front of it. β‡’ A patient read a creation date as a deadline. πŸ“ Seen on the running app: "Lose 10 pounds β€” Due by: this visit" directly above "Complete questionnaire: Contact Us β€” Friday, August 21, 2026". ⚠️ The helper is named for custom tasks and is not limited to them: an ordinary due date falls straight through to "Due by: ", and a task with none says "No due date" rather than borrowing another date to show. */} {/* NOT INSIDE A VISIT GROUP, WHERE IT IS THE HEADING PRINTED AGAIN. The deck's rule for this screen is one sentence: a visit's heading states the practice, the practitioner and the time ONCE, so its rows carry only the task. Measured on a real patient, a row under the heading "Sun, Sep 6 at 7:00 PM" read "Due by: appointment on Sun, Sep 6, 2026 at 7:00 PM Β· Zztest Secondclinician Β· Walk-in" - the heading and its subline, in full, on every row of the group they already head. Hoisting a shared fact and then repeating it underneath is the repetition this grouping was built to end. A DATE GROUP HOISTS ITS DATE THE SAME WAY: the grouped deck draws its rows with the repeat or the practice and no date (rowDueText). A visit the task is tied to was never hoisted, so that is still the row's to say. */} {/* Priority rides at the right of the due line, apart from the status, and only an urgent task draws it - routine is every row and says nothing. */} {isPatientOrProxy && group.kind !== "appointment" && rowPractice(task)} {((rowDueText(task, group) && (group.kind !== "appointment" || task.status === "cancelled")) || priorityWord(task.priority)) && (
{rowDueText(task, group) && (group.kind !== "appointment" || task.status === "cancelled") && ( )}
)} {/* NOT ON THE PATIENT'S LIST - AND THE FIRST REASON THIS GAVE IS NOW FALSE, so it is corrected here rather than left to be inherited. It said the deck defines `.chip` and draws it ZERO times, making the suppression a rule with nothing to contradict it. Measured against `mockups/patient-portal.html` as it stands: of 233 screens, seven draw `.chip` - and those seven are patient "My Tasks" screens. The patient's task list is not where the deck avoids chips; it is the ONLY place the deck uses them, and `Routine` is among the labels it draws there. The claim may well have been true of the deck it was written against - "all nine frames" does not describe a deck of 233 screens - so this corrects a premise that expired rather than one made carelessly. WHAT SURVIVES IS THE SECOND ARGUMENT, and the suppression now rests on it alone until Spencer rules: whether the patient sees these changes what the screen says to them, which is a product decision and not a restyle. EACH ONE FAILS A TEST THIS SCREEN HAS ALREADY APPLIED TO ITSELF. `Routine` is `Task.priority` and is identical on every row here, which is exactly the argument that took the due line out of a visit group's rows. The kind is the deck's own caption on the frame beside this one: *three visits in one week - the case a real patient has, where naming the KIND names nothing.* THE CHART KEEPS THEM, and that is the same two-readers split this file already makes twice above: the deck governs the PATIENT's list, and a coordinator running down a chart wants the type on every row. Not a blanket ban either - a chip carrying something that VARIES and changes what somebody does is a different question. */}
{isAuthorizedToEdit() && !isCustomTask(task) && (
)}
))}
))}
) )} {taskTab === "outstanding" && !loading && !error && standingInstructions.length > 0 && (

Standing instructions

From your care plans. Nothing to tick off.

{standingInstructions.map((task: Task) => (

{task.description}

{task.basedOn?.[0]?.display && (

{`From ${task.basedOn[0].display}`}

)}
))}
)} {/* THE PATIENT'S OWN SETUP STEPS, AND THEY SIT OUTSIDE THE BRANCH ABOVE ON PURPOSE. A patient whose practices have asked for nothing still owes these, and the branch above renders an empty state instead of a list in exactly that case - so mounting this inside it would hide the four global items from the patient most likely to still owe them. PATIENT OR PROXY ONLY: these are the patient's own housekeeping, computed from the record they hold, and the chart is not where anybody works them. */} {isPatientOrProxy && !loading && !error && ( )} {!embedded && selectedTask && ( { if (!open) closeTaskDetails(); }} > {isCustomTask(selectedTask) ? ( // The body pads only top and bottom; the patient's dialog insets it, so the sheet has to.
) : ( closeTaskDetails(opening)} /> )}
)} ); }; return (
{isPatientOrProxy && !embedded && } {packetRefresh !== "" && } {!namedByShell && (

{isPatientOrProxy ? "Available Tasks" : "Patient Tasks"}

{isPatientOrProxy ? "Review and complete tasks assigned by your care team." : "Review questionnaires and Custom Tasks assigned to this patient."}

{isAuthorizedToAssign() && (!embedded || onStartAssignment) && (
} disabled={!patientState} > startAssignment("questionnaire")} > Assign Questionnaire startAssignment("packet")}> Assign Packet startAssignment("custom")}> Assign Custom Task
)}
)} {showTaskTabs && ( setTaskTab(value as "outstanding" | "completed") } > {/* πŸ—‚οΈ TWO EQUAL HALVES ACROSS THE FULL WIDTH, as the decks draw it. πŸ—£οΈ Spencer, 2026-08-25: *"The outstanding/completed should be tabs."* The primitive defaults to `w-fit`, which packs them into a small pill pair at the left β€” it reads as a filter chip rather than as the two halves of the list, and on a phone it wastes the row it is already taking. */} Outstanding Completed )} {renderContent()} {isPatientOrProxy && !embedded && ( { if (!next) setDialogTaskId(undefined); }} taskId={dialogTaskId} /> )} {isPatientOrProxy && !embedded && ( referral.id === openReferralId)} onClose={() => setOpenReferralId(undefined)} onMarkBooked={markBooked} onDeclineInvite={declineInvite} /> )} {patientState && !embedded && ( <> setShowQuestionnaireTaskModal(false)} /> {/* Mounted only while open: its packet and appointment reads belong to one opening. */} {showPacketModal && ( setShowPacketModal(false)} /> )} setShowCustomTaskModal(false)} /> )}
); }; export default PatientTasks; // import { useNavigate } from 'react-router-dom'; // import { FormProvider, useForm } from 'react-hook-form'; // import { QuestionnaireResponse, Task } from 'fhir/r4'; // import { // IValueSet, // useValueSetServiceContext, // useQuestionnaireServiceContext, // useFhirServiceContext, // useAuthServiceContext, // useTaskServiceContext // } from '../../../../../services'; // import { AppSpinner, ScrollableTab, FhirTaskDisplay } from '../../../../../components'; // import QuestionnairePreview from '../../../QuestionnairesPage/QuestionnairePage/QuestionnairePreview/QuestionnairePreview.tsx'; // import { useFhirTaskBroadcast, useFhirTasks, useFhirTaskTargetedBroadcast } from '../../../../../hooks'; // import React from 'react'; // const PatientTasks = ({ isHomePage = false, elevated = false }: { isHomePage?: boolean; elevated?: boolean }) => { // const navigate = useNavigate(); // const { userRole, USER_ROLES } = useAuthServiceContext(); // const { loading, error, clearError, tasksList } = useFhirTasks(); // const { getTaskDisplay } = useTaskServiceContext(); // const { // fetchQuestionnaire, // fetchQuestionnaireResponse, // transformFromFhirQuestionnaire, // } = useQuestionnaireServiceContext(); // const { valueSetUrls, fetchValueSet } = useValueSetServiceContext(); // const { extractReferenceId } = useFhirServiceContext(); // const [showResponse, setShowResponse] = useState(false); // const [responseLoading, setResponseLoading] = useState<{[key: number]: boolean}>({}); // const [statusFilter, setStatusFilter] = useState('all'); // const [filteredTasks, setFilteredTasks] = useState([]); // const [taskStatus, setTaskStatus] = useState([]); // const [isGuideOpen, setIsGuideOpen] = useState(false); // const [selectedTask, setSelectedTask] = useState(null); // const [showDisplayModal, setShowDisplayModal] = useState(false); // const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); // const spacerMargin = '24px'; // useFhirTaskBroadcast(); // useFhirTaskTargetedBroadcast({ // task: selectedTask || undefined, // isOpen: showDisplayModal, // onUpdate: (updatedTask) => { // setSelectedTask(updatedTask); // }, // onDelete: () => { // setShowDisplayModal(false); // setSelectedTask(null); // } // }); // const methods = useForm({ // defaultValues: {} // }); // const { reset, watch } = methods; // useEffect(() => { // const loadTaskStatusOptions = async () => { // try { // const taskStatusValueSet = await fetchValueSet(valueSetUrls.taskStatus); // setTaskStatus(taskStatusValueSet) // } catch (error) { // console.error('Failed to load task status options:', error); // } // }; // loadTaskStatusOptions(); // }, [fetchValueSet, valueSetUrls]); // useEffect(() => { // if (tasksList.length > 0) { // if (statusFilter === 'all') { // setFilteredTasks(isHomePage ? tasksList.filter((task: Task) => task.status !== 'completed'): tasksList); // } else { // setFilteredTasks(tasksList.filter(task => task.status === statusFilter)); // } // } else { // setFilteredTasks([]); // } // }, [tasksList, statusFilter]); // const viewTask = (task: Task) => { // setSelectedTask(task); // setShowDisplayModal(true); // }; // const editTask = (event: React.MouseEvent, task: Task) => { // event.stopPropagation(); // navigate(isHomePage ? `task/${task.id}`: task.id as string, { // relative: 'path' // }); // }; // const openTask = (event: React.MouseEvent, task: Task) => { // event.stopPropagation(); // navigate(isHomePage ? `task/${task.id}`: task.id as string, { // relative: 'path' // }); // }; // const addTask = () => { // navigate('new', { // relative: 'path' // }); // }; // const getStatusIcon = (status: string) => { // switch (status) { // case 'completed': // return check; // case 'cancelled': // case 'failed': // return close; // case 'in-progress': // return list; // default: // return null; // } // }; // const getStatusStyle = (status: string) => { // switch (status) { // case 'completed': // return { color: '#4CAF50' }; // case 'cancelled': // case 'failed': // return { color: '#F44336' }; // case 'in-progress': // return { color: '#2196F3' }; // case 'on-hold': // return { color: '#FF9800' }; // default: // return { color: '#757575' }; // } // }; // const getPriorityStyle = (priority: string) => { // switch (priority) { // case 'urgent': // case 'asap': // return { color: '#F44336', fontWeight: 'bold' }; // case 'high': // return { color: '#FF9800', fontWeight: 'bold' }; // case 'routine': // return { color: '#4CAF50' }; // default: // return {}; // } // }; // const viewResponse = async (event: any, task: Task) => { // event.preventDefault(); // event.stopPropagation(); // setResponseLoading(prev => ({...prev, [task.id as string]: true })); // try { // const questionnaireId = task.focus?.reference?.split('/').pop(); // if (questionnaireId) { // const loadedQuestionnaire = await fetchQuestionnaire(questionnaireId); // const responseId = extractReferenceId(task.output?.[0]?.valueReference); // const questionnaireResponse: QuestionnaireResponse = await fetchQuestionnaireResponse(responseId as string); // const transformedQuestionnaire = transformFromFhirQuestionnaire(loadedQuestionnaire, questionnaireResponse); // reset(transformedQuestionnaire); // setShowResponse(true); // } // } catch (error) { // console.error('Failed to load questionnaire:', error); // } finally { // setResponseLoading(prev => ({...prev, [task.id as string]: false })); // } // }; // const isAuthorizedToEdit = () => { // return userRole && [USER_ROLES.PRACTITIONER].includes(userRole as string); // }; // const renderContent = () => { // return ( // <> // setIsGuideOpen(false)} // resourceType="Task"/> // {error && ( // <> // // {error} // // // // )} // {!loading && filteredTasks.length === 0 && ( // <> // // No {isHomePage ? 'incomplete': ''} tasks found. // // // // )} // {loading ? ( // // ): ( // // {filteredTasks.length !== 0 && ( // <> // // {filteredTasks.map((task: Task) => { // const displayTitle = getTaskDisplay(task); // const statusIcon = getStatusIcon(task.status); // const statusStyle = getStatusStyle(task.status); // const priorityStyle = getPriorityStyle(task.priority || 'routine'); // return ( // viewTask(task)}> // // // {statusIcon && ( // // )} // // // // // // {displayTitle} // // {task.priority && ( // // Priority: {task.priority.toUpperCase()} // // )} // // // // {task.output && task.output.length !== 0 && ( // // // // )} // {isHomePage && isPatient && ( // // // // )} // {isAuthorizedToEdit() && ( // // // // )} // // // // // // // ); // })} // // {/* Response Modal */} // {showResponse && ( // setShowResponse(false)}> // // //
//
// //
//
//
//
//
// )} // {/* Task Display Modal */} // {selectedTask && ( // { // setShowDisplayModal(false); // setSelectedTask(null); // }}/> // )} // // )} //
// )} // // ); // }; // const renderHeaderButtons = () => { // return ( // // // {isHomePage && ( // Tasks // )} // // // // // // {isAuthorizedToEdit() && ( // // )} // // // ); // }; // if (isHomePage) { // return ( // // {elevated && } // // {renderHeaderButtons()} // // // {renderContent()} // // // ); // } // return ( // // // // {filteredTasks.length !== 0 && ( // option.code === statusFilter)?.code} // options={[ // { key: 'all', name: 'All' }, //...taskStatus.map(option => ({ // key: option.code, // name: option.display // })) // ]} // onChange={(newValue: { selectedItem: { key: string, name: string } }) => { // setStatusFilter(newValue.selectedItem.key); // }}/> // )} // // // {renderHeaderButtons()} // // // // {renderContent()} // // ); // }; // export default PatientTasks;