feat(istruttoria UI): verifica riga-per-riga con thumbs up/down/occhio/download/rettifica

Replica il workflow del foglio Excel originale dell'istruttoria Gepafin.
Pattern preso da DomandaEditPreInstructor/components/ListOfFiles.

Pagina IstruttoriaPratica riscritta (858 righe):

RIEPILOGO FINANZIARIO esteso:
- Totale dichiarato (dal beneficiario)
- Totale verificato (somma AMMESSA + PARZIALE istruttore)
- Cap remissione (min(50% erogato, 12500))
- Remissione da riconoscere (da verificato)
- Residuo da restituire (erogato - remissione)

VERIFICA FATTURE per categoria con appPageSection__list:
- Ogni fattura come row con numero, fornitore, date, descrizione, Tag stato
- Tag rosso 'Date fuori periodo' se invoice_in_period=false O payment_in_period=false
- Riga dichiarato + riga verificato (se presente)
- Note rettifica evidenziate con barra arancione
- 5 pulsanti icona: eye (anteprima PDF) + download + pencil (rettifica con dialog)
  + thumbs-up AMMESSA + thumbs-down RESPINTA
- Thumbs up/down = ammissione/rifiuto rapido senza rettifica
- Pencil = dialog con dichiarato readonly + verificato editabile + note obbligatorie -> PARZIALE

VERIFICA ULA:
- Stesso pattern: eye/download/pencil/up/down
- Rettifica FTE (0-1) con note

VERIFICA DOCUMENTI:
- eye/download/thumbs-up VALIDO
- clock SCADUTO (apre dialog con motivazione)
- thumbs-down NON_VALIDO (apre dialog con motivazione)

VERBALE ISTRUTTORIA finale (visibile in UNDER_REVIEW/AWAITING_AMENDMENT):
- 3 checkbox: documentazione completa, ULA>1, erogato in range
- Textarea note sintetiche con save onBlur

Approva disabilitato finché tutte le righe hanno status != PENDING.
Anteprima PDF: dialog con placeholder sandbox (file reale sarà in prod).
Download: toast stub (in prod scarica dal storage).
This commit is contained in:
BFLOWS Sandbox
2026-04-18 11:03:15 +02:00
parent 115f31bdef
commit 61cdfbd06b
3 changed files with 569 additions and 151 deletions

View File

