feat(rendicontazione): integrazione upload reale + preview PDF + verbale nei flussi

IstruttoriaPratica.js:
- previewDialog esteso con entityType/entityId (non piu solo filename)
- openPreview/closePreview/doDownload rimpiazzano openPreview/downloadStub stub
- Dialog placeholder 'anteprima simulata' rimosso, sostituito con <FilePreviewDialog/>
- Bottoni anteprima/scarica in fatture/ULA/documenti usano gli endpoint reali
  (disabled se !storage_path)
- Nuovi bottoni 'Anteprima verbale' (HTML tab) e 'Scarica verbale PDF'
  nella toolbar per status in UNDER_REVIEW/AWAITING_AMENDMENT/APPROVED/REJECTED
- downloadVerbale/openVerbaleHtml helpers

PraticaRendicontazioneEdit.js:
- previewDialog state + openPreview/closePreview
- updateInvoiceFile/updateUlaFile/updateDocFile: aggiornano lo stato locale
  dopo upload/delete senza full reload pagina
- ensureDocRecord: auto-crea RemissionDocument (via upsertDocument con filename=null)
  prima dell'upload cosi FileUploadCell ha un entityId valido
- Colonne 'Allegato' nelle DataTable fatture/ULA ora renderizzano <FileUploadCell/>
  con onPreview/onChange wired
- Sezione documenti: FileUploadCell per record esistenti, bottone 'Carica'
  per record non ancora creati
- Modal fattura: rimosso campo 'Nome file PDF (simulato)', infobox post-save guida
  al caricamento dalla tabella
- Modal dipendente: rimosso campo 'Nome file allegato (simulato)', infobox analogo
- <FilePreviewDialog/> montato in chiusura

Test JSX: @babel/parser OK su entrambi i file. Webpack ricompila hot-reload.
This commit is contained in:
BFLOWS
2026-04-18 16:55:22 +02:00
parent fe0b4f1113
commit fca18de751
2 changed files with 159 additions and 64 deletions

View File

