From 4104dd32d87ba08bb7492996eeb5e71cbefea591 Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Wed, 11 Feb 2026 02:27:41 +0000 Subject: [PATCH] Task 12: Build ReferralsView with clinical referral form - Created ReferralsView component with clinical referral form UI - Pre-filled patient info (CHARLWOOD, Andrew; NHS Number) - Priority toggle: Urgent/Routine/Two-Week Wait with tooltips - Form fields: Referrer Name/Email/Org, Reason textarea - Contact method radio: Email/Phone/LinkedIn - Form validation for required fields - Loading state with spinner on submit - Success state with REF-YYYY-MMDD-NNN reference number - Direct Contact table with clickable email/phone/LinkedIn links - Responsive two-column layout for form fields - Consistent clinical system styling with NHS blue accents --- src/components/PMRInterface.tsx | 3 + src/components/views/ReferralsView.tsx | 486 +++++++++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 src/components/views/ReferralsView.tsx diff --git a/src/components/PMRInterface.tsx b/src/components/PMRInterface.tsx index b85def3..05254e8 100644 --- a/src/components/PMRInterface.tsx +++ b/src/components/PMRInterface.tsx @@ -8,6 +8,7 @@ import { MedicationsView } from './views/MedicationsView' import { ProblemsView } from './views/ProblemsView' import { InvestigationsView } from './views/InvestigationsView' import { DocumentsView } from './views/DocumentsView' +import { ReferralsView } from './views/ReferralsView' interface PMRInterfaceProps { children?: React.ReactNode @@ -52,6 +53,8 @@ export function PMRInterface({ children }: PMRInterfaceProps) { return case 'documents': return + case 'referrals': + return default: return (
diff --git a/src/components/views/ReferralsView.tsx b/src/components/views/ReferralsView.tsx new file mode 100644 index 0000000..38199a0 --- /dev/null +++ b/src/components/views/ReferralsView.tsx @@ -0,0 +1,486 @@ +import { useState, useRef } from 'react' +import { Send, Mail, Phone, MapPin, ExternalLink, Loader2, CheckCircle } from 'lucide-react' +import { patient } from '@/data/patient' + +type Priority = 'urgent' | 'routine' | 'two-week-wait' +type ContactMethod = 'email' | 'phone' | 'linkedin' + +interface FormData { + priority: Priority + referrerName: string + referrerEmail: string + referrerOrg: string + reason: string + contactMethod: ContactMethod +} + +interface FormErrors { + referrerName?: string + referrerEmail?: string +} + +function generateRefNumber(): string { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const seq = String(Math.floor(Math.random() * 999) + 1).padStart(3, '0') + return `REF-${year}-${month}${day}-${seq}` +} + +function PriorityOption({ + value, + label, + selected, + tooltip, + onSelect, +}: { + value: Priority + label: string + selected: boolean + tooltip: string + onSelect: () => void +}) { + const dotColors: Record = { + urgent: 'bg-red-500', + routine: 'bg-pmr-nhsblue', + 'two-week-wait': 'bg-amber-500', + } + + const labelColors: Record = { + urgent: 'text-red-700', + routine: 'text-pmr-nhsblue', + 'two-week-wait': 'text-amber-700', + } + + return ( + + ) +} + +function ContactMethodOption({ + value, + label, + selected, + onSelect, +}: { + value: ContactMethod + label: string + selected: boolean + onSelect: () => void +}) { + return ( + + ) +} + +function FormField({ + label, + id, + required, + error, + children, +}: { + label: string + id: string + required?: boolean + error?: string + children: React.ReactNode +}) { + return ( +
+ + {children} + {error &&

{error}

} +
+ ) +} + +function DirectContactTable() { + const contactMethods = [ + { + label: 'Email', + value: patient.email, + href: `mailto:${patient.email}`, + action: 'Send Email', + icon: Mail, + }, + { + label: 'Phone', + value: patient.phone, + href: `tel:${patient.phone}`, + action: 'Call', + icon: Phone, + }, + { + label: 'LinkedIn', + value: patient.linkedin, + href: `https://${patient.linkedin}`, + action: 'View Profile', + icon: ExternalLink, + external: true, + }, + { + label: 'Location', + value: 'Norwich, UK', + href: null, + action: null, + icon: MapPin, + }, + ] + + return ( +
+
+

+ Direct Contact +

+
+
+ {contactMethods.map((method) => ( +
+
+ + {method.label} + {method.href ? ( + + {method.value} + + ) : ( + {method.value} + )} +
+ {method.href && ( + + {method.action} + {method.external && } + + )} +
+ ))} +
+
+ ) +} + +export function ReferralsView() { + const prefersReducedMotion = useRef( + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ).current + + const [formData, setFormData] = useState({ + priority: 'routine', + referrerName: '', + referrerEmail: '', + referrerOrg: '', + reason: '', + contactMethod: 'email', + }) + const [errors, setErrors] = useState({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + const [refNumber, setRefNumber] = useState('') + + const validateForm = (): boolean => { + const newErrors: FormErrors = {} + + if (!formData.referrerName.trim()) { + newErrors.referrerName = 'Referrer name is required' + } + if (!formData.referrerEmail.trim()) { + newErrors.referrerEmail = 'Referrer email is required' + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.referrerEmail)) { + newErrors.referrerEmail = 'Please enter a valid email address' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validateForm()) return + + setIsSubmitting(true) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + setRefNumber(generateRefNumber()) + setIsSubmitting(false) + setIsSuccess(true) + } + + const handleReset = () => { + setFormData({ + priority: 'routine', + referrerName: '', + referrerEmail: '', + referrerOrg: '', + reason: '', + contactMethod: 'email', + }) + setErrors({}) + setIsSuccess(false) + setRefNumber('') + } + + if (isSuccess) { + return ( +
+
+
+

+ New Referral +

+
+
+
+ +
+

+ Referral sent successfully +

+

Reference: {refNumber}

+

+ Expected response time: 24-48 hours +

+ +
+
+ +
+ ) + } + + return ( +
+
+
+

+ New Referral +

+

+ Contact Andy using a clinical referral form format. +

+
+
+
+
+ + Referring to + + {patient.name} +
+
+ + NHS Number + + {patient.nhsNumber} +
+
+ +
+ + Priority + +
+ setFormData({ ...formData, priority: 'urgent' })} + /> + setFormData({ ...formData, priority: 'routine' })} + /> + setFormData({ ...formData, priority: 'two-week-wait' })} + /> +
+
+ +
+ + setFormData({ ...formData, referrerName: e.target.value })} + className="w-full border border-gray-300 rounded px-3 py-2 text-sm font-inter text-gray-900 focus:border-pmr-nhsblue focus:ring-2 focus:ring-pmr-nhsblue/20 focus:outline-none transition-colors" + placeholder="Your name" + /> + + + setFormData({ ...formData, referrerEmail: e.target.value })} + className="w-full border border-gray-300 rounded px-3 py-2 text-sm font-inter text-gray-900 focus:border-pmr-nhsblue focus:ring-2 focus:ring-pmr-nhsblue/20 focus:outline-none transition-colors" + placeholder="your.email@example.com" + /> + +
+ + + setFormData({ ...formData, referrerOrg: e.target.value })} + className="w-full border border-gray-300 rounded px-3 py-2 text-sm font-inter text-gray-900 focus:border-pmr-nhsblue focus:ring-2 focus:ring-pmr-nhsblue/20 focus:outline-none transition-colors" + placeholder="Organisation name (optional)" + /> + + + +