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:
@@ -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 */}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user