Pathora_Colposcopy_Assistant / src /components /ImagingObservations.tsx
nusaibah0110's picture
Update application with new features and components
1c68fe6
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: '',
// Examination Adequacy
cervixFullyVisible: null as null | 'Yes' | 'No',
obscuredBy: {
blood: false,
inflammation: false,
discharge: false,
scarring: false
},
adequacyNotes: '',
// SCJ & TZ
scjVisibility: 'Completely visible',
scjNotes: '',
tzType: 'TZ 1',
// Native exam
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);
}
};
// Load previously saved findings for this step from sessionStore on mount
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]);
// Save observations to sessionStore for this step on every change with debouncing
useEffect(() => {
if (!stepId) return;
// Debounce: save after 500ms of inactivity
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>;
}