"use client"; /** * "Termine" — the visits half of the checkups page. * * Two lists rather than one, and the split is the server's: upcoming reads * soonest-first, past reads newest-first, and the API resolves both so the * ordering lives in one place. A client that split and re-sorted one array * would be the second place it lives. * * No new nav entry and no new route. `/checkups` already owns exactly the right * sentence — when do I have to do what, where — and is deliberately not module * gated, which is also why a visit carries no module key of its own. */ import { CalendarClock, ContactRound, History, Plus } from "lucide-react"; import Link from "next/link"; import { Fragment, useState } from "react"; import { SinceLastVisit } from "@/components/day/since-last-visit"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { PageHeader } from "@/components/ui/page-header"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { SectionHeading } from "@/components/ui/section-heading"; import { Skeleton } from "@/components/ui/skeleton"; import { useDeepLinkedRecord } from "@/hooks/use-deep-linked-record"; import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; import { useEncounters, type Encounter } from "@/hooks/use-encounters"; import { apiGet } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { EncounterSheet } from "./encounter-sheet"; import { VisitCard } from "./visit-card"; /** One visit by id, for a link to a visit older than the list reads. */ function fetchVisit(id: string): Promise { return apiGet(`/api/encounters/${encodeURIComponent(id)}`).catch( () => null, ); } export function VisitsSection({ enabled = true }: { enabled?: boolean }) { const { t } = useTranslations(); const { canWriteDomain, canManageDomain } = useRecordCapabilities(); // `POST /api/encounters` answers at WRITE under the profile section; the // edit and the delete behind the row are both MANAGE. Only the add was // asked, so every row stayed tappable into a sheet whose Save and Delete // the server refuses. const canAddVisit = canWriteDomain("profile"); const canManageVisits = canManageDomain("profile"); const list = useEncounters(enabled); // `null` closed, `"new"` create, a row edits it. const [editing, setEditing] = useState(null); const [session, setSession] = useState(0); const openSheet = (target: Encounter | "new") => { setEditing(target); setSession((n) => n + 1); }; const upcoming = list.data?.upcoming ?? []; const past = list.data?.past ?? []; // `?visit=` — a document's link chip names the visit it is filed // against. Scroll to it, and open it for someone who may edit it. useDeepLinkedRecord({ param: "visit", records: list.data ? [...upcoming, ...past] : undefined, anchorAttribute: "data-encounter-id", open: canManageVisits ? openSheet : undefined, // The list reads the newest hundred of each arm; a search hit can name an // older visit, which is read on its own. resolve: fetchVisit, }); const empty = upcoming.length === 0 && past.length === 0; // The last visit that took place: the start of the visit preparation. const lastDone = past.find((encounter) => encounter.status === "DONE"); const addButton = canAddVisit ? ( ) : null; return (
{/* The empty state carries the add action while there is no visit. */} {!list.isPending && !list.isError && empty ? null : addButton} } /> {list.isError ? ( void list.refetch()} /> ) : list.isPending ? (
{Array.from({ length: 3 }, (_, index) => ( ))}
) : empty ? ( } title={t("encounters.emptyTitle")} description={t("encounters.emptyDescription")} action={addButton ?? undefined} /> ) : (
{upcoming.length > 0 ? (
{upcoming.map((encounter, i) => ( {/* v1.42 — the next appointment carries what happened since the last one, each date opening its day. The component draws its own card, and none when it has nothing to list. */} {i === 0 && lastDone ? ( ) : null} ))}
) : null} {past.length > 0 ? (
{past.map((encounter) => ( ))}
) : null}
)} {/* `key` changes on every open, so the sheet remounts with a fresh draft instead of copying props into state from an effect. It stays mounted while closing, which is what keeps the exit animation. */} { if (!open) setEditing(null); }} encounter={editing === "new" ? null : editing} />
); }