diff --git a/frontend/src/pages/ServiciosMunicipales.tsx b/frontend/src/pages/ServiciosMunicipales.tsx index 2253663..513bf83 100644 --- a/frontend/src/pages/ServiciosMunicipales.tsx +++ b/frontend/src/pages/ServiciosMunicipales.tsx @@ -150,10 +150,10 @@ export default function ServiciosMunicipales() { const [loading, setLoading] = useState(true); // Upload state - const [uploadedFile, setUploadedFile] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); - const [uploadResult, setUploadResult] = useState(null); + const [uploadResults, setUploadResults] = useState([]); const fileInputRef = useRef(null); const loadData = useCallback(async () => { @@ -205,34 +205,41 @@ export default function ServiciosMunicipales() { : 0; // Upload handlers - const handleFile = useCallback((files: FileList | null) => { + const handleFiles = useCallback((files: FileList | null) => { if (!files) return; - const pdf = Array.from(files).find((f) => f.type === 'application/pdf'); - if (pdf) { - setUploadedFile(pdf); - setUploadResult(null); + const pdfs = Array.from(files).filter((f) => f.type === 'application/pdf'); + if (pdfs.length > 0) { + setUploadedFiles((prev) => [...prev, ...pdfs]); + setUploadResults([]); } }, []); + const removeFile = (index: number) => { + setUploadedFiles((prev) => prev.filter((_, i) => i !== index)); + }; + const handleUpload = async () => { - if (!uploadedFile) return; + if (uploadedFiles.length === 0) return; setIsUploading(true); - setUploadResult(null); - try { - const { data } = await uploadMunicipalReceipt(uploadedFile); - setUploadResult(data); - setUploadedFile(null); - await loadData(); - } catch (err) { - setUploadResult({ - imported: 0, - updated: 0, - errors: [err instanceof Error ? err.message : 'Error al subir archivo'], - receipt: null, - }); - } finally { - setIsUploading(false); + setUploadResults([]); + const results: MunicipalReceiptUploadResult[] = []; + for (const file of uploadedFiles) { + try { + const { data } = await uploadMunicipalReceipt(file); + results.push(data); + } catch (err) { + results.push({ + imported: 0, + updated: 0, + errors: [`${file.name}: ${err instanceof Error ? err.message : 'Error al subir'}`], + receipt: null, + }); + } } + setUploadResults(results); + setUploadedFiles([]); + await loadData(); + setIsUploading(false); }; const formatFileSize = (bytes: number): string => { @@ -511,7 +518,7 @@ export default function ServiciosMunicipales() {

- Subir Recibo + Subir Recibos

@@ -519,12 +526,12 @@ export default function ServiciosMunicipales() {
{ e.preventDefault(); setIsDragging(true); }} onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} - onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFile(e.dataTransfer.files); }} + onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFiles(e.dataTransfer.files); }} onClick={() => fileInputRef.current?.click()} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && fileInputRef.current?.click()} - aria-label="Seleccionar archivo PDF" + aria-label="Seleccionar archivos PDF" className={[ 'border-2 border-dashed rounded-lg p-8', 'flex flex-col items-center justify-center gap-3', @@ -541,11 +548,11 @@ export default function ServiciosMunicipales() {

{isDragging - ? 'Suelta el archivo aquí' - : 'Arrastra el PDF aquí o toca para seleccionar'} + ? 'Suelta los archivos aquí' + : 'Arrastra PDFs aquí o toca para seleccionar'}

- Solo archivos PDF · Recibo Municipal de Belén + Solo archivos PDF · Múltiples archivos soportados

@@ -554,34 +561,48 @@ export default function ServiciosMunicipales() { ref={fileInputRef} type="file" accept="application/pdf" + multiple className="hidden" - onChange={(e) => handleFile(e.target.files)} + onChange={(e) => handleFiles(e.target.files)} /> - {/* Selected file */} - {uploadedFile && ( -
-
- -
-

{uploadedFile.name}

-

{formatFileSize(uploadedFile.size)}

-
+ {/* File list */} + {uploadedFiles.length > 0 && ( +
+

+ {uploadedFiles.length}{' '} + {uploadedFiles.length === 1 ? 'archivo en cola' : 'archivos en cola'} +

+
+ {uploadedFiles.map((file, i) => ( +
+
+ +
+

{file.name}

+

{formatFileSize(file.size)}

+
+
+ +
+ ))}
-
)} {/* Submit */} - {/* Upload result */} - {uploadResult && ( -
0 && !uploadResult.receipt - ? 'border-destructive/50 bg-destructive/5' - : 'border-emerald-500/50 bg-emerald-500/5', - ].join(' ')} - > -
- {uploadResult.receipt ? ( - - ) : ( - - )} - - {uploadResult.imported > 0 && 'Recibo importado'} - {uploadResult.updated > 0 && 'Recibo actualizado'} - {!uploadResult.receipt && 'Error al procesar'} - -
- {uploadResult.receipt && ( -
- - {periodLabel(uploadResult.receipt.period)} - - - {formatCRC(uploadResult.receipt.total)} - + {/* Upload results */} + {uploadResults.length > 0 && ( +
+ {uploadResults.map((result, i) => ( +
0 && !result.receipt + ? 'border-destructive/50 bg-destructive/5' + : 'border-emerald-500/50 bg-emerald-500/5', + ].join(' ')} + > +
+ {result.receipt ? ( + + ) : ( + + )} + + {result.imported > 0 && 'Recibo importado'} + {result.updated > 0 && 'Recibo actualizado'} + {!result.receipt && 'Error al procesar'} + +
+ {result.receipt && ( +
+ + {periodLabel(result.receipt.period)} + + + {formatCRC(result.receipt.total)} + +
+ )} + {result.errors.map((err, j) => ( +

{err}

+ ))}
- )} - {uploadResult.errors.map((err, i) => ( -

{err}

))}
)}