From fe0b4f1113282363b3a0bf245171a5bd9d16660d Mon Sep 17 00:00:00 2001 From: BFLOWS Date: Sat, 18 Apr 2026 16:55:06 +0200 Subject: [PATCH] feat(rendicontazione): componenti riusabili upload/preview PDF - components/FilePreviewDialog: Dialog full-height con iframe, blob URL autenticato (fetch + Bearer token), revoca URL alla chiusura, bottone scarica, usato da istruttore e beneficiario - components/FileUploadCell: cella compatta per righe DataTable, stati nessun-file/uploading/caricato, upload drag&drop (accept .pdf/.jpg/.png max 15MB), preview/download/sostituisci/elimina con conferma confirmPopup, readOnly per istruttore - service: 4 nuovi metodi file (uploadEntityFile multipart senza Content-Type forzato, deleteEntityFile, fetchEntityFileBlob con parse Content-Disposition, downloadEntityFile con anchor tag e revoke URL) - service: 2 metodi verbale (downloadVerbale blob PDF, openVerbaleHtml apre HTML in nuova tab per preview rapida) Nessun pdf.js, solo iframe nativo + ObjectURL. Zero dipendenze aggiuntive. --- .../components/FilePreviewDialog.js | 114 ++++++++++++ .../components/FileUploadCell.js | 164 ++++++++++++++++++ .../service/rendicontazioneService.js | 127 ++++++++++++++ 3 files changed, 405 insertions(+) create mode 100644 src/modules/rendicontazione/components/FilePreviewDialog.js create mode 100644 src/modules/rendicontazione/components/FileUploadCell.js diff --git a/src/modules/rendicontazione/components/FilePreviewDialog.js b/src/modules/rendicontazione/components/FilePreviewDialog.js new file mode 100644 index 0000000..6502e36 --- /dev/null +++ b/src/modules/rendicontazione/components/FilePreviewDialog.js @@ -0,0 +1,114 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { __ } from '@wordpress/i18n'; +import { Dialog } from 'primereact/dialog'; +import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; + +import RendicontazioneService from '../service/rendicontazioneService'; + +/** + * Dialog full-height per preview inline PDF/immagini. + * Fetcha il blob dal microservizio (Authorization header), crea object URL, + * lo monta in iframe. Revoca l'URL alla chiusura. + * + * Props: + * visible boolean + * onHide () => void + * entityType 'invoice' | 'ula' | 'document' + * entityId UUID + * title stringa titolo dialog + * filename nome file da mostrare e usare per download + */ +const FilePreviewDialog = ({ visible, onHide, entityType, entityId, title, filename }) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [objectUrl, setObjectUrl] = useState(null); + const currentUrlRef = useRef(null); + + useEffect(() => { + // cleanup url precedente + if (currentUrlRef.current) { + URL.revokeObjectURL(currentUrlRef.current); + currentUrlRef.current = null; + } + + if (!visible || !entityType || !entityId) { + setObjectUrl(null); + setError(null); + return; + } + + setLoading(true); + setError(null); + RendicontazioneService.fetchEntityFileBlob(entityType, entityId, true, + ({ objectUrl }) => { + currentUrlRef.current = objectUrl; + setObjectUrl(objectUrl); + setLoading(false); + }, + (err) => { + setError(err.detail || __('Errore caricamento file', 'gepafin')); + setLoading(false); + } + ); + + return () => { + if (currentUrlRef.current) { + URL.revokeObjectURL(currentUrlRef.current); + currentUrlRef.current = null; + } + }; + }, [visible, entityType, entityId]); + + const onDownload = () => { + RendicontazioneService.downloadEntityFile(entityType, entityId, + (err) => setError(err.detail || __('Errore download', 'gepafin')) + ); + }; + + const footer = ( +
+ {filename} +
+
+
+ ); + + return ( + + {loading && ( +
+ +
+ )} + {error && !loading && ( +
+
{error}
+
+ )} + {!loading && !error && objectUrl && ( +