@@ -11,9 +11,9 @@ import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { InputTextarea } from 'primereact/inputtextarea';
import { Calendar } from 'primereact/calendar';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Checkbox } from 'primereact/checkbox';
import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
import { isNil } from 'ramda';
import RendicontazioneService from '../service/rendicontazioneService';
@@ -25,13 +25,27 @@ const CONTRACT_TYPES = {
const PRACTICE_STATUS = {
DRAFT: { severity: 'warning', label: 'Bozza beneficiario' },
SUBMITTED: { severity: 'info', label: 'Inviata' },
UNDER_REVIEW: { severity: 'warning', label: 'In valutazione' },
SUBMITTED: { severity: 'info', label: 'Inviata — da prendere in carico' },
UNDER_REVIEW: { severity: 'warning', label: 'In lavorazione' },
AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso aperto' },
APPROVED: { severity: 'success', label: 'Approvata' },
REJECTED: { severity: 'danger', label: 'Respinta' }
};
const VERIFICATION_INVOICE_TAG = {
PENDING: { severity: 'secondary', label: 'Da verificare' },
AMMESSA: { severity: 'success', label: 'Ammessa' },
PARZIALE: { severity: 'warning', label: 'Parziale' },
RESPINTA: { severity: 'danger', label: 'Respinta' }
};
const VERIFICATION_DOC_TAG = {
PENDING: { severity: 'secondary', label: 'Da verificare' },
VALIDO: { severity: 'success', label: 'Valido' },
NON_VALIDO: { severity: 'danger', label: 'Non valido' },
SCADUTO: { severity: 'warning', label: 'Scaduto' }
};
const AMENDMENT_STATUS = {
AWAITING: { severity: 'warning', label: 'Attesa risposta' },
RESPONSE_RECEIVED: { severity: 'info', label: 'Risposta ricevuta' },
@@ -39,7 +53,7 @@ const AMENDMENT_STATUS = {
EXPIRED: { severity: 'danger', label: 'Scaduta' }
};
const euro = (v) => '€ ' + Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const euro = (v) => v == null ? '—' : '€ ' + Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT') : '—';
const formatDateTime = (d) => d ? new Date(d).toLocaleString('it-IT') : '—';
@@ -50,7 +64,13 @@ const IstruttoriaPratica = () => {
const toast = useRef(null);
const [loading, setLoading] = useState(true);
const [bundle, setBundle] = useState(null); // {practice, gate_check, amendments}
const [bundle, setBundle] = useState(null);
// dialoghi
const [previewDialog, setPreviewDialog] = useState({ visible: false, filename: null, title: null });
const [invRectDialog, setInvRectDialog] = useState({ visible: false, invoice: null });
const [ulaRectDialog, setUlaRectDialog] = useState({ visible: false, employee: null });
const [docNoteDialog, setDocNoteDialog] = useState({ visible: false, doc: null, status: null });
const [approveDialog, setApproveDialog] = useState({ visible: false, amount: null });
const [rejectDialog, setRejectDialog] = useState({ visible: false, reason: '' });
@@ -86,12 +106,12 @@ const IstruttoriaPratica = () => {
}, [sections]);
const openAmendments = amendments.filter(a => a.status === 'AWAITING' || a.status === 'RESPONSE_RECEIVED');
const isReviewable = practice && ['SUBMITTED', 'UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status);
const isDecidable = practice && ['UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status);
const isVerifiable = practice && ['UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status);
// ---------- actions ----------
const afterOk = (msg) => () => {
const afterOk = (msg) => (resp) => {
toast.current?.show({ severity: 'success', summary: msg });
load();
};
@@ -100,63 +120,144 @@ const IstruttoriaPratica = () => {
detail: typeof err?.detail === 'object' ? JSON.stringify(err.detail) : err?.detail });
};
const handleClaim = (e) => {
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') });
};
// Quick verify (thumbs up/down) senza rettifica
const quickVerifyInvoice = (invoice, newStatus) => {
RendicontazioneService.verifyInvoice(practiceId, invoice.id,
{ verification_status: newStatus, verification_notes: null },
afterOk(__(`Fattura ${newStatus.toLowerCase()}`, 'gepafin')), onErr);
};
const quickVerifyUla = (employee, newStatus) => {
RendicontazioneService.verifyUlaEmployee(practiceId, employee.id,
{ verification_status: newStatus, verification_notes: null },
afterOk(__(`Dipendente ${newStatus.toLowerCase()}`, 'gepafin')), onErr);
};
const quickVerifyDoc = (doc, newStatus) => {
RendicontazioneService.verifyDocument(practiceId, doc.doc_code,
{ verification_status: newStatus, verification_notes: null },
afterOk(__(`Documento ${newStatus.toLowerCase()}`, 'gepafin')), onErr);
};
const saveInvoiceRect = () => {
const i = invRectDialog.invoice;
RendicontazioneService.verifyInvoice(practiceId, i.id, {
verification_status: 'PARZIALE',
taxable_verified: i.taxable_verified,
vat_verified: i.vat_verified,
total_verified: i.total_verified,
verification_notes: i.verification_notes
},
(resp) => { setInvRectDialog({ visible: false, invoice: null }); afterOk(__('Rettifica salvata', 'gepafin'))(resp); },
onErr);
};
const saveUlaRect = () => {
const e = ulaRectDialog.employee;
RendicontazioneService.verifyUlaEmployee(practiceId, e.id, {
verification_status: 'PARZIALE',
fte_pct_verified: e.fte_pct_verified,
verification_notes: e.verification_notes
},
(resp) => { setUlaRectDialog({ visible: false, employee: null }); afterOk(__('Rettifica ULA salvata', 'gepafin'))(resp); },
onErr);
};
const saveDocNote = () => {
const d = docNoteDialog.doc;
const status = docNoteDialog.status;
RendicontazioneService.verifyDocument(practiceId, d.doc_code,
{ verification_status: status, verification_notes: d.verification_notes },
(resp) => { setDocNoteDialog({ visible: false, doc: null, status: null }); afterOk(__('Documento aggiornato', 'gepafin'))(resp); },
onErr);
};
// Final notes + checklist (debounced inline save)
const saveFinalNotes = (patch) => {
RendicontazioneService.setInstructorFinalNotes(practiceId, patch, afterOk(__('Verbale aggiornato', 'gepafin')), onErr);
};
// Claim / approve / reject / amendment / close amendment — come prima
const handleClaim = (ev) => {
confirmPopup({
target: e.currentTarget,
message: __('Prendere in carico la pratica? Lo stato passerà a In valutazione.', 'gepafin'),
target: ev.currentTarget,
message: __('Prendere in carico la pratica? Lo stato passerà a "In lavorazione".', 'gepafin'),
icon: 'pi pi-info-circle',
acceptLabel: __('Prendi in carico', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'),
accept: () => RendicontazioneService.claimPractice(practiceId,
afterOk(__('Pratica presa in carico', 'gepafin')), onErr)
});
};
const doApprove = () => {
const body = approveDialog.amount != null ? { approved_remission: approveDialog.amount } : {};
RendicontazioneService.approvePractice(practiceId, body,
(resp) => { setApproveDialog({ visible: false, amount: null }); afterOk(__('Pratica approvata', 'gepafin'))(resp); },
onErr);
(resp) => { setApproveDialog({ visible: false, amount: null }); afterOk(__('Pratica approvata', 'gepafin'))(resp); }, onErr);
};
const doReject = () => {
if (!rejectDialog.reason || rejectDialog.reason.trim().length < 10) {
toast.current?.show({ severity: 'warn', summary: __('Motivazione troppo corta', 'gepafin'), detail: __('Minimo 10 caratteri', 'gepafin') });
toast.current?.show({ severity: 'warn', summary: __('Motivazione troppo corta (min 10 caratteri)', 'gepafin') });
return;
}
RendicontazioneService.rejectPractice(practiceId, rejectDialog.reason,
(resp) => { setRejectDialog({ visible: false, reason: '' }); afterOk(__('Pratica respinta', 'gepafin'))(resp); },
onErr);
(resp) => { setRejectDialog({ visible: false, reason: '' }); afterOk(__('Pratica respinta', 'gepafin'))(resp); }, onErr);
};
const doAmend = () => {
if (!amendDialog.text || amendDialog.text.trim().length < 10) {
toast.current?.show({ severity: 'warn', summary: __('Testo troppo corto', 'gepafin') });
return;
toast.current?.show({ severity: 'warn', summary: __('Testo troppo corto', 'gepafin') }); return;
}
if (!amendDialog.deadline) {
toast.current?.show({ severity: 'warn', summary: __('Deadline obbligatoria', 'gepafin') });
return;
toast.current?.show({ severity: 'warn', summary: __('Deadline obbligatoria', 'gepafin') }); return;
}
const body = {
request_text: amendDialog.text,
deadline: typeof amendDialog.deadline === 'string' ? amendDialog.deadline : amendDialog.deadline.toISOString().slice(0, 10)
};
const body = { request_text: amendDialog.text,
deadline: typeof amendDialog.deadline === 'string' ? amendDialog.deadline : amendDialog.deadline.toISOString().slice(0, 10) };
RendicontazioneService.createAmendment(practiceId, body,
(resp) => { setAmendDialog({ visible: false, text: '', deadline: null }); afterOk(__('Soccorso avviato', 'gepafin'))(resp); },
onErr);
(resp) => { setAmendDialog({ visible: false, text: '', deadline: null }); afterOk(__('Soccorso avviato', 'gepafin'))(resp); }, onErr);
};
const closeAmendment = (e, amendment) => {
const closeAmendment = (ev, a) => {
confirmPopup({
target: e.currentTarget,
message: __('Chiudi questa richiesta di soccorso? La pratica torna in valutazione.', 'gepafin'),
target: ev.currentTarget,
message: __('Chiudi questa richiesta di soccorso? La pratica torna in lavorazione.', 'gepafin'),
icon: 'pi pi-info-circle',
acceptLabel: __('Chiudi', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'),
accept: () => RendicontazioneService.closeAmendment(practiceId, amendment.id,
afterOk(__('Soccorso chiuso', 'gepafin')), onErr)
accept: () => RendicontazioneService.closeAmendment(practiceId, a.id, afterOk(__('Soccorso chiuso', 'gepafin')), onErr)
});
};
// ---------- render helpers ----------
const renderThumbsRow = (currentStatus, onUp, onDown, onPreview, onDownload, filename, extraButtons) => {
const isUp = currentStatus === 'AMMESSA' || currentStatus === 'VALIDO';
const isDown = currentStatus === 'RESPINTA' || currentStatus === 'NON_VALIDO' || currentStatus === 'SCADUTO';
return (
<div className="appPageSection__iconActions">
<Button icon="pi pi-eye" rounded outlined severity="info"
onClick={onPreview} disabled={!filename}
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Anteprima', 'gepafin')} />
<Button icon="pi pi-download" rounded outlined severity="info"
onClick={onDownload} disabled={!filename}
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Scarica', 'gepafin')} />
{extraButtons}
<Button icon="pi pi-thumbs-up" rounded outlined
severity={isUp ? 'success' : 'secondary'}
disabled={!isVerifiable}
onClick={onUp}
tooltip={__('Ammetti', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Ammetti', 'gepafin')} />
<Button icon="pi pi-thumbs-down" rounded outlined
severity={isDown ? 'danger' : 'secondary'}
disabled={!isVerifiable}
onClick={onDown}
tooltip={__('Respingi', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Respingi', 'gepafin')} />
</div>
);
};
// ---------- render ----------
if (loading) {
return <div className="appPage"><div className="appPageSection"><Skeleton width="100%" height="15rem" /></div></div>;
@@ -167,20 +268,26 @@ const IstruttoriaPratica = () => {
const statusCfg = PRACTICE_STATUS[practice.status] || { severity: 'secondary', label: practice.status };
const totals = gate?.totals || {};
const perCat = totals.per_category || {};
const perDecl = totals.per_category_declared || {};
const perVerif = totals.per_category_verified || {};
const invoicesOfCat = (code) => practice.invoices.filter(i => i.category_code === code);
const allItemsVerified = practice.invoices.every(i => i.verification_status !== 'PENDING') &&
practice.ula_employees.every(e => e.verification_status !== 'PENDING') &&
practice.documents.every(d => d.verification_status !== 'PENDING');
const checklist = practice.instructor_checklist || {};
return (
<div className="appPage">
<Toast ref={toast} />
<ConfirmPopup />
{/* HEADER */}
<div className="appPage__pageHeader">
<h1>{__('Istruttoria pratica', 'gepafin')}</h1>
<h1>{__('Istruttoria pratica rendicontazione', 'gepafin')}</h1>
<p>
<span className="companyName">
{practice.schema_snapshot?.template_label || `Call #${practice.call_id}`} · {__('Pratica', 'gepafin')} #{practice.application_id}
{practice.schema_snapshot?.template_label || `Bando #${practice.call_id}`} · {__('Pratica', 'gepafin')} #{practice.application_id}
</span>
<span style={{ marginLeft: '1rem' }}>
<Tag severity={statusCfg.severity} value={statusCfg.label} />
@@ -204,6 +311,9 @@ const IstruttoriaPratica = () => {
{isDecidable && (<>
<Button type="button" icon="pi pi-check" iconPos="right" severity="success"
label={__('Approva', 'gepafin')}
disabled={!allItemsVerified}
tooltip={!allItemsVerified ? __('Completa la verifica di tutte le righe prima di approvare', 'gepafin') : null}
tooltipOptions={{ showOnDisabled: true }}
onClick={() => setApproveDialog({ visible: true, amount: totals.remission_due || 0 })} />
<Button type="button" icon="pi pi-times" iconPos="right" severity="danger" outlined
label={__('Respingi', 'gepafin')}
@@ -220,12 +330,8 @@ const IstruttoriaPratica = () => {
{/* RIEPILOGO */}
<div className="appPageSection" style={{ background: 'var(--surface-50)', padding: '1.25rem', borderRadius: '6px' }}>
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Riepilogo', 'gepafin')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '1rem', width: '100%' }}>
<div>
<small className="text-color-secondary">{__('Azienda', 'gepafin')}</small>
<div style={{ fontSize: '1rem', fontWeight: 700 }}>Company #{practice.company_id}</div>
</div>
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Riepilogo finanziario', 'gepafin')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '1rem', width: '100%' }}>
<div>
<small className="text-color-secondary">{__('Erogato', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(practice.amount_erogato)}</div>
@@ -235,16 +341,24 @@ const IstruttoriaPratica = () => {
<div style={{ fontSize: '1rem', fontWeight: 700 }}>{practice.iva_regime || '—'}</div>
</div>
<div>
<small className="text-color-secondary">{__('Totale fatture', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.grand_total || 0)}</div>
<small className="text-color-secondary">{__('Totale dichiarato', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.grand_total_declared)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Totale verificato', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700, color: 'var(--green-700)' }}>{euro(totals.grand_total_verified)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Cap remissione', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.max_remission || 0)}</div>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.max_remission)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Remissione calcolata', 'gepafin')}</small>
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--primary-color)' }}>{euro(totals.remission_due || 0)}</div>
<small className="text-color-secondary">{__('Remissione da riconoscere', 'gepafin')}</small>
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--primary-color)' }}>{euro(totals.remission_due)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Residuo da restituire', 'gepafin')}</small>
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--red-600)' }}>{euro(totals.residuo_da_restituire)}</div>
</div>
{practice.approved_remission != null && (
<div>
@@ -290,9 +404,7 @@ const IstruttoriaPratica = () => {
<small className="text-color-secondary">{__('Richiesta istruttore:', 'gepafin')}</small>
<div style={{ whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}>{a.request_text}</div>
{a.response_text && (<>
<small className="text-color-secondary">
{__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):
</small>
<small className="text-color-secondary">{__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):</small>
<div style={{ whiteSpace: 'pre-wrap', padding: '0.5rem', background: 'white', borderRadius: '4px', marginTop: '0.25rem' }}>{a.response_text}</div>
</>)}
</div>
@@ -304,118 +416,382 @@ const IstruttoriaPratica = () => {
<div className="appPage__spacer"></div>
</>)}
{/* GATE CHECKS */}
{gate && (
<div className="appPageSection">
<h2>{__('Requisiti di validità', 'gepafin')}</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', width: '100%' }}>
{gate.checks.map((c, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<i className={c.passed ? 'pi pi-check-circle' : 'pi pi-times-circle'}
style={{ color: c.passed ? 'var(--green-500)' : 'var(--orange-500)', fontSize: '1.25rem' }} />
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600 }}>{c.label}</div>
<small className="text-color-secondary">{c.detail}</small>
</div>
</div>
))}
</div>
</div>
)}
<div className="appPage__spacer"></div>
{/* FATTURE PER CATEGORIA */}
{/* FATTURE PER CATEGORIA — pattern ListOfFiles */}
<div className="appPageSection">
<h2>{__('Fatture rendicontate', 'gepafin')}</h2>
<div className="fieldsRepeater">
{categories.map(cat => {
const invs = invoicesOfCat(cat.code);
const total = perCat[cat.code] || 0;
return (
<div key={cat.code} className="fieldsRepeater__panel"
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem' }}>
<div className="fieldsRepeater__heading" style={{ marginBottom: '0.5rem' }}>
<div>
<strong style={{ color: 'var(--primary-color)' }}>{cat.code}</strong> {cat.label}
</div>
<div style={{ textAlign: 'right' }}>
<strong>{euro(total)}</strong>
<div><small className="text-color-secondary">{invs.length} {__('fatture', 'gepafin')}</small></div>
</div>
<h2>{__('Verifica fatture', 'gepafin')}</h2>
<p className="text-color-secondary" style={{ marginTop: 0 }}>
{__('Per ogni fattura: anteprima, download, pollice su per ammettere, pollice giù per respingere, icona matita per rettificare importi ammissibili.', 'gepafin')}
</p>
{categories.map(cat => {
const invs = invoicesOfCat(cat.code);
const totalDecl = perDecl[cat.code] || 0;
const totalVerif = perVerif[cat.code] || 0;
return (
<div key={cat.code} style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', borderBottom: '2px solid var(--primary-color)', paddingBottom: '0.25rem', marginBottom: '0.5rem' }}>
<h3 style={{ margin: 0 }}>
<span style={{ color: 'var(--primary-color)' }}>{cat.code}</span> {cat.label}
</h3>
<div style={{ display: 'flex', gap: '1.5rem', alignItems: 'baseline' }}>
<div><small className="text-color-secondary">{__('Dichiarato:', 'gepafin')}</small> <strong>{euro(totalDecl)}</strong></div>
<div><small className="text-color-secondary">{__('Verificato:', 'gepafin')}</small> <strong style={{ color: 'var(--green-700)' }}>{euro(totalVerif)}</strong></div>
</div>
{invs.length > 0 ? (
<DataTable value={invs} dataKey="id" size="small" responsiveLayout="scroll">
<Column field="invoice_number" header={__('N°', 'gepafin')} />
<Column header={__('Data', 'gepafin')} body={(r) => formatDate(r.invoice_date)} />
<Column header={__('Pagamento', 'gepafin')} body={(r) => formatDate(r.payment_date)} />
<Column field="supplier_name" header={__('Fornitore', 'gepafin')} />
<Column field="description" header={__('Descrizione', 'gepafin')}
body={(r) => <span title={r.description}>{r.description.slice(0, 40)}{r.description.length > 40 ? '…' : ''}</span>} />
<Column header={__('Imponibile', 'gepafin')} body={(r) => euro(r.taxable)} />
<Column header={__('IVA', 'gepafin')} body={(r) => euro(r.vat)} />
<Column header={__('Totale', 'gepafin')} body={(r) => euro(r.total)} />
</DataTable>
) : <small className="text-color-secondary">{__('Nessuna fattura', 'gepafin')}</small>}
</div>
);
})}
</div>
{invs.length === 0 ? (
<small className="text-color-secondary">{__('Nessuna fattura in questa categoria', 'gepafin')}</small>
) : (
<ol className="appPageSection__list">
{invs.map((inv) => {
const statusCfg = VERIFICATION_INVOICE_TAG[inv.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
const dateChecks = inv.date_checks || {};
const invalidDates = dateChecks.invoice_in_period === false || dateChecks.payment_in_period === false;
const hasRect = inv.verification_status === 'PARZIALE' && inv.taxable_verified != null;
return (
<li key={inv.id} className="appPageSection__listItem">
<div className="appPageSection__listItemRow">
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<strong>{inv.invoice_number}</strong>
<span className="text-color-secondary"></span>
<span>{inv.supplier_name}</span>
<Tag severity={statusCfg.severity} value={statusCfg.label} />
{invalidDates && <Tag severity="danger" icon="pi pi-exclamation-triangle" value={__('Date fuori periodo', 'gepafin')} />}
</div>
<div style={{ marginTop: '0.25rem', fontSize: '0.9em', color: 'var(--text-color-secondary)' }}>
{__('Emessa', 'gepafin')} {formatDate(inv.invoice_date)} · {__('pagata', 'gepafin')} {formatDate(inv.payment_date)} · {inv.description}
</div>
<div style={{ marginTop: '0.5rem', display: 'flex', gap: '1.5rem', fontSize: '0.9em' }}>
<span><small className="text-color-secondary">{__('Dichiarato imp.:', 'gepafin')}</small> <strong>{euro(inv.taxable)}</strong> · IVA {euro(inv.vat)} · tot {euro(inv.total)}</span>
{hasRect && (
<span style={{ color: 'var(--green-700)' }}>
<small>{__('Verificato imp.:', 'gepafin')}</small> <strong>{euro(inv.taxable_verified)}</strong> · IVA {euro(inv.vat_verified)} · tot {euro(inv.total_verified)}
</span>
)}
</div>
{inv.verification_notes && (
<div style={{ marginTop: '0.5rem', padding: '0.4rem 0.6rem', background: 'var(--surface-100)', borderLeft: '3px solid var(--orange-400)', fontSize: '0.9em' }}>
<i className="pi pi-pencil" style={{ marginRight: '0.4rem' }} />{inv.verification_notes}
</div>
)}
</div>
{renderThumbsRow(
inv.verification_status,
() => quickVerifyInvoice(inv, 'AMMESSA'),
() => quickVerifyInvoice(inv, 'RESPINTA'),
() => openPreview(inv.pdf_filename || `fattura_${inv.invoice_number}.pdf`, `${__('Fattura', 'gepafin')} ${inv.invoice_number}`),
() => downloadStub(inv.pdf_filename || `fattura_${inv.invoice_number}.pdf`),
inv.pdf_filename || `fattura_${inv.invoice_number}.pdf`,
<Button icon="pi pi-pencil" rounded outlined severity="warning"
disabled={!isVerifiable}
onClick={() => setInvRectDialog({
visible: true,
invoice: {
...inv,
taxable_verified: inv.taxable_verified != null ? Number(inv.taxable_verified) : Number(inv.taxable),
vat_verified: inv.vat_verified != null ? Number(inv.vat_verified) : Number(inv.vat),
total_verified: inv.total_verified != null ? Number(inv.total_verified) : Number(inv.total)
}
})}
tooltip={__('Rettifica con note', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Rettifica', 'gepafin')} />
)}
</div>
</li>
);
})}
</ol>
)}
</div>
);
})}
</div>
{/* ULA */}
{ulaSection.enabled && (<>
{/* ULA DIPENDENTI */}
{ulaSection.enabled && practice.ula_employees.length > 0 && (<>
<div className="appPage__spacer"></div>
<div className="appPageSection">
<h2>{__('Dipendenti ULA', 'gepafin')}</h2>
{practice.ula_employees.length > 0 ? (
<DataTable value={practice.ula_employees} dataKey="id" size="small" responsiveLayout="scroll" style={{ width: '100%' }}>
<Column field="codice_fiscale" header="CF" />
<Column field="full_name" header={__('Nome', 'gepafin')} />
<Column header={__('Contratto', 'gepafin')} body={(r) => CONTRACT_TYPES[r.contract_type] || r.contract_type} />
<Column field="role_description" header={__('Mansione', 'gepafin')} />
<Column header="FTE" body={(r) => Number(r.fte_pct).toFixed(2)} />
<Column header={__('Periodo', 'gepafin')}
body={(r) => `${formatDate(r.period_start_date)}${formatDate(r.period_end_date)}`} />
<Column field="supporting_doc_filename" header={__('Allegato', 'gepafin')}
body={(r) => r.supporting_doc_filename
? <span><i className="pi pi-file" /> {r.supporting_doc_filename}</span>
: <span className="text-color-secondary"></span>} />
</DataTable>
) : <p className="text-color-secondary">{__('Nessun dipendente caricato', 'gepafin')}</p>}
<h2>{__('Verifica dipendenti ULA', 'gepafin')}</h2>
<ol className="appPageSection__list">
{practice.ula_employees.map(e => {
const cfg = VERIFICATION_INVOICE_TAG[e.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
return (
<li key={e.id} className="appPageSection__listItem">
<div className="appPageSection__listItemRow">
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<strong>{e.full_name}</strong>
<span className="text-color-secondary">({e.codice_fiscale})</span>
<Tag severity={cfg.severity} value={cfg.label} />
</div>
<div style={{ marginTop: '0.25rem', fontSize: '0.9em', color: 'var(--text-color-secondary)' }}>
{CONTRACT_TYPES[e.contract_type] || e.contract_type}
{e.role_description && ` · ${e.role_description}`}
{' · '}{formatDate(e.period_start_date)} {formatDate(e.period_end_date)}
</div>
<div style={{ marginTop: '0.4rem', display: 'flex', gap: '1.5rem', fontSize: '0.9em' }}>
<span><small className="text-color-secondary">{__('FTE dichiarato:', 'gepafin')}</small> <strong>{Number(e.fte_pct).toFixed(2)}</strong></span>
{e.fte_pct_verified != null && (
<span style={{ color: 'var(--green-700)' }}>
<small>{__('FTE verificato:', 'gepafin')}</small> <strong>{Number(e.fte_pct_verified).toFixed(2)}</strong>
</span>
)}
</div>
{e.verification_notes && (
<div style={{ marginTop: '0.5rem', padding: '0.4rem 0.6rem', background: 'var(--surface-100)', borderLeft: '3px solid var(--orange-400)', fontSize: '0.9em' }}>
<i className="pi pi-pencil" style={{ marginRight: '0.4rem' }} />{e.verification_notes}
</div>
)}
</div>
{renderThumbsRow(
e.verification_status,
() => quickVerifyUla(e, 'AMMESSA'),
() => quickVerifyUla(e, 'RESPINTA'),
() => openPreview(e.supporting_doc_filename, `${__('Documento ULA', 'gepafin')}${e.full_name}`),
() => downloadStub(e.supporting_doc_filename),
e.supporting_doc_filename,
<Button icon="pi pi-pencil" rounded outlined severity="warning"
disabled={!isVerifiable}
onClick={() => setUlaRectDialog({
visible: true,
employee: {
...e,
fte_pct_verified: e.fte_pct_verified != null ? Number(e.fte_pct_verified) : Number(e.fte_pct)
}
})}
tooltip={__('Rettifica FTE', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Rettifica', 'gepafin')} />
)}
</div>
</li>
);
})}
</ol>
</div>
</>)}
<div className="appPage__spacer"></div>
{/* DOCUMENTI */}
<div className="appPage__spacer"></div>
<div className="appPageSection">
<h2>{__('Documenti', 'gepafin')}</h2>
<div className="fieldsRepeater">
<h2>{__('Verifica documenti', 'gepafin')}</h2>
<ol className="appPageSection__list">
{docsRequired.map(dr => {
const existing = practice.documents.find(d => d.doc_code === dr.code);
const doc = practice.documents.find(d => d.doc_code === dr.code) || { doc_code: dr.code };
const cfg = VERIFICATION_DOC_TAG[doc.verification_status || 'PENDING'];
return (
<div key={dr.code} className="fieldsRepeater__panel"
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '0.75rem 1rem',
display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<i className={existing?.filename ? 'pi pi-check-circle' : 'pi pi-times-circle'}
style={{ color: existing?.filename ? 'var(--green-500)' : 'var(--orange-500)', fontSize: '1.25rem' }} />
<div style={{ flex: 1, minWidth: '200px' }}>
<strong>{dr.label}</strong>
<div><small className="text-color-secondary"><code>{dr.code}</code></small></div>
<li key={dr.code} className="appPageSection__listItem">
<div className="appPageSection__listItemRow">
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<strong>{dr.label}</strong>
<code style={{ fontSize: '0.85em' }}>{dr.code}</code>
{doc.filename ? <Tag severity={cfg.severity} value={cfg.label} /> : <Tag severity="danger" value={__('Non caricato', 'gepafin')} />}
</div>
{doc.filename && (
<div style={{ marginTop: '0.25rem', fontSize: '0.9em', color: 'var(--text-color-secondary)' }}>
<i className="pi pi-file" /> {doc.filename} · {__('caricato il', 'gepafin')} {formatDateTime(doc.uploaded_at)}
</div>
)}
{doc.verification_notes && (
<div style={{ marginTop: '0.5rem', padding: '0.4rem 0.6rem', background: 'var(--surface-100)', borderLeft: '3px solid var(--orange-400)', fontSize: '0.9em' }}>
<i className="pi pi-pencil" style={{ marginRight: '0.4rem' }} />{doc.verification_notes}
</div>
)}
</div>
{renderThumbsRow(
doc.verification_status,
() => quickVerifyDoc(doc, 'VALIDO'),
() => setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'NON_VALIDO' }),
() => openPreview(doc.filename, `${dr.label}${doc.filename || ''}`),
() => downloadStub(doc.filename),
doc.filename,
<Button icon="pi pi-clock" rounded outlined severity="warning"
disabled={!isVerifiable || !doc.filename}
onClick={() => setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'SCADUTO' })}
tooltip={__('Segna come scaduto', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Scaduto', 'gepafin')} />
)}
</div>
<div style={{ flex: 2, minWidth: '200px' }}>
{existing?.filename
? <span><i className="pi pi-file" /> {existing.filename} {__('caricato il', 'gepafin')} {formatDateTime(existing.uploaded_at)}</span>
: <span className="text-color-secondary">{__('Non caricato', 'gepafin')}</span>}
</div>
</div>
</li>
);
})}
</div>
</ol>
</div>
{/* ---------- DIALOG APPROVA ---------- */}
{/* VERBALE ISTRUTTORIA */}
{isVerifiable && (<>
<div className="appPage__spacer"></div>
<div className="appPageSection" style={{ background: 'var(--surface-50)', padding: '1.25rem', borderRadius: '6px' }}>
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Verbale istruttoria', 'gepafin')}</h2>
<div style={{ marginBottom: '1rem' }}>
<h3 style={{ fontSize: '1rem', margin: '0.5rem 0' }}>{__('Checklist finale', 'gepafin')}</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{[
{ id: 'domanda_completa', label: __('Documentazione completa e coerente', 'gepafin') },
{ id: 'ula_ok', label: __('Incremento ULA > 1 verificato', 'gepafin') },
{ id: 'erogato_in_range', label: __('Importo erogato entro il range bando', 'gepafin') }
].map(item => (
<div key={item.id} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<Checkbox inputId={item.id} checked={!!checklist[item.id]}
onChange={(e) => saveFinalNotes({
instructor_checklist: { ...checklist, [item.id]: e.checked }
})} />
<label htmlFor={item.id}>{item.label}</label>
</div>
))}
</div>
</div>
<div className="appForm p-fluid">
<div className="appForm__field">
<label>{__('Note sintetiche di istruttoria', 'gepafin')}</label>
<InputTextarea rows={4} autoResize
defaultValue={practice.instructor_final_notes || ''}
onBlur={(e) => {
if (e.target.value !== (practice.instructor_final_notes || '')) {
saveFinalNotes({ instructor_final_notes: e.target.value });
}
}}
placeholder={__('Note di sintesi che saranno incluse nel verbale finale...', 'gepafin')} />
<small className="text-color-secondary">{__('Le note si salvano quando esci dal campo.', 'gepafin')}</small>
</div>
</div>
</div>
</>)}
{/* ============ 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>
{/* Rettifica fattura */}
<Dialog visible={invRectDialog.visible} style={{ width: '640px', maxWidth: '95vw' }}
header={__('Rettifica fattura', 'gepafin')} modal
onHide={() => setInvRectDialog({ visible: false, invoice: null })}>
{invRectDialog.invoice && (
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); saveInvoiceRect(); }}>
<div style={{ padding: '0.75rem', background: 'var(--surface-50)', borderRadius: '4px', marginBottom: '1rem', fontSize: '0.9em' }}>
<strong>{invRectDialog.invoice.invoice_number}</strong> — {invRectDialog.invoice.supplier_name}<br />
<span className="text-color-secondary">{invRectDialog.invoice.description}</span>
</div>
<h4 style={{ margin: '0 0 0.5rem 0' }}>{__('Dichiarato dal beneficiario', 'gepafin')}</h4>
<div className="appForm__cols" style={{ opacity: 0.7 }}>
<div className="appForm__field">
<label>{__('Imponibile', 'gepafin')}</label>
<InputNumber value={Number(invRectDialog.invoice.taxable)} disabled mode="currency" currency="EUR" locale="it-IT" />
</div>
<div className="appForm__field">
<label>IVA</label>
<InputNumber value={Number(invRectDialog.invoice.vat)} disabled mode="currency" currency="EUR" locale="it-IT" />
</div>
<div className="appForm__field">
<label>{__('Totale', 'gepafin')}</label>
<InputNumber value={Number(invRectDialog.invoice.total)} disabled mode="currency" currency="EUR" locale="it-IT" />
</div>
</div>
<h4 style={{ margin: '1rem 0 0.5rem 0', color: 'var(--green-700)' }}>{__('Valori verificati (rettifica)', 'gepafin')}</h4>
<div className="appForm__cols">
<div className="appForm__field">
<label>{__('Imponibile ammesso', 'gepafin')}</label>
<InputNumber value={invRectDialog.invoice.taxable_verified} mode="currency" currency="EUR" locale="it-IT"
onValueChange={(e) => setInvRectDialog(d => ({ ...d, invoice: { ...d.invoice, taxable_verified: e.value } }))} />
</div>
<div className="appForm__field">
<label>{__('IVA ammessa', 'gepafin')}</label>
<InputNumber value={invRectDialog.invoice.vat_verified} mode="currency" currency="EUR" locale="it-IT"
onValueChange={(e) => setInvRectDialog(d => ({ ...d, invoice: { ...d.invoice, vat_verified: e.value } }))} />
</div>
<div className="appForm__field">
<label>{__('Totale ammesso', 'gepafin')}</label>
<InputNumber value={invRectDialog.invoice.total_verified} mode="currency" currency="EUR" locale="it-IT"
onValueChange={(e) => setInvRectDialog(d => ({ ...d, invoice: { ...d.invoice, total_verified: e.value } }))} />
</div>
</div>
<div className="appForm__field">
<label>{__('Motivo della rettifica', 'gepafin')}</label>
<InputTextarea rows={3} autoResize
value={invRectDialog.invoice.verification_notes || ''}
onChange={(e) => setInvRectDialog(d => ({ ...d, invoice: { ...d.invoice, verification_notes: e.target.value } }))}
placeholder={__('Esempio: decurtata quota di euro X per voce non ammissibile (assicurazione)...', 'gepafin')} />
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setInvRectDialog({ visible: false, invoice: null })} />
<Button type="submit" label={__('Salva rettifica (parziale)', 'gepafin')} icon="pi pi-check" severity="warning" />
</div>
</form>
)}
</Dialog>
{/* Rettifica ULA */}
<Dialog visible={ulaRectDialog.visible} style={{ width: '520px' }}
header={__('Rettifica FTE dipendente', 'gepafin')} modal
onHide={() => setUlaRectDialog({ visible: false, employee: null })}>
{ulaRectDialog.employee && (
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); saveUlaRect(); }}>
<div style={{ padding: '0.75rem', background: 'var(--surface-50)', borderRadius: '4px', marginBottom: '1rem', fontSize: '0.9em' }}>
<strong>{ulaRectDialog.employee.full_name}</strong> ({ulaRectDialog.employee.codice_fiscale})<br />
<span className="text-color-secondary">FTE dichiarato: {Number(ulaRectDialog.employee.fte_pct).toFixed(2)}</span>
</div>
<div className="appForm__field">
<label>{__('FTE ammesso (0.00 - 1.00)', 'gepafin')}</label>
<InputNumber value={ulaRectDialog.employee.fte_pct_verified} mode="decimal" minFractionDigits={2} maxFractionDigits={4} min={0} max={1}
onValueChange={(e) => setUlaRectDialog(d => ({ ...d, employee: { ...d.employee, fte_pct_verified: e.value } }))} />
</div>
<div className="appForm__field">
<label>{__('Motivo rettifica', 'gepafin')}</label>
<InputTextarea rows={3} autoResize
value={ulaRectDialog.employee.verification_notes || ''}
onChange={(e) => setUlaRectDialog(d => ({ ...d, employee: { ...d.employee, verification_notes: e.target.value } }))}
placeholder={__('Esempio: dipendente in part-time verificato al 60% non 100%...', 'gepafin')} />
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setUlaRectDialog({ visible: false, employee: null })} />
<Button type="submit" label={__('Salva rettifica', 'gepafin')} icon="pi pi-check" severity="warning" />
</div>
</form>
)}
</Dialog>
{/* Note documento (scaduto/non valido) */}
<Dialog visible={docNoteDialog.visible} style={{ width: '520px' }}
header={docNoteDialog.status === 'SCADUTO' ? __('Segna documento scaduto', 'gepafin') : __('Marca documento non valido', 'gepafin')}
modal onHide={() => setDocNoteDialog({ visible: false, doc: null, status: null })}>
{docNoteDialog.doc && (
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); saveDocNote(); }}>
<div className="appForm__field">
<label>{__('Motivazione', 'gepafin')}</label>
<InputTextarea rows={4} autoResize
value={docNoteDialog.doc.verification_notes}
onChange={(e) => setDocNoteDialog(d => ({ ...d, doc: { ...d.doc, verification_notes: e.target.value } }))}
placeholder={docNoteDialog.status === 'SCADUTO'
? __('Esempio: DURC scaduto il 15/10/2021, non valido al momento della rendicontazione...', 'gepafin')
: __('Esempio: visura camerale non corrisponde alla ragione sociale del beneficiario...', 'gepafin')} />
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setDocNoteDialog({ visible: false, doc: null, status: null })} />
<Button type="submit" label={__('Conferma', 'gepafin')} icon="pi pi-check" severity={docNoteDialog.status === 'SCADUTO' ? 'warning' : 'danger'} />
</div>
</form>
)}
</Dialog>
{/* DIALOG APPROVA */}
<Dialog visible={approveDialog.visible} style={{ width: '480px' }}
header={__('Approva pratica', 'gepafin')} modal
onHide={() => setApproveDialog({ visible: false, amount: null })}>
@@ -425,7 +801,7 @@ const IstruttoriaPratica = () => {
<InputNumber value={approveDialog.amount} mode="currency" currency="EUR" locale="it-IT"
onValueChange={(e) => setApproveDialog(d => ({ ...d, amount: e.value }))} />
<small className="text-color-secondary">
{__('Valore calcolato:', 'gepafin')} {euro(totals.remission_due || 0)}. {__('Puoi modificarlo se necessario.', 'gepafin')}
{__('Valore calcolato da verificati:', 'gepafin')} {euro(totals.remission_due)}
</small>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
@@ -435,7 +811,7 @@ const IstruttoriaPratica = () => {
</form>
</Dialog>
{/* ---------- DIALOG RESPINGI ---------- */}
{/* DIALOG RESPINGI */}
<Dialog visible={rejectDialog.visible} style={{ width: '560px' }}
header={__('Respingi pratica', 'gepafin')} modal
onHide={() => setRejectDialog({ visible: false, reason: '' })}>
@@ -453,7 +829,7 @@ const IstruttoriaPratica = () => {
</form>
</Dialog>
{/* ---------- DIALOG SOCCORSO ---------- */}
{/* DIALOG SOCCORSO */}
<Dialog visible={amendDialog.visible} style={{ width: '560px' }}
header={__('Avvia soccorso istruttorio', 'gepafin')} modal
onHide={() => setAmendDialog({ visible: false, text: '', deadline: null })}>
@@ -463,7 +839,6 @@ const IstruttoriaPratica = () => {
<InputTextarea value={amendDialog.text} rows={5} autoResize
onChange={(e) => setAmendDialog(d => ({ ...d, text: e.target.value }))}
placeholder={__('Descrivi le integrazioni richieste...', 'gepafin')} />
<small className="text-color-secondary">{__('Sarà visibile al beneficiario, che potrà rispondere integrando la documentazione.', 'gepafin')}</small>
</div>
<div className="appForm__field">
<label>{__('Scadenza risposta', 'gepafin')}</label>