import { useEffect, useRef, useState, type ReactNode } from 'react'; import { ArrowLeft, Loader2, TriangleAlert } from 'lucide-react'; import { FormSubmission, FormSubmissionReply as SentReply, useFormSubmissionsService, } from '../../../services/FormSubmissionsService.tsx'; import { operationStatusToast } from '../../../components/OperationStatusToast/OperationStatusToast.tsx'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; /** * ANSWERING SOMEBODY WHO WROTE IN -, and Spencer's reframe is the whole design of this file. * * 2026-08-18: *"remember this is public facing form submissions for things like 'contact us' forms and * stuff. not actual clinical care stuff."* **That reset the risk model this card had been reasoning * from.** The reply had been weighed as *"outbound PHI to an unverified address"* and shelved as * near-undecidable. It is a contact form: the reply goes to whoever wrote in, about what they wrote in * about, at the address they typed. * * SO THE EXPOSURE IS NOT THE CHANNEL - IT IS A STAFF MEMBER TYPING SOMETHING CLINICAL INTO IT, and the * control follows from that: **a disclaimer aimed at the person composing**, not a check on the recipient. * Deliberately not built: address verification, a consent gate, a content scanner. All three answer a * question nobody is asking, and each would stop a practice answering its own contact form. * * AND IT SITS WHERE THE REPLY IS TYPED, permanently. Spencer: *"The disclaimer must sit where the reply * is TYPED, not in a settings page or a one-time notice."* A warning shown once is a warning nobody sees * on the day it matters - the day somebody starts typing a test result into a contact-form reply. */ /** One sentence, above the box, always. Not a tooltip, not a first-run notice, not a settings page. */ export const REPLY_DISCLAIMER = 'This is an ordinary email to an address someone typed into a public form — nobody has verified it. Do not include test results, diagnoses, or anything else from a patient\'s record.'; interface FormSubmissionReplyProps { submission: FormSubmission; /** The draft belongs to the overlay, so Back and a return keep it for this visit. */ draft: string; onDraftChange: (draft: string) => void; onBack: () => void; /** Who sent it and what they sent, drawn above the reply until a send fails. */ facts?: ReactNode; onBusyChange?: (busy: boolean) => void; /** A failed send, held by the overlay so it survives a trip to the discard question and back. */ error?: string; onErrorChange?: (error: string | undefined) => void; /** * THE WHOLE REPLY, BODY INCLUDED - not the server's receipt. The receipt carries when, by whom and * to where; the body is only here. Handing back the receipt alone put an EMPTY message into the thread * on screen, which is worse than not showing it: it reads as a reply that was sent blank. */ onReplySent: (reply: SentReply) => void; } const formatSentAt = (iso: string): string => { const date = new Date(iso); return isNaN(date.getTime()) ? iso : date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); }; /** What was already sent, so two people do not answer differently. */ export const SentReplies = ({ replies }: { replies: SentReply[] }) => ( <> {replies.map((reply, index) => (
{reply.body}
Reply not sent
Your reply is still here. Check the recipient and try again.
{error}
{REPLY_DISCLAIMER}