From 6473c2f24a69f623a3412aa07807c6d0b81c07c7 Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 14 Jan 2026 21:48:08 -0300 Subject: [PATCH 01/16] Import/export fix & expansion --- ...tControls.tsx => ExportImportPatients.tsx} | 141 ++- .../patients/ExportImportSessions.tsx | 263 +++++ src/components/patients/PatientDetails.tsx | 32 +- src/components/patients/PatientList.tsx | 4 +- src/i18n/locales/es.json | 17 +- src/lib/export/dird-exporter.ts | 139 ++- src/lib/export/dird-importer.ts | 911 ++++++++++++++++-- 7 files changed, 1324 insertions(+), 183 deletions(-) rename src/components/patients/{ExportImportControls.tsx => ExportImportPatients.tsx} (60%) create mode 100644 src/components/patients/ExportImportSessions.tsx diff --git a/src/components/patients/ExportImportControls.tsx b/src/components/patients/ExportImportPatients.tsx similarity index 60% rename from src/components/patients/ExportImportControls.tsx rename to src/components/patients/ExportImportPatients.tsx index 39b89ea..52926b3 100644 --- a/src/components/patients/ExportImportControls.tsx +++ b/src/components/patients/ExportImportPatients.tsx @@ -3,6 +3,7 @@ import { Download, Upload, Database } from 'lucide-react'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; + import { Dialog, DialogContent, @@ -10,48 +11,38 @@ import { DialogTitle, DialogDescription, } from '@/components/ui/dialog'; -import { exportPatient, exportAllData, downloadDirdFile } from '@/lib/export/dird-exporter'; -import { importDirdFile } from '@/lib/export/dird-importer'; + +import { exportAllData, downloadDirdFile } from '@/lib/export/dird-exporter'; +import { importDirdFile, importDirdType } from '@/lib/export/dird-importer'; import type { ImportResult } from '@/lib/export/dird-importer'; -interface ExportImportControlsProps { - patientId?: number; - patientName?: string; +interface ExportImportPatientsProps { onImportComplete?: () => void; } -const ExportImportControls: React.FC = ({ - patientId, - patientName, +const ExportImportPatients: React.FC = ({ onImportComplete, }) => { const { t } = useTranslation(); const [showImportDialog, setShowImportDialog] = useState(false); + const [exporting, setExporting] = useState(false); const [importing, setImporting] = useState(false); const [importResult, setImportResult] = useState(null); const fileInputRef = useRef(null); + const closeTimeoutRef = useRef(null); - const handleExportPatient = async () => { - if (!patientId) return; - - try { - const blob = await exportPatient(patientId); - const filename = `paciente_${patientName?.replace(/\s/g, '_')}_${Date.now()}`; - downloadDirdFile(blob, filename); - } catch (error) { - console.error('Error exporting patient:', error); - toast.error(t('export.errorPatient')); - } - }; const handleExportAll = async () => { + setExporting(true); try { const blob = await exportAllData(); - const filename = `dird_backup_${Date.now()}`; - downloadDirdFile(blob, filename); + downloadDirdFile(blob, `dird_backup_${Date.now()}`); + toast.success(t('export.fullSuccess')); } catch (error) { console.error('Error exporting data:', error); toast.error(t('export.errorData')); + } finally { + setExporting(false); } }; @@ -60,15 +51,37 @@ const ExportImportControls: React.FC = ({ setImportResult(null); try { + const type = await importDirdType(file); + + if (type === 'session') { + setImporting(false); + + setImportResult({ + success: false, + error: t('import.invalidFile'), + patientsImported: 0, + sessionsImported: 0, + imagesImported: 0, + detectionsImported: 0, + segmentationsImported: 0, + measurementsImported: 0, + reportsImported: 0 + }); + return; + } + const result = await importDirdFile(file); setImportResult(result); if (result.success) { - setTimeout(() => { + closeTimeoutRef.current = window.setTimeout(() => { setShowImportDialog(false); + setImporting(false); + setImportResult(null); onImportComplete?.(); - }, 3000); + }, 5000); } + } catch (error) { console.error('Error importing file:', error); setImportResult({ @@ -77,6 +90,9 @@ const ExportImportControls: React.FC = ({ sessionsImported: 0, imagesImported: 0, detectionsImported: 0, + segmentationsImported: 0, + measurementsImported: 0, + reportsImported: 0 }); } finally { setImporting(false); @@ -90,34 +106,66 @@ const ExportImportControls: React.FC = ({ } }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0]; + if (file) { + handleImport(file); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + return ( <>
- {patientId && ( - - )} - -
{/* Import Dialog */} - + { + setShowImportDialog(open); + + if (!open) { + if (closeTimeoutRef.current !== null) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + + setImportResult(null); + setImporting(false); + } + + if (fileInputRef.current) {fileInputRef.current.value = '';} + }} + > + - {t('import.title')} + {t('import.patientTitle')} - {t('import.description')} + {t('import.patientDescription')} @@ -133,6 +181,8 @@ const ExportImportControls: React.FC = ({ />
fileInputRef.current?.click()} + onDragOver={handleDragOver} + onDrop={handleDrop} className="border-2 border-dashed border-coal-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary-400 transition-colors" > @@ -166,10 +216,21 @@ const ExportImportControls: React.FC = ({ {t('import.successTitle')}
-

{t('import.patientLabel')}{importResult.patient?.name}

+ + {importResult.import_type === 'patient' && ( +

{t('import.patientLabel')}{importResult.patient?.name}

+ )} + + {importResult.import_type === 'full' && ( +

{t('import.patientsImported')}{importResult.patientsImported}

+ )} +

{t('import.sessionsImported')}{importResult.sessionsImported}

{t('import.imagesImported')}{importResult.imagesImported}

+

{t('import.reportsImported')}{importResult.reportsImported}

{t('import.detectionsImported')}{importResult.detectionsImported}

+

{t('import.segmentationsImported')}{importResult.segmentationsImported}

+

{t('import.measurementsImported')}{importResult.measurementsImported}

) : ( @@ -187,4 +248,4 @@ const ExportImportControls: React.FC = ({ ); }; -export default ExportImportControls; +export default ExportImportPatients; diff --git a/src/components/patients/ExportImportSessions.tsx b/src/components/patients/ExportImportSessions.tsx new file mode 100644 index 0000000..33ee72a --- /dev/null +++ b/src/components/patients/ExportImportSessions.tsx @@ -0,0 +1,263 @@ +import React, { useState, useRef } from 'react'; +import { Download, Upload, Database } from 'lucide-react'; +import { toast } from 'sonner'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; + +import { db } from '@/lib/db/schema'; + +import { useLiveQuery } from 'dexie-react-hooks'; +import { exportPatient, downloadDirdFile } from '@/lib/export/dird-exporter'; +import { importDirdFile, importDirdType } from '@/lib/export/dird-importer'; +import type { ImportResult } from '@/lib/export/dird-importer'; + +interface ExportImportSessionsProps { + patientId?: number; + patientName?: string; + onImportComplete?: () => void; +} + +const ExportImportSessions: React.FC = ({ + patientId, + patientName, + onImportComplete, +}) => { + const { t } = useTranslation(); + const [showImportDialog, setShowImportDialog] = useState(false); + const [importing, setImporting] = useState(false); + const [importResult, setImportResult] = useState(null); + const [isExporting, setIsExporting] = useState(false); + const fileInputRef = useRef(null); + const closeTimeoutRef = useRef(null); + + const patient = useLiveQuery( + () => (patientId ? db.patients.get(patientId) : undefined), + [patientId] + ); + + const handleExportPatient = async () => { + if (!patient) return; + setIsExporting(true); + + try { + const blob = await exportPatient(patientId!); + downloadDirdFile(blob, `dird_export_patient_${patient.patientId}`); + toast.success(t('export.patientSuccess')); + } catch (error) { + console.error('Error exporting patient:', error); + toast.error(t('export.errorPatient')); + } finally { + setIsExporting(false); + } + }; + + const handleImport = async (file: File) => { + setImporting(true); + setImportResult(null); + + try { + const type = await importDirdType(file); + + if (type === 'full') { + setImporting(false); + + setImportResult({ + success: false, + error: t('import.invalidFile'), + sessionsImported: 0, + imagesImported: 0, + detectionsImported: 0, + segmentationsImported: 0, + measurementsImported: 0, + reportsImported: 0 + }); + return; + } + + const result = await importDirdFile(file, patientId); + setImportResult(result); + + if (result.success) { + closeTimeoutRef.current = window.setTimeout(() => { + setShowImportDialog(false); + setImporting(false); + setImportResult(null); + onImportComplete?.(); + }, 5000); + } + + } catch (error) { + console.error('Error importing file:', error); + setImportResult({ + success: false, + error: t('import.processError'), + sessionsImported: 0, + imagesImported: 0, + detectionsImported: 0, + segmentationsImported: 0, + measurementsImported: 0, + reportsImported: 0 + }); + } finally { + setImporting(false); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleImport(file); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0]; + if (file) { + handleImport(file); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + + return ( + <> +
+ {patientId && ( + + )} + + +
+ + {/* Import Dialog */} + { + setShowImportDialog(open); + + if (!open) { + if (closeTimeoutRef.current !== null) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + + setImportResult(null); + setImporting(false); + } + + if (fileInputRef.current) {fileInputRef.current.value = '';} + }} + > + + + + {t('import.sessionTitle')} + + {t('import.sessionDescription')} + + + +
+ {!importing && !importResult && ( + <> + +
fileInputRef.current?.click()} + onDragOver={handleDragOver} + onDrop={handleDrop} + className="border-2 border-dashed border-coal-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary-400 transition-colors" + > + +

+ {t('import.selectFile')} +

+

{t('export.dragAndDrop')}

+
+ + )} + + {importing && ( +
+
+

{t('import.importing')}

+

{t('import.waitMessage')}

+
+ )} + + {importResult && ( +
+ {importResult.success ? ( +
+

+ {t('import.successTitle')} +

+
+ + {importResult.import_type === 'patient' && ( +

{t('import.patientLabel')}{importResult.patient?.name}

+ )} + + {importResult.import_type === 'full' && ( +

{t('import.patientsImported')}{importResult.patientsImported}

+ )} + +

{t('import.sessionsImported')}{importResult.sessionsImported}

+

{t('import.imagesImported')}{importResult.imagesImported}

+

{t('import.reportsImported')}{importResult.reportsImported}

+

{t('import.detectionsImported')}{importResult.detectionsImported}

+

{t('import.segmentationsImported')}{importResult.segmentationsImported}

+

{t('import.measurementsImported')}{importResult.measurementsImported}

+
+
+ ) : ( +
+

{t('import.errorTitle')}

+

{importResult.error}

+
+ )} +
+ )} +
+
+
+ + ); +}; + +export default ExportImportSessions; diff --git a/src/components/patients/PatientDetails.tsx b/src/components/patients/PatientDetails.tsx index a59c7ca..773b096 100644 --- a/src/components/patients/PatientDetails.tsx +++ b/src/components/patients/PatientDetails.tsx @@ -10,7 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import SessionForm from './SessionForm'; import PatientForm from './PatientForm'; import { db, Session, Patient } from '@/lib/db/schema'; -import { exportPatient, downloadDirdFile } from '@/lib/export/dird-exporter'; +import ExportImportSessions from './ExportImportSessions'; import { duplicateSession } from '@/lib/db/actions'; import { createCombinedSession } from '@/lib/db/combinedSessions'; @@ -21,7 +21,6 @@ const PatientDetails: React.FC = () => { const { confirm, ConfirmDialogComponent } = useConfirm(); const [showSessionForm, setShowSessionForm] = useState(false); const [showPatientForm, setShowPatientForm] = useState(false); - const [isExporting, setIsExporting] = useState(false); const [isDuplicating, setIsDuplicating] = useState(null); const [sessionToEdit, setSessionToEdit] = useState(); const [patientToEdit, setPatientToEdit] = useState(); @@ -40,21 +39,6 @@ const PatientDetails: React.FC = () => { [patientId] ); - const handleExportPatient = async () => { - if (!patient) return; - setIsExporting(true); - try { - const blob = await exportPatient(patient.id!); - downloadDirdFile(blob, `dird_export_patient_${patient.patientId}`); - toast.success(t('export.patientSuccess')); - } catch (error) { - console.error('Error exporting patient:', error); - toast.error(t('errors.exportPatient')); - } finally { - setIsExporting(false); - } - }; - const handleDeleteSession = async (sessionId: number) => { const confirmed = await confirm({ title: t('confirmations.deleteSessionTitle') || t('sessions.delete'), @@ -173,9 +157,11 @@ const PatientDetails: React.FC = () => {

{t('patients.idLabel')}{patient.patientId}

-
+
+ + {/* Refresh handled by useLiveQuery */}} /> + -
diff --git a/src/components/patients/PatientList.tsx b/src/components/patients/PatientList.tsx index 5d65993..2ca68fc 100644 --- a/src/components/patients/PatientList.tsx +++ b/src/components/patients/PatientList.tsx @@ -9,7 +9,7 @@ import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import PatientCard, { PatientReportStatus } from './PatientCard'; import PatientForm from './PatientForm'; -import ExportImportControls from './ExportImportControls'; +import ExportImportPatients from './ExportImportPatients'; import { db, Patient } from '@/lib/db/schema'; import { deletePatient } from '@/lib/db/actions'; import { useConfirm } from '@/hooks/useConfirm'; @@ -122,7 +122,7 @@ const PatientList: React.FC = () => {

- {/* Refresh handled by useLiveQuery */}} /> + {/* Refresh handled by useLiveQuery */}} />
- {/* Refresh handled by useLiveQuery */}} /> + {/* Refresh handled by useLiveQuery */}} /> diff --git a/src/components/upload/SessionView.tsx b/src/components/upload/SessionView.tsx index 35f3835..dc4c2b4 100644 --- a/src/components/upload/SessionView.tsx +++ b/src/components/upload/SessionView.tsx @@ -278,7 +278,7 @@ const SessionView: React.FC = () => { )} - {showLargeDropzone && ( + {( Date: Thu, 15 Jan 2026 19:54:09 -0300 Subject: [PATCH 03/16] fix: dragging across the same eye type fixed, cross eye dragging is not possible anymore, extra image field added to schema.ts to control dragging --- src/components/upload/ImageGallery.tsx | 67 ++++++++++++++++++-------- src/lib/db/schema.ts | 1 + 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/components/upload/ImageGallery.tsx b/src/components/upload/ImageGallery.tsx index 03b1469..8b435e9 100644 --- a/src/components/upload/ImageGallery.tsx +++ b/src/components/upload/ImageGallery.tsx @@ -12,7 +12,7 @@ import { useSensor, useSensors, } from '@dnd-kit/core'; -import { SortableContext, useSortable, rectSortingStrategy } from '@dnd-kit/sortable'; +import { SortableContext, useSortable, rectSortingStrategy, arrayMove } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Trash2, Brain, GripVertical, ArrowRight, ArrowLeft, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; @@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next'; import { inferenceService } from '@/lib/ai/inference-service'; import { db } from '@/lib/db/schema'; + interface ImageGalleryProps { images: ImageType[]; patientId: string; @@ -58,6 +59,7 @@ const DraggableImageCard = React.forwardRef { if (image.id) { @@ -297,6 +299,8 @@ const SortableImageCard: React.FC = (props) => { const ImageGallery: React.FC = ({ images, patientId, sessionId, onDelete, isLocked, refreshKey }) => { const [thumbnails, setThumbnails] = useState>(new Map()); const [activeImage, setActiveImage] = useState(null); + const [oiOrder, setOiOrder] = useState([]); + const [odOrder, setOdOrder] = useState([]); const sensors = useSensors( useSensor(PointerSensor, { @@ -306,6 +310,21 @@ const ImageGallery: React.FC = ({ images, patientId, sessionI }) ); const { t } = useTranslation(); + + useEffect(() => { + const oi = images + .filter(i => i.eyeType === 'OI') + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) + .map(i => i.id!); + + const od = images + .filter(i => i.eyeType === 'OD') + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) + .map(i => i.id!); + + setOiOrder(oi); + setOdOrder(od); + }, [images]); useEffect(() => { const newThumbnails = new Map(); @@ -375,10 +394,12 @@ const ImageGallery: React.FC = ({ images, patientId, sessionI }, [images]); const { oiImages, odImages } = useMemo(() => { - const oi = images.filter(img => img.eyeType === 'OI'); - const od = images.filter(img => img.eyeType === 'OD'); - return { oiImages: oi, odImages: od }; - }, [images]); + const byId = new Map(images.map(i => [i.id!, i])); + return { + oiImages: oiOrder.map(id => byId.get(id)!).filter(Boolean), + odImages: odOrder.map(id => byId.get(id)!).filter(Boolean), + }; + }, [images, oiOrder, odOrder]); const handleDragStart = (event: DragStartEvent) => { const { active } = event; @@ -397,25 +418,33 @@ const ImageGallery: React.FC = ({ images, patientId, sessionI setActiveImage(null); const { active, over } = event; - if (!over) { - return; - } + if (!over || active.id === over.id) return; const activeImage = images.find(img => img.id === active.id); - if (!activeImage) { - return; - } + const overImg = images.find(img => img.id === over.id); + if (!activeImage || !overImg) return; - let targetEye: 'OI' | 'OD' | null = null; - if (over.id === 'OI' || oiImages.some(i => i.id === over.id)) { - targetEye = 'OI'; - } else if (over.id === 'OD' || odImages.some(i => i.id === over.id)) { - targetEye = 'OD'; - } + const isOI = activeImage.eyeType === 'OI'; + + const currentOrder = isOI ? oiOrder: odOrder; + const from = currentOrder.indexOf(active.id as number); + const to = currentOrder.indexOf(over.id as number); + if (from === -1 || to === -1) return; + + const nextOrder = arrayMove(currentOrder, from, to); - if (targetEye && activeImage.eyeType !== targetEye) { - handleMove(activeImage.id!, targetEye); + if (isOI) { + setOiOrder(nextOrder); + } else { + setOdOrder(nextOrder); } + + await db.transaction('rw', db.images, async () => { + + for (let i = 0; i < nextOrder.length; i++) { + await db.images.update(nextOrder[i], {order: i}); + } + }); }; const DroppableColumn: React.FC<{ id: 'OI' | 'OD'; title: string; imageList: ImageType[]; className: string; }> = ({ id, title, imageList, className }) => { diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 8ad3d3e..8fe12da 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -42,6 +42,7 @@ export interface Image { sessionId: number; filename: string; eyeType: 'OI' | 'OD'; + order: number; // Ordenar imagenes y actualizar su orden en una sesion originalBlob: Blob; processedBlob?: Blob; width: number; From 53cb44322c4d02c17f499a3a2caa3f6146e8321f Mon Sep 17 00:00:00 2001 From: aaron Date: Fri, 16 Jan 2026 19:14:17 -0300 Subject: [PATCH 04/16] fix: campo order en image ahora no es obligatorio para no romper demo --- src/lib/db/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 8fe12da..6ce406c 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -42,7 +42,7 @@ export interface Image { sessionId: number; filename: string; eyeType: 'OI' | 'OD'; - order: number; // Ordenar imagenes y actualizar su orden en una sesion + order?: number; // Ordenar imagenes y actualizar su orden en una sesion originalBlob: Blob; processedBlob?: Blob; width: number; From 2372826537dda2c997228986700f01b74eb26f0a Mon Sep 17 00:00:00 2001 From: aaron Date: Fri, 16 Jan 2026 19:15:48 -0300 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20eliminacion=20de=20boton=20(a?= =?UTF-8?q?=C3=B1adir=20imagenes)=20en=20favor=20de=20dropzone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/patients/ExportImportPatients.tsx | 2 +- src/components/upload/ImageDropzone.tsx | 14 +++++++------- src/components/upload/SessionView.tsx | 16 +++++++--------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/patients/ExportImportPatients.tsx b/src/components/patients/ExportImportPatients.tsx index 52926b3..a11ac00 100644 --- a/src/components/patients/ExportImportPatients.tsx +++ b/src/components/patients/ExportImportPatients.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef } from 'react'; -import { Download, Upload, Database } from 'lucide-react'; +import { Download, Upload } from 'lucide-react'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; diff --git a/src/components/upload/ImageDropzone.tsx b/src/components/upload/ImageDropzone.tsx index 2451beb..492d061 100644 --- a/src/components/upload/ImageDropzone.tsx +++ b/src/components/upload/ImageDropzone.tsx @@ -73,19 +73,19 @@ const ImageDropzone: React.FC = ({ sessionId, onUploadComple
{t('upload.selectEye')}
- +
diff --git a/src/components/upload/SessionView.tsx b/src/components/upload/SessionView.tsx index dc4c2b4..6573299 100644 --- a/src/components/upload/SessionView.tsx +++ b/src/components/upload/SessionView.tsx @@ -231,7 +231,7 @@ const SessionView: React.FC = () => { return new Date(date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }); }; - const showLargeDropzone = (images.length === 0 || isUploading) && !session.locked; + const showLargeDropzone = !session.locked; return ( <> @@ -259,14 +259,10 @@ const SessionView: React.FC = () => { {isExporting ? t('export.exporting') : t('export.session')} - {session.locked ? ( + {session.locked && ( {t('sessions.locked')} - ) : ( -
- setRefreshKey((prev) => prev + 1)} /> -
)}
@@ -278,7 +274,7 @@ const SessionView: React.FC = () => { )} - {( + {showLargeDropzone && ( {
{t('sessions.galleryTitle')}
- {images.length > 0 && !session.locked && ( + + {/*images.length > 0 && !session.locked && ( - )} + )*/} + {images.length > 0 && !session.locked && ( - -
- -
- - - ); -}; - const SessionView: React.FC = () => { const { patientId, sessionId } = useParams<{ patientId: string; sessionId: string }>(); const navigate = useNavigate(); @@ -157,6 +95,7 @@ const SessionView: React.FC = () => { const handleUploadStart = () => { setIsUploading(true); + return isUploading; }; const handleProcessWithAI = async () => { @@ -296,14 +235,6 @@ const SessionView: React.FC = () => {
{t('sessions.galleryTitle')}
- - {/*images.length > 0 && !session.locked && ( - - )*/} {images.length > 0 && !session.locked && ( + +
+ +
+ + + ); +}; + const SessionView: React.FC = () => { const { patientId, sessionId } = useParams<{ patientId: string; sessionId: string }>(); const navigate = useNavigate(); @@ -214,13 +277,6 @@ const SessionView: React.FC = () => { )} - {showLargeDropzone && ( - - )} @@ -235,6 +291,14 @@ const SessionView: React.FC = () => {
{t('sessions.galleryTitle')}
+ + {images.length > 0 && !session.locked && ( + + )} {images.length > 0 && !session.locked && (
+ + {showLargeDropzone && images.length < 1 && ( + + )} + Date: Sun, 22 Feb 2026 16:47:27 -0300 Subject: [PATCH 16/16] =?UTF-8?q?boton=20de=20edicion=20de=20sesiones=20a?= =?UTF-8?q?=C3=B1adido?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/upload/SessionView.tsx | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/upload/SessionView.tsx b/src/components/upload/SessionView.tsx index 2f1b7e2..5fa2cfd 100644 --- a/src/components/upload/SessionView.tsx +++ b/src/components/upload/SessionView.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { useLiveQuery } from 'dexie-react-hooks'; -import { ArrowLeft, Play, Lock, ChevronRight, Download, Plus } from 'lucide-react'; +import { ArrowLeft, Play, Lock, ChevronRight, Download, Plus, Pencil } from 'lucide-react'; import { toast } from 'sonner'; import { useConfirm } from '@/hooks/useConfirm'; import { useTranslation } from 'react-i18next'; @@ -15,8 +15,9 @@ import ImageGallery from './ImageGallery'; import ReportGenerator from '../reports/ReportGenerator'; import ReportsList from '../reports/ReportsList'; import AnalysisView from './AnalysisView'; +import SessionForm from '@/components/patients/SessionForm'; import UploadProgressModal from './UploadProgressModal'; -import { db } from '@/lib/db/schema'; +import { db, Session } from '@/lib/db/schema'; import { exportSession, downloadDirdFile } from '@/lib/export/dird-exporter'; import { inferenceService } from '@/lib/ai/inference-service'; import { useImageUploader } from '@/hooks/useImageUploader'; @@ -95,6 +96,8 @@ const SessionView: React.FC = () => { const [isProcessing, setIsProcessing] = useState(false); const [processingProgress, setProcessingProgress] = useState({ current: 0, total: 0 }); const [isUploading, setIsUploading] = useState(false); + const [sessionToEdit, setSessionToEdit] = useState(); + const [showSessionForm, setShowSessionForm] = useState(false); const session = useLiveQuery( () => (sessionId ? db.sessions.get(parseInt(sessionId)) : undefined), @@ -126,6 +129,11 @@ const SessionView: React.FC = () => { } }; + const handleEditSession = (session: Session) => { + setSessionToEdit(session); + setShowSessionForm(true); + }; + const handleDeleteImage = async (imageId: number) => { const confirmed = await confirm({ title: t('confirmations.deleteImageTitle') || t('upload.deleteImage'), @@ -262,6 +270,18 @@ const SessionView: React.FC = () => { {isExporting ? t('export.exporting') : t('export.session')} + + {!session.locked && ( // Solo mostrar editar y eliminar si no está bloqueado + <> + + + )} {session.locked && ( {t('sessions.locked')} @@ -404,6 +424,17 @@ const SessionView: React.FC = () => {
+ + { + setShowSessionForm(false); + setSessionToEdit(undefined); + }} + /> {ConfirmDialogComponent} );