| | 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>; |
| | } |