| import { useState } from 'react'; |
| import { Calendar, User, Eye, FileText, Clock, Camera, ArrowLeft, Search, Filter } from 'lucide-react'; |
|
|
| |
| interface ExamRecord { |
| id: string; |
| date: Date; |
| indication: string; |
| impression: string; |
| tzType: string; |
| biopsy: 'Taken' | 'Not Taken'; |
| finalStatus: string; |
| performedBy?: string; |
| scjVisibility: 'Fully' | 'Partially' | 'Not visible'; |
| media: MediaItem[]; |
| findings: Findings; |
| outcome: Outcome; |
| } |
|
|
| interface MediaItem { |
| id: string; |
| step: string; |
| timestamp: Date; |
| thumbnail: string; |
| type: 'image' | 'video'; |
| } |
|
|
| interface Findings { |
| acetowhite: string; |
| borders: string; |
| vascularPattern: string; |
| lugolsUptake: string; |
| } |
|
|
| interface Outcome { |
| impression: string; |
| biopsyTaken: boolean; |
| plan: string; |
| } |
|
|
| |
| const dummyExams: ExamRecord[] = [ |
| { |
| id: 'COLPO-2025-001', |
| date: new Date('2025-01-10'), |
| indication: 'VIA positive', |
| impression: 'Low-grade lesion', |
| tzType: 'Type 2', |
| biopsy: 'Taken', |
| finalStatus: 'Follow-up advised', |
| performedBy: 'Dr. Sarah Johnson', |
| scjVisibility: 'Fully', |
| media: [ |
| { id: '1', step: 'Native', timestamp: new Date('2025-01-10T09:00:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '2', step: 'Acetic Acid (1 min)', timestamp: new Date('2025-01-10T09:01:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '3', step: 'Acetic Acid (3 min)', timestamp: new Date('2025-01-10T09:03:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '4', step: 'Green Filter', timestamp: new Date('2025-01-10T09:05:00'), thumbnail: '/greenC87Aceto_(1).jpg', type: 'image' }, |
| { id: '5', step: 'Lugol\'s Iodine', timestamp: new Date('2025-01-10T09:07:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '6', step: 'Biopsy Marking', timestamp: new Date('2025-01-10T09:10:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' } |
| ], |
| findings: { |
| acetowhite: 'Thin, rapidly fading', |
| borders: 'Irregular', |
| vascularPattern: 'Fine punctation', |
| lugolsUptake: 'Partial' |
| }, |
| outcome: { |
| impression: 'Low-grade squamous intraepithelial lesion (LSIL)', |
| biopsyTaken: true, |
| plan: 'Follow-up in 6 months with repeat colposcopy' |
| } |
| }, |
| { |
| id: 'COLPO-2025-002', |
| date: new Date('2025-02-15'), |
| indication: 'Follow-up', |
| impression: 'Normal', |
| tzType: 'Type 1', |
| biopsy: 'Not Taken', |
| finalStatus: 'Discharged', |
| performedBy: 'Dr. Sarah Johnson', |
| scjVisibility: 'Fully', |
| media: [ |
| { id: '7', step: 'Native', timestamp: new Date('2025-02-15T10:30:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '8', step: 'Acetic Acid (1 min)', timestamp: new Date('2025-02-15T10:31:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '9', step: 'Acetic Acid (3 min)', timestamp: new Date('2025-02-15T10:33:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' } |
| ], |
| findings: { |
| acetowhite: 'None', |
| borders: 'Regular', |
| vascularPattern: 'Normal', |
| lugolsUptake: 'Complete' |
| }, |
| outcome: { |
| impression: 'Normal colposcopy', |
| biopsyTaken: false, |
| plan: 'Return to routine screening' |
| } |
| }, |
| { |
| id: 'COLPO-2025-003', |
| date: new Date('2025-03-20'), |
| indication: 'Abnormal cytology', |
| impression: 'High-grade lesion', |
| tzType: 'Type 3', |
| biopsy: 'Taken', |
| finalStatus: 'Referred to oncology', |
| performedBy: 'Dr. Sarah Johnson', |
| scjVisibility: 'Partially', |
| media: [ |
| { id: '10', step: 'Native', timestamp: new Date('2025-03-20T11:00:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '11', step: 'Acetic Acid (1 min)', timestamp: new Date('2025-03-20T11:01:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '12', step: 'Acetic Acid (3 min)', timestamp: new Date('2025-03-20T11:03:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '13', step: 'Green Filter', timestamp: new Date('2025-03-20T11:05:00'), thumbnail: '/greenC87Aceto_(1).jpg', type: 'image' }, |
| { id: '14', step: 'Lugol\'s Iodine', timestamp: new Date('2025-03-20T11:07:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' }, |
| { id: '15', step: 'Biopsy Marking', timestamp: new Date('2025-03-20T11:10:00'), thumbnail: '/C87Aceto_(1).jpg', type: 'image' } |
| ], |
| findings: { |
| acetowhite: 'Dense, persistent', |
| borders: 'Irregular, jagged', |
| vascularPattern: 'Coarse mosaicism', |
| lugolsUptake: 'Negative staining' |
| }, |
| outcome: { |
| impression: 'High-grade squamous intraepithelial lesion (HSIL)', |
| biopsyTaken: true, |
| plan: 'Referred to gynecologic oncology for further management' |
| } |
| } |
| ]; |
|
|
| type Props = { |
| goBack: () => void; |
| }; |
|
|
| export function ExaminationRecordsPage({ goBack }: Props) { |
| const [selectedExam, setSelectedExam] = useState<ExamRecord | null>(null); |
| const [searchTerm, setSearchTerm] = useState(''); |
| const [filterStatus, setFilterStatus] = useState<string>('all'); |
|
|
| const filteredExams = dummyExams.filter(exam => { |
| const matchesSearch = exam.id.toLowerCase().includes(searchTerm.toLowerCase()) || |
| exam.indication.toLowerCase().includes(searchTerm.toLowerCase()) || |
| exam.impression.toLowerCase().includes(searchTerm.toLowerCase()); |
|
|
| const matchesFilter = filterStatus === 'all' || exam.finalStatus.toLowerCase().includes(filterStatus.toLowerCase()); |
|
|
| return matchesSearch && matchesFilter; |
| }); |
|
|
| const formatDate = (date: Date) => { |
| return date.toLocaleDateString('en-US', { |
| year: 'numeric', |
| month: 'short', |
| day: 'numeric' |
| }); |
| }; |
|
|
| const formatTime = (date: Date) => { |
| return date.toLocaleTimeString('en-US', { |
| hour: '2-digit', |
| minute: '2-digit' |
| }); |
| }; |
|
|
| if (selectedExam) { |
| return ( |
| <div className="w-full bg-white/95 relative"> |
| <div className="relative z-10 py-4 md:py-6 lg:py-8"> |
| <div className="w-full max-w-7xl mx-auto px-4 md:px-6"> |
| |
| {/* Header */} |
| <div className="mb-6 flex items-center gap-4"> |
| <button |
| onClick={() => setSelectedExam(null)} |
| className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-600" |
| > |
| <ArrowLeft className="w-5 h-5" /> |
| </button> |
| <div> |
| <h1 className="text-2xl md:text-3xl font-bold text-[#0A2540]"> |
| Examination Record: {selectedExam.id} |
| </h1> |
| <p className="text-gray-600 mt-1"> |
| {formatDate(selectedExam.date)} • Patient ID: PT-2025-8492 |
| </p> |
| </div> |
| </div> |
| |
| <div className="space-y-6"> |
| |
| {/* Examination Summary */} |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> |
| <h2 className="text-xl font-bold text-[#0A2540] mb-4 flex items-center gap-2"> |
| <FileText className="w-5 h-5 text-[#05998c]" /> |
| Examination Summary |
| </h2> |
| |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
| <div className="space-y-3"> |
| <div className="flex items-center gap-2 text-sm"> |
| <Calendar className="w-4 h-4 text-gray-500" /> |
| <span className="font-medium text-gray-700">Date & Time:</span> |
| <span>{formatDate(selectedExam.date)} at {formatTime(selectedExam.date)}</span> |
| </div> |
| {selectedExam.performedBy && ( |
| <div className="flex items-center gap-2 text-sm"> |
| <User className="w-4 h-4 text-gray-500" /> |
| <span className="font-medium text-gray-700">Performed by:</span> |
| <span>{selectedExam.performedBy}</span> |
| </div> |
| )} |
| <div className="flex items-center gap-2 text-sm"> |
| <span className="font-medium text-gray-700">Indication:</span> |
| <span>{selectedExam.indication}</span> |
| </div> |
| </div> |
| |
| <div className="space-y-3"> |
| <div className="flex items-center gap-2 text-sm"> |
| <span className="font-medium text-gray-700">SCJ Visibility:</span> |
| <span>{selectedExam.scjVisibility}</span> |
| </div> |
| <div className="flex items-center gap-2 text-sm"> |
| <span className="font-medium text-gray-700">TZ Type:</span> |
| <span>{selectedExam.tzType}</span> |
| </div> |
| </div> |
| |
| <div className="space-y-3"> |
| <div className="flex items-center gap-2 text-sm"> |
| <span className="font-medium text-gray-700">Impression:</span> |
| <span className="font-semibold text-[#05998c]">{selectedExam.impression}</span> |
| </div> |
| <div className="flex items-center gap-2 text-sm"> |
| <span className="font-medium text-gray-700">Biopsy:</span> |
| <span className={selectedExam.biopsy === 'Taken' ? 'text-red-600 font-semibold' : 'text-green-600'}> |
| {selectedExam.biopsy} |
| </span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Media Timeline */} |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> |
| <h2 className="text-xl font-bold text-[#0A2540] mb-4 flex items-center gap-2"> |
| <Camera className="w-5 h-5 text-[#05998c]" /> |
| Media Timeline |
| </h2> |
| |
| <div className="space-y-4"> |
| {selectedExam.media.map((item, index) => ( |
| <div key={item.id} className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg"> |
| <div className="relative"> |
| <img |
| src={item.thumbnail} |
| alt={item.step} |
| className="w-16 h-16 object-cover rounded-lg border-2 border-gray-200" |
| /> |
| <div className="absolute -top-2 -right-2 bg-[#05998c] text-white text-xs px-2 py-1 rounded-full font-semibold"> |
| {index + 1} |
| </div> |
| </div> |
| |
| <div className="flex-1"> |
| <h3 className="font-semibold text-[#0A2540]">{item.step}</h3> |
| <div className="flex items-center gap-2 text-sm text-gray-600 mt-1"> |
| <Clock className="w-4 h-4" /> |
| <span>{formatTime(item.timestamp)}</span> |
| <span className="text-gray-400">•</span> |
| <span className="capitalize">{item.type}</span> |
| </div> |
| </div> |
| |
| <button className="px-4 py-2 bg-[#05998c] text-white rounded-lg hover:bg-[#047569] transition-colors flex items-center gap-2"> |
| <Eye className="w-4 h-4" /> |
| View |
| </button> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Key Findings */} |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> |
| <h2 className="text-xl font-bold text-[#0A2540] mb-4">Key Findings</h2> |
| |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| <div className="space-y-3"> |
| <div className="flex justify-between items-center py-2 border-b border-gray-100"> |
| <span className="font-medium text-gray-700">Acetowhite:</span> |
| <span className="text-gray-900">{selectedExam.findings.acetowhite}</span> |
| </div> |
| <div className="flex justify-between items-center py-2 border-b border-gray-100"> |
| <span className="font-medium text-gray-700">Borders:</span> |
| <span className="text-gray-900">{selectedExam.findings.borders}</span> |
| </div> |
| </div> |
| |
| <div className="space-y-3"> |
| <div className="flex justify-between items-center py-2 border-b border-gray-100"> |
| <span className="font-medium text-gray-700">Vascular Pattern:</span> |
| <span className="text-gray-900">{selectedExam.findings.vascularPattern}</span> |
| </div> |
| <div className="flex justify-between items-center py-2 border-b border-gray-100"> |
| <span className="font-medium text-gray-700">Lugol's Uptake:</span> |
| <span className="text-gray-900">{selectedExam.findings.lugolsUptake}</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Outcome & Plan */} |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> |
| <h2 className="text-xl font-bold text-[#0A2540] mb-4">Outcome & Plan</h2> |
| |
| <div className="space-y-4"> |
| <div> |
| <h3 className="font-semibold text-gray-700 mb-2">Colposcopic Impression</h3> |
| <p className="text-[#0A2540] font-medium">{selectedExam.outcome.impression}</p> |
| </div> |
| |
| <div> |
| <h3 className="font-semibold text-gray-700 mb-2">Biopsy Status</h3> |
| <p className={`font-medium ${selectedExam.outcome.biopsyTaken ? 'text-red-600' : 'text-green-600'}`}> |
| {selectedExam.outcome.biopsyTaken ? 'Biopsy taken' : 'No biopsy taken'} |
| </p> |
| </div> |
| |
| <div> |
| <h3 className="font-semibold text-gray-700 mb-2">Management Plan</h3> |
| <p className="text-gray-900 bg-gray-50 p-3 rounded-lg">{selectedExam.outcome.plan}</p> |
| </div> |
| </div> |
| </div> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className="w-full bg-white/95 relative"> |
| <div className="relative z-10 py-4 md:py-6 lg:py-8"> |
| <div className="w-full max-w-7xl mx-auto px-4 md:px-6"> |
| |
| {/* Header */} |
| <div className="mb-6 flex items-center justify-between"> |
| <div className="flex items-center gap-4"> |
| <button |
| onClick={goBack} |
| className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-600" |
| > |
| <ArrowLeft className="w-5 h-5" /> |
| </button> |
| <div> |
| <h1 className="text-2xl md:text-3xl font-bold text-[#0A2540]">Examination Records</h1> |
| <p className="text-gray-600 mt-1">Patient ID: PT-2025-8492 • {dummyExams.length} examinations</p> |
| </div> |
| </div> |
| |
| {/* Search and Filter */} |
| <div className="flex items-center gap-4"> |
| <div className="relative"> |
| <Search className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" /> |
| <input |
| type="text" |
| placeholder="Search exams..." |
| value={searchTerm} |
| onChange={(e) => setSearchTerm(e.target.value)} |
| className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#05998c] focus:border-transparent" |
| /> |
| </div> |
| |
| <div className="relative"> |
| <Filter className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" /> |
| <select |
| value={filterStatus} |
| onChange={(e) => setFilterStatus(e.target.value)} |
| className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#05998c] focus:border-transparent" |
| > |
| <option value="all">All Status</option> |
| <option value="follow-up">Follow-up</option> |
| <option value="discharged">Discharged</option> |
| <option value="referred">Referred</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| |
| {/* Examination List */} |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> |
| <div className="overflow-x-auto"> |
| <table className="w-full"> |
| <thead className="bg-gray-50 border-b border-gray-200"> |
| <tr> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Exam ID</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Date</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Indication</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Impression</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">TZ Type</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Biopsy</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th> |
| <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-gray-200"> |
| {filteredExams.map((exam) => ( |
| <tr key={exam.id} className="hover:bg-gray-50"> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <div className="font-mono font-semibold text-[#0A2540]">{exam.id}</div> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <div className="text-sm text-gray-900">{formatDate(exam.date)}</div> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <div className="text-sm text-gray-900">{exam.indication}</div> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <div className="text-sm text-gray-900">{exam.impression}</div> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <div className="text-sm text-gray-900">{exam.tzType}</div> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${ |
| exam.biopsy === 'Taken' |
| ? 'bg-red-100 text-red-800' |
| : 'bg-green-100 text-green-800' |
| }`}> |
| {exam.biopsy} |
| </span> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${ |
| exam.finalStatus.includes('Follow-up') |
| ? 'bg-yellow-100 text-yellow-800' |
| : exam.finalStatus.includes('Discharged') |
| ? 'bg-green-100 text-green-800' |
| : 'bg-red-100 text-red-800' |
| }`}> |
| {exam.finalStatus} |
| </span> |
| </td> |
| <td className="px-6 py-4 whitespace-nowrap"> |
| <button |
| onClick={() => setSelectedExam(exam)} |
| className="text-[#05998c] hover:text-[#047569] font-medium text-sm flex items-center gap-1" |
| > |
| <Eye className="w-4 h-4" /> |
| View |
| </button> |
| </td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| |
| {filteredExams.length === 0 && ( |
| <div className="text-center py-12"> |
| <FileText className="w-12 h-12 text-gray-300 mx-auto mb-4" /> |
| <p className="text-gray-500">No examination records found</p> |
| </div> |
| )} |
| </div> |
| |
| </div> |
| </div> |
| </div> |
| ); |
| } |