Files
temetro/frontend/components/patients/transfer-patient-dialog.tsx
T
Khalid Abdi 3eb5687f4d frontend: messages search, care-team employee dialog, PCP picker + transfer, activity detail, analytics grid
- Messages: search the inbox and the compose member picker.
- Care Team: clickable member rows open an employee dialog showing role +
  permissions, with change-role (updateMemberRole) and remove.
- Patients: Primary Care is now a provider dropdown (defaults to self for a
  doctor); add a Transfer action + dialog wired to the transfer API.
- Activity: entries are clickable, opening a detail dialog.
- Analytics: Section takes a columns prop so each row fills evenly (no orphan
  card in Appointments).
- Add lib/staff.ts (listProviders), transferPatient client, rolePermissionSummary
  helper, and i18n keys for all new strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:51:38 +03:00

141 lines
4.3 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import { ROLE_LABELS } from "@/lib/access";
import { type Patient, transferPatient } from "@/lib/patients";
import { listProviders, type Provider } from "@/lib/staff";
import { notify } from "@/lib/toast";
const selectClass =
"h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30";
type Props = {
patient: Patient;
open: boolean;
onOpenChange: (open: boolean) => void;
onTransferred: (patient: Patient) => void;
};
// Reassign a patient to another clinician. The new provider becomes the
// patient's primary provider (and PCP label), which moves the chart into their
// panel under per-doctor visibility.
export function TransferPatientDialog({
patient,
open,
onOpenChange,
onTransferred,
}: Props) {
const { t } = useTranslation();
const [providers, setProviders] = useState<Provider[]>([]);
const [providerId, setProviderId] = useState(patient.primaryProviderId ?? "");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setProviderId(patient.primaryProviderId ?? "");
setError(null);
let active = true;
listProviders()
.then((list) => active && setProviders(list))
.catch(() => active && setProviders([]));
return () => {
active = false;
};
}, [open, patient.primaryProviderId]);
const submit = async () => {
if (!providerId || submitting) return;
setSubmitting(true);
setError(null);
try {
const updated = await transferPatient(patient.fileNumber, providerId);
onTransferred(updated);
notify.success(
t("patients.transfer.successTitle"),
t("patients.transfer.successBody", {
name: updated.name,
provider: updated.pcp,
}),
);
onOpenChange(false);
} catch (err) {
const message =
err instanceof Error ? err.message : t("patients.transfer.error");
setError(message);
notify.error(t("patients.transfer.errorTitle"), message);
} finally {
setSubmitting(false);
}
};
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>{t("patients.transfer.title")}</DialogTitle>
<DialogDescription>
{t("patients.transfer.description", { name: patient.name })}
</DialogDescription>
</DialogHeader>
<DialogPanel className="flex flex-col gap-3">
{error && (
<p className="rounded-2xl bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</p>
)}
<label className="flex flex-col gap-1.5">
<span className="text-xs text-muted-foreground">
{t("patients.transfer.providerLabel")}
</span>
<select
className={selectClass}
onChange={(e) => setProviderId(e.target.value)}
value={providerId}
>
<option value="">{t("patients.transfer.choose")}</option>
{providers.map((p) => (
<option key={p.userId} value={p.userId}>
{p.name} ·{" "}
{ROLE_LABELS[p.role as keyof typeof ROLE_LABELS] ?? p.role}
</option>
))}
</select>
</label>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}>
{t("patients.transfer.cancel")}
</DialogClose>
<Button
disabled={
submitting ||
!providerId ||
providerId === patient.primaryProviderId
}
onClick={submit}
type="button"
>
{submitting
? t("patients.transfer.transferring")
: t("patients.transfer.confirm")}
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}