import type { AccessRequestInput, HeardFrom, InvitePreview, LicensingBody, Profession } from "../api/types";
export const TERMS_VERSION = "clinician-terms-2026-10";
export const countries = [
{ code: "NG", name: "Nigeria", open: true },
{ code: "RW", name: "Rwanda", open: false },
{ code: "QA", name: "Qatar", open: false },
{ code: "GH", name: "Ghana", open: false },
{ code: "KE", name: "Kenya", open: false },
{ code: "GB", name: "United Kingdom", open: false },
{ code: "OTHER", name: "Another country", open: false },
] as const;
export const heardFromLabels: Record<HeardFrom, string> = {
film: "The MARMAR Context film",
patient_invite: "A patient's invite",
colleague: "A colleague",
other: "Somewhere else",
};
export type AccessForm = {
fullName: string;
profession: Profession | "";
licenceNumber: string;
country: string;
clinicName: string;
clinicPhone: string;
workEmail: string;
mobile: string;
heardFrom: HeardFrom | "";
heardFromOther: string;
acceptedTerms: boolean;
};
export type AccessErrors = Partial<Record<keyof AccessForm, string>>;
export const emptyAccessForm = (): AccessForm => ({
fullName: "",
profession: "",
licenceNumber: "",
country: "NG",
clinicName: "",
clinicPhone: "",
workEmail: "",
mobile: "",
heardFrom: "",
heardFromOther: "",
acceptedTerms: false,
});
/** ACC-4: a request from a patient's invite starts with what the invite already knows. */
export function prefillFromInvite(form: AccessForm, invite: InvitePreview): AccessForm {
const name = invite.doctorName.replace(/^(dr\.?|doctor|pharm\.?)\s+/i, "").trim();
return {
...form,
fullName: form.fullName || name,
clinicName: form.clinicName || invite.clinicName || "",
workEmail: form.workEmail || invite.doctorEmail || "",
profession: form.profession || (/^(dr\.?|doctor)\s/i.test(invite.doctorName) ? "doctor" : ""),
heardFrom: "patient_invite",
};
}
export const licensingBodyFor = (profession: Profession): LicensingBody => (profession === "doctor" ? "MDCN" : "PCN");
const email = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
const phone = (value: string) => {
const digits = value.replace(/[\s().-]/g, "");
return /^\+?\d{7,15}$/.test(digits);
};
/** A number Context may text: with its country code (+234... or 00234...), as the backend stores it for alerts. */
export const isInternationalNumber = (value: string) => /^(\+|00)[1-9]\d{6,14}$/.test(value.replace(/[\s().-]/g, ""));
/** MDCN folio numbers such as MDCN/R/123456, or the bare folio digits. */
const mdcn = /^(MDCN\/[A-Z]{1,3}\/\d{3,7}|\d{3,7})$/i;
/** PCN registration numbers: letters, digits and slashes or dashes. */
const pcn = /^[A-Z0-9][A-Z0-9/-]{2,19}$/i;
/** ACC-3 validation. Each message says what to fix. */
export function validateAccess(form: AccessForm): AccessErrors {
const errors: AccessErrors = {};
const name = form.fullName.trim();
if (name.split(/\s+/).length < 2 || name.length > 120) errors.fullName = "Enter your full name as it appears on your licence.";
if (!form.profession) errors.profession = "Choose doctor or pharmacist.";
const licence = form.licenceNumber.trim();
if (!licence) errors.licenceNumber = "Enter your licence number.";
else if (form.profession === "doctor" && !mdcn.test(licence))
errors.licenceNumber = "Enter your MDCN folio number, for example MDCN/R/123456.";
else if (form.profession === "pharmacist" && !pcn.test(licence))
errors.licenceNumber = "Enter your PCN registration number, letters and digits only.";
if (!form.country) errors.country = "Choose the country you practise in.";
if (form.clinicName.trim().length < 2) errors.clinicName = "Enter your clinic or facility name.";
if (!phone(form.clinicPhone)) errors.clinicPhone = "Enter the clinic phone with its country code, for example +234 1 234 5678.";
if (!email.test(form.workEmail.trim())) errors.workEmail = "Enter your work email, for example name@clinic.ng.";
if (!isInternationalNumber(form.mobile)) errors.mobile = "Enter your mobile number with its country code, for example +234 803 123 4567.";
if (!form.heardFrom) errors.heardFrom = "Tell us how you heard about Context.";
else if (form.heardFrom === "other" && !form.heardFromOther.trim()) errors.heardFromOther = "Tell us where you heard about it.";
if (!form.acceptedTerms) errors.acceptedTerms = "Accept the clinician terms to send your request.";
return errors;
}
export function toAccessRequest(form: AccessForm, inviteToken: string | null): AccessRequestInput {
const profession = form.profession as Profession;
return {
fullName: form.fullName.trim(),
profession,
licensingBody: licensingBodyFor(profession),
licenceNumber: form.licenceNumber.trim().toUpperCase(),
country: form.country,
clinicName: form.clinicName.trim(),
clinicPhone: form.clinicPhone.trim(),
workEmail: form.workEmail.trim().toLowerCase(),
mobile: form.mobile.trim(),
heardFrom: form.heardFrom as HeardFrom,
heardFromOther: form.heardFrom === "other" ? form.heardFromOther.trim() : null,
acceptedTermsVersion: TERMS_VERSION,
inviteToken,
};
}
/** ACC-7: lock after the chosen idle time; the session itself ends after 12 hours. */
export const shouldLock = (lastActivity: number, now: number, idleMinutes: number) =>
now - lastActivity >= idleMinutes * 60_000;
export const sessionEnded = (expiresAt: string, now: number) => now >= new Date(expiresAt).getTime();