| import React, { useState, useEffect } from 'react'; |
| import { CheckSquare, FileText } from 'lucide-react'; |
| import { sessionStore } from '../store/sessionStore'; |
|
|
| interface ImagingObservationsProps { |
| onObservationsChange?: (observations: any) => void; |
| layout?: 'vertical' | 'horizontal'; |
| stepId?: 'native' | 'acetowhite' | 'greenFilter' | 'lugol' | 'biopsyMarking'; |
| } |
| export function ImagingObservations({ |
| onObservationsChange, |
| layout = 'vertical', |
| stepId |
| }: ImagingObservationsProps) { |
| const [observations, setObservations] = useState({ |
| obviousGrowths: false, |
| contactBleeding: false, |
| irregularSurface: false, |
| other: false, |
| additionalNotes: '', |
| |
| cervixFullyVisible: null as null | 'Yes' | 'No', |
| obscuredBy: { |
| blood: false, |
| inflammation: false, |
| discharge: false, |
| scarring: false |
| }, |
| adequacyNotes: '', |
| |
| scjVisibility: 'Completely visible', |
| scjNotes: '', |
| tzType: 'TZ 1', |
| |
| suspiciousAtNativeView: false, |
| skipStainInterpretation: false |
| }); |
| const handleCheckboxChange = (field: string) => { |
| const updated = { |
| ...observations, |
| [field]: !observations[field as keyof typeof observations] |
| }; |
| setObservations(updated); |
| if (onObservationsChange) { |
| onObservationsChange(updated); |
| } |
| }; |
| const handleFieldChange = (field: string, value: any) => { |
| const updated = { |
| ...observations, |
| [field]: value |
| }; |
| setObservations(updated); |
| if (onObservationsChange) { |
| onObservationsChange(updated); |
| } |
| }; |
| const handleObscuredChange = (key: string) => { |
| const updatedObscured = { |
| ...observations.obscuredBy, |
| [key]: !observations.obscuredBy[key as keyof typeof observations.obscuredBy] |
| }; |
| const updated = { |
| ...observations, |
| obscuredBy: updatedObscured |
| }; |
| setObservations(updated); |
| if (onObservationsChange) onObservationsChange(updated); |
| }; |
| const handleNotesChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { |
| const updated = { |
| ...observations, |
| additionalNotes: e.target.value |
| }; |
| setObservations(updated); |
| if (onObservationsChange) { |
| onObservationsChange(updated); |
| } |
| }; |
|
|
| |
| useEffect(() => { |
| if (!stepId) return; |
| const session = sessionStore.get(); |
| if (session.stepFindings?.[stepId]) { |
| setObservations(prev => ({ |
| ...prev, |
| ...session.stepFindings[stepId] |
| })); |
| console.log(`[ImagingObservations] Loaded saved findings for step: ${stepId}`, session.stepFindings[stepId]); |
| } |
| }, [stepId]); |
|
|
| |
| useEffect(() => { |
| if (!stepId) return; |
| |
| |
| const timer = setTimeout(() => { |
| const session = sessionStore.get(); |
| const newStepFindings = { |
| ...(session.stepFindings || {}), |
| [stepId]: observations |
| }; |
| sessionStore.merge({ stepFindings: newStepFindings }); |
| console.log(`[ImagingObservations] Saved observations for step: ${stepId}`, observations); |
| }, 500); |
|
|
| return () => clearTimeout(timer); |
| }, [stepId, JSON.stringify(observations)]); |
|
|
| return <div className={`bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden ${layout === 'vertical' ? 'h-full flex flex-col' : ''}`}> |
| <div className="bg-teal-50/50 p-3 md:p-4 border-b border-teal-100 flex items-center gap-2 md:gap-3"> |
| <div className="w-8 h-8 md:w-10 md:h-10 rounded-full bg-teal-100 text-teal-700 flex items-center justify-center flex-shrink-0"> |
| <CheckSquare className="w-4 h-4 md:w-5 md:h-5" /> |
| </div> |
| <h3 className="font-bold text-sm md:text-base text-[#0A2540]">Visual Observations</h3> |
| </div> |
| |
| <div className={`p-4 md:p-6 ${layout === 'horizontal' ? 'space-y-6 flex-1 overflow-y-auto' : 'space-y-4 md:space-y-6 flex-1 overflow-y-auto'}`}> |
| {layout === 'horizontal' && ( |
| <div className="space-y-6"> |
| {/* Row: Cervix fully visible */} |
| <div className="pb-4 border-b border-gray-100"> |
| <div className="flex flex-col md:flex-row md:items-center gap-3 md:gap-6"> |
| <label className="text-sm md:text-base font-semibold text-gray-900 min-w-fit">Cervix fully visible?</label> |
| <div className="flex items-center gap-2"> |
| {['Yes', 'No'].map(opt => ( |
| <label key={opt} className={`px-4 py-2 rounded-lg border cursor-pointer transition-all text-xs md:text-sm font-medium ${observations.cervixFullyVisible === opt ? 'border-teal-300 bg-teal-50 text-teal-700' : 'border-gray-200 bg-gray-50 text-gray-600'}`}> |
| <input type="radio" name="cervixVisible" checked={observations.cervixFullyVisible === opt} onChange={() => handleFieldChange('cervixFullyVisible', opt)} className="mr-2" /> |
| {opt} |
| </label> |
| ))} |
| </div> |
| </div> |
| </div> |
| |
| {/* Row: Obscured by */} |
| <div className="pb-4 border-b border-gray-100"> |
| <label className="block text-sm md:text-base font-semibold text-gray-900 mb-3">Obscured by:</label> |
| <div className="flex flex-wrap gap-2"> |
| {['blood', 'inflammation', 'discharge', 'scarring'].map(k => <label key={k} className="flex items-center gap-2 px-4 py-2 rounded-lg border bg-gray-50 border-gray-200 cursor-pointer hover:border-teal-200 transition-all"> |
| <input type="checkbox" checked={observations.obscuredBy[k as keyof typeof observations.obscuredBy]} onChange={() => handleObscuredChange(k)} className="w-4 h-4" /> |
| <span className="text-sm capitalize">{k}</span> |
| </label>)} |
| </div> |
| </div> |
| |
| {/* Row: Adequacy notes */} |
| <div className="pb-4 border-b border-gray-100"> |
| <label className="block text-sm font-semibold text-gray-900 mb-2">Adequacy Notes</label> |
| <textarea value={observations.adequacyNotes} onChange={e => handleFieldChange('adequacyNotes', e.target.value)} rows={2} className="w-full px-3 py-2 bg-gray-50 border-2 border-gray-200 rounded-lg text-sm focus:border-teal-300 focus:ring-2 focus:ring-teal-100 outline-none transition-all" placeholder="Notes about adequacy..." /> |
| </div> |
| |
| {/* Row: SCJ Visibility */} |
| <div className="pb-4 border-b border-gray-100"> |
| <label className="block text-sm md:text-base font-semibold text-gray-900 mb-3">SCJ Visibility</label> |
| <div className="flex flex-wrap items-center gap-2"> |
| {['Completely visible', 'Partially visible', 'Not visible'].map(opt => <label key={opt} className={`px-4 py-2 rounded-lg border cursor-pointer transition-all text-xs md:text-sm font-medium ${observations.scjVisibility === opt ? 'border-teal-300 bg-teal-50 text-teal-700' : 'border-gray-200 bg-gray-50 text-gray-600'}`}> |
| <input type="radio" name="scj" checked={observations.scjVisibility === opt} onChange={() => handleFieldChange('scjVisibility', opt)} className="mr-2" /> |
| {opt} |
| </label>)} |
| </div> |
| <div className="mt-3"> |
| <textarea value={observations.scjNotes} onChange={e => handleFieldChange('scjNotes', e.target.value)} rows={2} className="w-full px-3 py-2 bg-gray-50 border-2 border-gray-200 rounded-lg text-sm focus:border-teal-300 focus:ring-2 focus:ring-teal-100 outline-none transition-all" placeholder="SCJ notes..." /> |
| </div> |
| </div> |
| |
| {/* Row: Transformation Zone Type */} |
| <div className="pb-4 border-b border-gray-100"> |
| <label className="block text-sm md:text-base font-semibold text-gray-900 mb-3">Transformation Zone (TZ) Type</label> |
| <div className="flex items-center gap-2"> |
| {['TZ 1', 'TZ 2', 'TZ 3'].map(tz => ( |
| <label key={tz} className={`px-4 py-2 rounded-lg border cursor-pointer transition-all text-xs md:text-sm font-medium ${observations.tzType === tz ? 'border-teal-300 bg-teal-50 text-teal-700' : 'border-gray-200 bg-gray-50 text-gray-600'}`}> |
| <input type="radio" name="tzType" checked={observations.tzType === tz} onChange={() => handleFieldChange('tzType', tz)} className="mr-2" /> |
| {tz} |
| </label> |
| ))} |
| </div> |
| </div> |
| |
| {/* Row: Native (Untreated) Examination */} |
| <div className="pb-4 border-b border-gray-100"> |
| <label className="block text-sm md:text-base font-semibold text-gray-900 mb-3">Native (Untreated) Examination</label> |
| <label className="flex items-center gap-3 cursor-pointer"> |
| <input type="checkbox" checked={observations.suspiciousAtNativeView} onChange={() => { |
| const next = !observations.suspiciousAtNativeView; |
| const updated = { |
| ...observations, |
| suspiciousAtNativeView: next, |
| skipStainInterpretation: next |
| }; |
| setObservations(updated); |
| if (onObservationsChange) onObservationsChange(updated); |
| }} className="w-5 h-5 rounded text-teal-600 border-gray-300" /> |
| <span className="text-sm font-medium text-gray-900">Suspicious at Native View</span> |
| </label> |
| {observations.suspiciousAtNativeView && <div className="mt-3"> |
| <textarea value={observations.adequacyNotes} onChange={e => handleFieldChange('adequacyNotes', e.target.value)} rows={2} className="w-full px-3 py-2 bg-gray-50 border-2 border-gray-200 rounded-lg text-sm focus:border-teal-300 focus:ring-2 focus:ring-teal-100 outline-none transition-all" placeholder="Additional notes..." /> |
| </div>} |
| </div> |
| </div> |
| )} |
| |
| {layout !== 'horizontal' && ( |
| <> |
| {/* Default Vertical Layout - kept for non-native pages */} |
| <div> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-2 md:mb-3"> |
| Examination Adequacy Checklist |
| </label> |
| <div className="space-y-2 md:space-y-3"> |
| <div className="flex flex-col md:flex-row md:items-center gap-2 md:gap-4"> |
| <label className="text-xs md:text-sm font-medium whitespace-nowrap">Cervix fully visible?</label> |
| <div className="flex items-center gap-2"> |
| {['Yes', 'No'].map(opt => ( |
| <label key={opt} className={`px-3 py-1 rounded-lg border cursor-pointer text-xs md:text-sm ${observations.cervixFullyVisible === opt ? 'bg-teal-50 border-teal-200' : 'bg-gray-50 border-gray-200'}`}> |
| <input type="radio" name="cervixVisibleVertical" checked={observations.cervixFullyVisible === opt} onChange={() => handleFieldChange('cervixFullyVisible', opt)} className="mr-2" /> |
| {opt} |
| </label> |
| ))} |
| </div> |
| </div> |
| |
| <div> |
| <span className="block text-sm font-semibold text-gray-900">Obscured by:</span> |
| <div className="mt-2 grid grid-cols-2 gap-2"> |
| {['blood', 'inflammation', 'discharge', 'scarring'].map(k => <label key={k} className="flex items-center gap-2 p-3 rounded-lg border bg-gray-50 border-gray-200 cursor-pointer"> |
| <input type="checkbox" checked={observations.obscuredBy[k as keyof typeof observations.obscuredBy]} onChange={() => handleObscuredChange(k)} className="w-4 h-4" /> |
| <span className="text-sm capitalize">{k}</span> |
| </label>)} |
| </div> |
| </div> |
| |
| <div className="pt-2"> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-2">Additional notes</label> |
| <textarea value={observations.adequacyNotes} onChange={e => handleFieldChange('adequacyNotes', e.target.value)} rows={3} className="w-full px-3 py-2 bg-gray-50 border-2 border-gray-200 rounded-lg text-sm" placeholder="Notes about adequacy..." /> |
| </div> |
| </div> |
| </div> |
| |
| {/* SCJ Visibility and TZ */} |
| <div className="pt-4 border-t border-gray-100"> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-3">SCJ Visibility</label> |
| <div className="flex items-center gap-3"> |
| {['Completely visible', 'Partially visible', 'Not visible'].map(opt => <label key={opt} className={`p-3 rounded-lg border ${observations.scjVisibility === opt ? 'border-teal-200 bg-teal-50' : 'border-gray-200 bg-gray-50'} cursor-pointer`}> |
| <input type="radio" name="scj" checked={observations.scjVisibility === opt} onChange={() => handleFieldChange('scjVisibility', opt)} className="mr-2" /> |
| <span className="text-sm">{opt}</span> |
| </label>)} |
| </div> |
| <div className="mt-3"> |
| <textarea value={observations.scjNotes} onChange={e => handleFieldChange('scjNotes', e.target.value)} rows={2} className="w-full px-3 py-2 bg-gray-50 border-2 border-gray-200 rounded-lg text-sm" placeholder="SCJ notes..." /> |
| </div> |
| |
| <div className="mt-4"> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-2">Transformation Zone (TZ) Type</label> |
| <div className="flex items-center gap-2"> |
| {['TZ 1', 'TZ 2', 'TZ 3'].map(tz => ( |
| <label key={tz} className={`px-3 py-2 rounded-lg border cursor-pointer text-sm ${observations.tzType === tz ? 'border-teal-200 bg-teal-50' : 'border-gray-200 bg-gray-50'}`}> |
| <input type="radio" name="tzTypeVertical" checked={observations.tzType === tz} onChange={() => handleFieldChange('tzType', tz)} className="mr-2" /> |
| {tz} |
| </label> |
| ))} |
| </div> |
| </div> |
| </div> |
| |
| {/* Native (Untreated) Examination */} |
| <div className="pt-4 border-t border-gray-100"> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-3">Native (Untreated) Examination (STEP 2)</label> |
| <div className="flex items-center gap-3"> |
| <label className="flex items-center gap-2"> |
| <input type="checkbox" checked={observations.suspiciousAtNativeView} onChange={() => { |
| const next = !observations.suspiciousAtNativeView; |
| const updated = { |
| ...observations, |
| suspiciousAtNativeView: next, |
| skipStainInterpretation: next |
| }; |
| setObservations(updated); |
| if (onObservationsChange) onObservationsChange(updated); |
| }} className="w-4 h-4" /> |
| <span className="text-sm font-medium">Suspicious at Native View</span> |
| </label> |
| </div> |
| {observations.suspiciousAtNativeView && <div className="mt-3"> |
| <textarea value={observations.adequacyNotes} onChange={e => handleFieldChange('adequacyNotes', e.target.value)} rows={2} className="w-full px-3 py-2 bg-gray-50 border-2 border-gray-200 rounded-lg text-sm" placeholder="Additional notes..." /> |
| </div>} |
| </div> |
| </> |
| )} |
| |
| <div> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-3"> |
| Clinical Findings |
| </label> |
| <div className="space-y-3"> |
| <label className="flex items-start p-4 rounded-lg border-2 border-gray-200 hover:border-[#05998c]/50 cursor-pointer transition-all bg-gray-50/50 group"> |
| <input type="checkbox" checked={observations.obviousGrowths} onChange={() => handleCheckboxChange('obviousGrowths')} className="w-5 h-5 rounded text-[#05998c] focus:ring-[#05998c] border-gray-300 mt-0.5" /> |
| <div className="ml-3"> |
| <span className="block text-sm font-semibold text-gray-900 group-hover:text-[#0A2540]"> |
| Obvious growths / ulcers |
| </span> |
| <span className="block text-xs text-gray-500 mt-0.5"> |
| Visible abnormal tissue growth or ulceration |
| </span> |
| </div> |
| </label> |
| |
| <label className="flex items-start p-4 rounded-lg border-2 border-gray-200 hover:border-[#05998c]/50 cursor-pointer transition-all bg-gray-50/50 group"> |
| <input type="checkbox" checked={observations.contactBleeding} onChange={() => handleCheckboxChange('contactBleeding')} className="w-5 h-5 rounded text-[#05998c] focus:ring-[#05998c] border-gray-300 mt-0.5" /> |
| <div className="ml-3"> |
| <span className="block text-sm font-semibold text-gray-900 group-hover:text-[#0A2540]"> |
| Contact bleeding |
| </span> |
| <span className="block text-xs text-gray-500 mt-0.5"> |
| Bleeding upon contact with instrument |
| </span> |
| </div> |
| </label> |
| |
| <label className="flex items-start p-4 rounded-lg border-2 border-gray-200 hover:border-[#05998c]/50 cursor-pointer transition-all bg-gray-50/50 group"> |
| <input type="checkbox" checked={observations.irregularSurface} onChange={() => handleCheckboxChange('irregularSurface')} className="w-5 h-5 rounded text-[#05998c] focus:ring-[#05998c] border-gray-300 mt-0.5" /> |
| <div className="ml-3"> |
| <span className="block text-sm font-semibold text-gray-900 group-hover:text-[#0A2540]"> |
| Irregular surface |
| </span> |
| <span className="block text-xs text-gray-500 mt-0.5"> |
| Uneven or abnormal surface texture |
| </span> |
| </div> |
| </label> |
| |
| <label className="flex items-start p-4 rounded-lg border-2 border-gray-200 hover:border-[#05998c]/50 cursor-pointer transition-all bg-gray-50/50 group"> |
| <input type="checkbox" checked={observations.other} onChange={() => handleCheckboxChange('other')} className="w-5 h-5 rounded text-[#05998c] focus:ring-[#05998c] border-gray-300 mt-0.5" /> |
| <div className="ml-3"> |
| <span className="block text-sm font-semibold text-gray-900 group-hover:text-[#0A2540]"> |
| Other findings |
| </span> |
| <span className="block text-xs text-gray-500 mt-0.5"> |
| Additional observations not listed above |
| </span> |
| </div> |
| </label> |
| </div> |
| </div> |
| |
| <div className="pt-6 border-t border-gray-100"> |
| <label className="block text-xs font-semibold text-gray-500 uppercase mb-3 flex items-center gap-2"> |
| <FileText className="w-4 h-4" /> |
| Additional Notes |
| </label> |
| <textarea value={observations.additionalNotes} onChange={handleNotesChange} rows={6} className="w-full px-4 py-3 bg-gray-50 border-2 border-gray-200 rounded-lg focus:ring-2 focus:ring-[#05998c] focus:border-[#05998c] outline-none transition-all resize-none text-sm" placeholder="Document any additional observations, measurements, or clinical notes..." /> |
| </div> |
| |
| <div className="bg-blue-50 rounded-lg p-4 border border-blue-100"> |
| <p className="text-xs text-blue-800 leading-relaxed"> |
| <strong>Tip:</strong> Use the annotation tool to mark specific areas |
| of concern on the image, then document your findings here. |
| </p> |
| </div> |
| </div> |
| </div>; |
| } |