@@ -16,6 +16,7 @@ import { Checkbox } from 'primereact/checkbox';
import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
import RendicontazioneService from '../service/rendicontazioneService';
import FilePreviewDialog from '../components/FilePreviewDialog';
const CONTRACT_TYPES = {
T_IND: 'Tempo indeterminato', T_DET: 'Tempo determinato',
@@ -67,7 +68,7 @@ const IstruttoriaPratica = () => {
const [bundle, setBundle] = useState(null);
// dialoghi
const [previewDialog, setPreviewDialog] = useState({ visible: false, filename: null, title: null });
const [previewDialog, setPreviewDialog] = useState({ visible: false, entityType: null, entityId: null, filename: null, title: null });
const [docNoteDialog, setDocNoteDialog] = useState({ visible: false, doc: null, status: null });
// tabelle: expanded rows + buffer modifiche inline
const [expandedInv, setExpandedInv] = useState({});
@@ -123,10 +124,22 @@ const IstruttoriaPratica = () => {
detail: typeof err?.detail === 'object' ? JSON.stringify(err.detail) : err?.detail });
};
const openPreview = (filename, title) => setPreviewDialog({ visible: true, filename, title });
const downloadStub = (filename) => {
toast.current?.show({ severity: 'info', summary: __('Sandbox', 'gepafin'),
detail: __(`Download di ${filename} — in produzione scarica il file reale dallo storage.`, 'gepafin') });
const openPreview = (entityType, entityId, title, filename) => setPreviewDialog({ visible: true, entityType, entityId, title, filename });
const closePreview = () => setPreviewDialog({ visible: false, entityType: null, entityId: null, filename: null, title: null });
const downloadVerbale = () => {
RendicontazioneService.downloadVerbale(practiceId,
(err) => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail || __('Verbale non disponibile', 'gepafin') })
);
};
const openVerbaleHtml = () => {
RendicontazioneService.openVerbaleHtml(practiceId).catch(() =>
toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: __('Verbale non disponibile', 'gepafin') })
);
};
const doDownload = (entityType, entityId) => {
RendicontazioneService.downloadEntityFile(entityType, entityId,
(err) => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail || __('Download non riuscito', 'gepafin') })
);
};
// Quick verify (thumbs up/down) senza rettifica
@@ -403,6 +416,16 @@ const IstruttoriaPratica = () => {
disabled={openAmendments.length > 0}
onClick={() => setAmendDialog({ visible: true, text: '', deadline: null })} />
</>)}
{/* Verbale: sempre visibile all'istruttore per preview e scarico */}
{['UNDER_REVIEW', 'AWAITING_AMENDMENT', 'APPROVED', 'REJECTED'].includes(practice.status) && (<>
<Button type="button" icon="pi pi-eye" iconPos="right" outlined
label={__('Anteprima verbale', 'gepafin')}
onClick={openVerbaleHtml} />
<Button type="button" icon="pi pi-file-pdf" iconPos="right" severity="info"
label={__('Scarica verbale PDF', 'gepafin')}
onClick={downloadVerbale} />
</>)}
</div>
</div>
@@ -658,10 +681,12 @@ const IstruttoriaPratica = () => {
body={(r) => (
<div style={{ display: 'flex', gap: '0.25rem' }}>
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
onClick={() => openPreview(r.pdf_filename || `fattura_${r.invoice_number}.pdf`, `Fattura ${r.invoice_number}`)}
disabled={!r.storage_path}
onClick={() => openPreview('invoice', r.id, `Fattura ${r.invoice_number}`, r.pdf_filename)}
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }} />
<Button icon="pi pi-download" rounded outlined size="small" severity="info"
onClick={() => downloadStub(r.pdf_filename || `fattura_${r.invoice_number}.pdf`)}
disabled={!r.storage_path}
onClick={() => doDownload('invoice', r.id)}
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }} />
<Button icon="pi pi-check" rounded outlined size="small"
severity={r.verification_status === 'AMMESSA' ? 'success' : 'secondary'}
@@ -814,12 +839,12 @@ const IstruttoriaPratica = () => {
body={(r) => (
<div style={{ display: 'flex', gap: '0.25rem' }}>
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
disabled={!r.supporting_doc_filename}
onClick={() => openPreview(r.supporting_doc_filename, `${r.full_name}`)}
disabled={!r.storage_path}
onClick={() => openPreview('ula', r.id, `${r.full_name}`, r.supporting_doc_filename)}
tooltip={__('Anteprima allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
<Button icon="pi pi-download" rounded outlined size="small" severity="info"
disabled={!r.supporting_doc_filename}
onClick={() => downloadStub(r.supporting_doc_filename)}
disabled={!r.storage_path}
onClick={() => doDownload('ula', r.id)}
tooltip={__('Scarica allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
<Button icon="pi pi-check" rounded outlined size="small"
severity={r.verification_status === 'AMMESSA' ? 'success' : 'secondary'}
@@ -877,13 +902,13 @@ const IstruttoriaPratica = () => {
</div>
<div className="appPageSection__iconActions">
<Button icon="pi pi-eye" rounded outlined severity="info"
disabled={!doc.filename}
onClick={() => openPreview(doc.filename, `${dr.label}${doc.filename || ''}`)}
disabled={!doc.storage_path}
onClick={() => openPreview('document', doc.id, `${dr.label}`, doc.filename)}
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Anteprima', 'gepafin')} />
<Button icon="pi pi-download" rounded outlined severity="info"
disabled={!doc.filename}
onClick={() => downloadStub(doc.filename)}
disabled={!doc.storage_path}
onClick={() => doDownload('document', doc.id)}
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Scarica', 'gepafin')} />
<Button icon="pi pi-clock" rounded outlined severity="warning"
@@ -963,22 +988,15 @@ const IstruttoriaPratica = () => {
{/* ============ DIALOGS ============ */}
{/* Preview PDF */}
<Dialog visible={previewDialog.visible} style={{ width: '640px' }}
header={previewDialog.title || __('Anteprima', 'gepafin')} modal
onHide={() => setPreviewDialog({ visible: false, filename: null, title: null })}>
<div style={{ padding: '2rem', textAlign: 'center', background: 'var(--surface-100)', borderRadius: '6px' }}>
<i className="pi pi-file-pdf" style={{ fontSize: '4rem', color: 'var(--red-500)' }} />
<h3 style={{ margin: '1rem 0 0.5rem 0' }}>{previewDialog.filename || '—'}</h3>
<p className="text-color-secondary" style={{ marginTop: 0 }}>
{__('Anteprima PDF — in sandbox il file reale non è caricato. In produzione qui viene visualizzato il PDF originale della fattura/documento.', 'gepafin')}
</p>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1rem' }}>
<Button label={__('Chiudi', 'gepafin')} outlined
onClick={() => setPreviewDialog({ visible: false, filename: null, title: null })} />
</div>
</Dialog>
{/* Preview PDF reale — iframe con blob autenticato */}
<FilePreviewDialog
visible={previewDialog.visible}
onHide={closePreview}
entityType={previewDialog.entityType}
entityId={previewDialog.entityId}
title={previewDialog.title}
filename={previewDialog.filename}
/>
{/* Rettifica fattura */}