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 = { 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>; 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();