import { CheckCircle, EnvelopeSimple, Info } from "@phosphor-icons/react";
import { useState } from "react";
import type { FormEvent } from "react";
import { clinicianApi } from "../../api/client";
import type { AccessRequestReceipt, InvitePreview } from "../../api/types";
import {
countries,
emptyAccessForm,
heardFromLabels,
prefillFromInvite,
toAccessRequest,
validateAccess,
type AccessErrors,
type AccessForm,
} from "../../logic/access";
import { siteWords } from "../../logic/site";
import { Link } from "../router";
import { errorText, Field } from "../ui";
import { SiteLayout } from "./Landing";
/** ACC-3: the request form, pre-filled from a patient's invite when there is one (ACC-4). */
export function RequestAccessForm({ invite, inviteToken }: { invite: InvitePreview | null; inviteToken: string | null }) {
const [form, setForm] = useState<AccessForm>(() => (invite ? prefillFromInvite(emptyAccessForm(), invite) : emptyAccessForm()));
const [errors, setErrors] = useState<AccessErrors>({});
const [sending, setSending] = useState(false);
const [failure, setFailure] = useState("");
const [receipt, setReceipt] = useState<AccessRequestReceipt | null>(null);
const set = <K extends keyof AccessForm>(key: K, value: AccessForm[K]) => {
setForm((f) => ({ ...f, [key]: value }));
if (errors[key]) setErrors((e) => ({ ...e, [key]: undefined }));
};
const submit = async (e: FormEvent) => {
e.preventDefault();
const found = validateAccess(form);
setErrors(found);
const first = Object.keys(found)[0];
if (first) {
document.getElementById(`acc-${first}`)?.focus();
return;
}
setSending(true);
setFailure("");
try {
setReceipt(await clinicianApi.requestAccess(toAccessRequest(form, inviteToken)));
} catch (error) {
setFailure(errorText(error));
} finally {
setSending(false);
}
};
const props = (key: keyof AccessForm) => ({
id: `acc-${key}`,
"aria-invalid": errors[key] ? true : undefined,
"aria-describedby": errors[key] ? `acc-${key}-error` : undefined,
});
if (receipt)
return (
<div className="receipt" role="status">
<CheckCircle size={28} weight="fill" />
<h2>Request sent</h2>
<p>
We sent a confirmation to <strong>{form.workEmail.trim()}</strong>.{" "}
{siteWords().receiptNext(form.profession === "doctor" ? "MDCN" : "PCN")}
</p>
{receipt.priority && invite && (
<p>
Because {invite.patientName} invited you, your request is near the front of the queue. The invite waits for you
until you are verified.
</p>
)}
{!receipt.countryOpen && (
<p className="hint">Approvals in your country open once its regulatory and data checks are done. We keep your request until then.</p>
)}
<a className="button" href="/demo">
Try a fictional patient meanwhile
</a>
</div>
);
return (
<form className="access-form" onSubmit={submit} noValidate>
<fieldset>
<legend>You</legend>
<Field id="acc-fullName" label="Full name" error={errors.fullName} hint="As it appears on your licence.">
<input {...props("fullName")} autoComplete="name" value={form.fullName} onChange={(e) => set("fullName", e.target.value)} />
</Field>
<div className="field" role="radiogroup" aria-labelledby="acc-profession-label">
<span className="label" id="acc-profession-label">
Profession
</span>
<div className="choice-row">
{(["doctor", "pharmacist"] as const).map((p, i) => (
<label key={p} className={`choice ${form.profession === p ? "on" : ""}`}>
<input
type="radio"
name="profession"
id={i === 0 ? "acc-profession" : undefined}
checked={form.profession === p}
onChange={() => set("profession", p)}
/>
{p === "doctor" ? "Doctor (MDCN)" : "Pharmacist (PCN)"}
</label>
))}
</div>
{errors.profession && (
<small className="field-error" role="alert">
{errors.profession}
</small>
)}
</div>
<Field
id="acc-licenceNumber"
label={form.profession === "pharmacist" ? "PCN registration number" : "MDCN folio number"}
error={errors.licenceNumber}
hint={form.profession === "pharmacist" ? "From your PCN licence." : "For example MDCN/R/123456."}
>
<input {...props("licenceNumber")} value={form.licenceNumber} autoCapitalize="characters" onChange={(e) => set("licenceNumber", e.target.value)} />
</Field>
<Field id="acc-workEmail" label="Work email" error={errors.workEmail} hint="We send your sign-in codes here.">
<input {...props("workEmail")} type="email" autoComplete="email" inputMode="email" value={form.workEmail} onChange={(e) => set("workEmail", e.target.value)} />
</Field>
<Field id="acc-mobile" label="Mobile number" error={errors.mobile}>
<input {...props("mobile")} type="tel" autoComplete="tel" inputMode="tel" placeholder="+234" value={form.mobile} onChange={(e) => set("mobile", e.target.value)} />
</Field>
</fieldset>
<fieldset>
<legend>Your clinic</legend>
<Field id="acc-clinicName" label="Clinic or facility name" error={errors.clinicName}>
<input {...props("clinicName")} autoComplete="organization" value={form.clinicName} onChange={(e) => set("clinicName", e.target.value)} />
</Field>
<Field id="acc-clinicPhone" label="Clinic phone" error={errors.clinicPhone} hint="Shown to patients after a check-in.">
<input {...props("clinicPhone")} type="tel" inputMode="tel" placeholder="+234" value={form.clinicPhone} onChange={(e) => set("clinicPhone", e.target.value)} />
</Field>
<Field id="acc-country" label="Country" error={errors.country} hint={countries.find((c) => c.code === form.country)?.open ? undefined : "Requests from this country wait until its checks are done."}>
<select {...props("country")} value={form.country} onChange={(e) => set("country", e.target.value)}>
{countries.map((c) => (
<option key={c.code} value={c.code}>
{c.name}
</option>
))}
</select>
</Field>
<Field id="acc-heardFrom" label="How did you hear about Context?" error={errors.heardFrom}>
<select {...props("heardFrom")} value={form.heardFrom} onChange={(e) => set("heardFrom", e.target.value as AccessForm["heardFrom"])}>
<option value="">Choose one</option>
{Object.entries(heardFromLabels).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</Field>
{form.heardFrom === "other" && (
<Field id="acc-heardFromOther" label="Where?" error={errors.heardFromOther}>
<input {...props("heardFromOther")} value={form.heardFromOther} onChange={(e) => set("heardFromOther", e.target.value)} />
</Field>
)}
</fieldset>
<label className={`check terms ${errors.acceptedTerms ? "has-error" : ""}`}>
<input
type="checkbox"
id="acc-acceptedTerms"
checked={form.acceptedTerms}
aria-invalid={errors.acceptedTerms ? true : undefined}
onChange={(e) => set("acceptedTerms", e.target.checked)}
/>
<span>
I accept the <a href="/terms/clinician" target="_blank" rel="noreferrer">clinician terms</a>. Context shows
information and its source; I make the clinical decisions.
</span>
</label>
{errors.acceptedTerms && (
<small className="field-error" role="alert">
{errors.acceptedTerms}
</small>
)}
{failure && (
<div className="banner error" role="alert">
<Info size={16} />
<div>{failure}</div>
</div>
)}
<div className="form-actions">
<button className="button primary large" disabled={sending}>
<EnvelopeSimple size={16} />
{sending ? "Sending…" : "Send request"}
</button>
<span className="hint">Takes about two minutes. We confirm by email.</span>
</div>
</form>
);
}
export default function RequestAccess() {
const intro = siteWords().requestIntro;
return (
<SiteLayout>
<div className="narrow">
<header className="page-head">
<h1>Request access</h1>
{intro && <p className="lead">{intro}</p>}
<p>
For doctors registered with the MDCN and pharmacists registered with the PCN. Already verified?{" "}
<Link to="/sign-in">Sign in</Link>.
</p>
</header>
<RequestAccessForm invite={null} inviteToken={null} />
</div>
</SiteLayout>
);
}