import { useState } from "react";
import { clinicianApi } from "../../api/client";
import type { DeclineReason, Invitation } from "../../api/types";
import { formatDate } from "../../flow";
import { firstName, whenLabel } from "../../logic/format";
import { sharesLine, withLine } from "../../logic/patients";
import { Button, Checkbox, DialogActions, FieldError, Icon, labelClass, Modal, Radio, Textarea } from "../kit";
import { navigate } from "../router";
import { shortName, useClinician } from "../session";
import { errorText } from "../ui";
const reasons: { id: DeclineReason; label: string }[] = [
{ id: "not_my_patient", label: "Not my patient" },
{ id: "not_for_context", label: "I don't use Context for this" },
{ id: "other", label: "Another reason" },
];
/** INV-11 to INV-14: an invitation card with Decline and Accept. */
export function InvitationCard({ invitation, onDone }: { invitation: Invitation; onDone: (message: string) => void }) {
const { me } = useClinician();
const [dialog, setDialog] = useState<"accept" | "decline" | null>(null);
const [confirmed, setConfirmed] = useState(false);
const [link, setLink] = useState<"link" | "separate" | null>(null);
const [reason, setReason] = useState<DeclineReason | null>(null);
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const first = firstName(invitation.patientName);
const match = invitation.possibleMatch;
const close = () => {
setDialog(null);
setError("");
};
const accept = async () => {
setBusy(true);
setError("");
try {
const { patientId } = await clinicianApi.acceptInvitation(invitation.id, {
confirmedMyPatient: true,
linkToPatientId: match && link === "link" ? match.patientId : null,
});
navigate(`/app/patients/${patientId}`);
} catch (e) {
setError(errorText(e));
} finally {
setBusy(false);
}
};
const decline = async () => {
setBusy(true);
setError("");
try {
await clinicianApi.declineInvitation(invitation.id, { reason, note: reason === "other" ? note.trim() || null : null });
close();
onDone(`Declined. ${first} sees: "${shortName(me)} couldn't accept your invite."`);
} catch (e) {
setError(errorText(e));
} finally {
setBusy(false);
}
};
return (
<article className="flex flex-wrap items-start gap-x-3.5 gap-y-3 rounded-2xl bg-page p-4 shadow-card">
<span aria-hidden="true" className="flex size-9 shrink-0 items-center justify-center rounded-[10px] bg-indigo-fill text-indigo-icon">
<Icon name="mail-open-line" size={18} />
</span>
<div className="flex min-w-0 flex-1 basis-60 flex-col gap-0.5">
<b className="text-[14.5px] leading-5 font-medium text-ink">{invitation.patientName} invited you</b>
<small className="text-[13px] leading-[18px] text-body">
From MARMAR Health
{invitation.withPerson ? ` · ${withLine(invitation.withPerson, invitation.patientPronoun)}` : ""} · {sharesLine(invitation.scopes)}
{invitation.allowsCheckInCalls ? " · lets you ask Yasmar to check in" : ""}
</small>
<small className="text-[12.5px] leading-[18px] text-muted">
Sent {whenLabel(invitation.sentAt)} · expires {formatDate(invitation.expiresAt.slice(0, 10))}
{match ? ` · You have a clinic patient with the same name and date of birth` : ""}
</small>
</div>
<div className="ml-auto flex gap-2 self-center">
<Button onClick={() => setDialog("decline")}>Decline</Button>
<Button variant="primary" onClick={() => setDialog("accept")}>
Accept
</Button>
</div>
<Modal open={dialog === "accept"} title={`Accept ${invitation.patientName}'s invite`} onClose={close}>
<p className="m-0">
You will see what {first} shares from MARMAR Health. {first} can pause or stop sharing at any time, and sees that
you accepted.
</p>
<Checkbox checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)}>
<strong className="font-semibold text-ink">{invitation.patientName} is my patient.</strong>
</Checkbox>
{match && (
<fieldset className="m-0 flex flex-col gap-2.5 rounded-xl border-0 bg-tile p-3.5">
<legend className="float-left mb-1 p-0 text-[13px] leading-[19px] font-medium text-ink">
You already have a clinic patient {match.name}
{match.dateOfBirth ? `, born ${formatDate(match.dateOfBirth)}` : ""}. Is this the same person?
</legend>
<Radio name={`link-${invitation.id}`} checked={link === "link"} onChange={() => setLink("link")} className="clear-left">
Yes, link them. The records you added stay, and the app data joins them.
</Radio>
<Radio name={`link-${invitation.id}`} checked={link === "separate"} onChange={() => setLink("separate")}>
No, keep them separate.
</Radio>
</fieldset>
)}
{error && <FieldError>{error}</FieldError>}
<DialogActions>
<Button onClick={close}>Cancel</Button>
<Button variant="primary" disabled={!confirmed || (!!match && !link) || busy} onClick={() => void accept()}>
{busy ? "Accepting…" : "Accept and open"}
</Button>
</DialogActions>
</Modal>
<Modal open={dialog === "decline"} title={`Decline ${invitation.patientName}'s invite`} onClose={close}>
<p className="m-0">
{first} sees a kind message: "{shortName(me)} couldn't accept your invite." A reason is optional and only MARMAR
sees it.
</p>
<fieldset className="m-0 flex flex-col gap-2.5 border-0 p-0">
<legend className="mb-2 p-0 text-[12.5px] font-medium text-cell">Reason (optional)</legend>
{reasons.map((r) => (
<Radio key={r.id} name={`reason-${invitation.id}`} checked={reason === r.id} onChange={() => setReason(r.id)}>
{r.label}
</Radio>
))}
</fieldset>
{reason === "other" && (
<label className={labelClass}>
Tell us more (optional)
<Textarea rows={2} maxLength={300} value={note} onChange={(e) => setNote(e.target.value)} />
</label>
)}
{error && <FieldError>{error}</FieldError>}
<DialogActions>
<Button onClick={close}>Cancel</Button>
<Button variant="primary" disabled={busy} onClick={() => void decline()}>
{busy ? "Declining…" : "Decline invite"}
</Button>
</DialogActions>
</Modal>
</article>
);
}
/** The patient's phone, always shared for connected patients so the clinic can call (INV-3). */
export const PhoneLink = ({ phone }: { phone: string }) => (
<a className="phone-link" href={`tel:${phone.replace(/[^\d+]/g, "")}`}>
<Icon name="phone-line" size={14} /> {phone}
</a>
);