diff --git a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
index 3fd8546..19df8ed 100644
--- a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
+++ b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
@@ -61,6 +61,7 @@ const schemaJsonToForm = (j) => {
return {
amount_min: gate.amount_range?.min ?? 5000,
amount_max: gate.amount_range?.max ?? 25000,
+ period_start: gate.period_start ? new Date(gate.period_start) : null,
period_end: gate.period_end ? new Date(gate.period_end) : null,
period_start_rule: gate.period_start_rule ?? 'erogato_date',
iva_regimes_allowed: ivaAllowed,
@@ -125,6 +126,7 @@ const formToSchemaJson = (f, base = null) => {
cap_absolute: f.cap_absolute,
iva_ordinario_imponibile_only: f.iva_ordinario_imponibile_only,
period_start_rule: f.period_start_rule,
+ period_start: fmtDate(f.period_start),
period_end: fmtDate(f.period_end),
require_at_least_one_invoice_per_nonzero_category: f.require_invoice_per_category,
require_ula_above_threshold: f.require_ula_above_threshold,
@@ -333,13 +335,19 @@ const BandoRendicontazioneSchemaEdit = () => {
- {__('Periodo di ammissibilità — inizio','gepafin')}
+ {__('Inizio periodo — regola','gepafin')}
update({period_start_rule: e.value})}
options={PERIOD_START_RULES} disabled={readOnly} />
- {__('Periodo di ammissibilità — fine','gepafin')}
+ {__('Inizio periodo — data (se fissa)','gepafin')}
+ update({period_start: e.value})}
+ dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
+ {__("Usata dalla verifica date fatture. Compila se la regola non è 'data erogazione'.",'gepafin')}
+
+
+ {__('Fine periodo','gepafin')}
update({period_end: e.value})}
dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
diff --git a/src/modules/rendicontazione/pages/IstruttoriaPratica.js b/src/modules/rendicontazione/pages/IstruttoriaPratica.js
index 648aab8..e9914c6 100644
--- a/src/modules/rendicontazione/pages/IstruttoriaPratica.js
+++ b/src/modules/rendicontazione/pages/IstruttoriaPratica.js
@@ -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 (
+
+
+
+ {extraButtons}
+
+
+
+ );
+ };
+
// ---------- render ----------
if (loading) {
return
;
@@ -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 (
+ {/* HEADER */}
-
{__('Istruttoria pratica', 'gepafin')}
+
{__('Istruttoria pratica rendicontazione', 'gepafin')}
- {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}
@@ -204,6 +311,9 @@ const IstruttoriaPratica = () => {
{isDecidable && (<>
setApproveDialog({ visible: true, amount: totals.remission_due || 0 })} />
{
{/* RIEPILOGO */}
-
{__('Riepilogo', 'gepafin')}
-
-
-
{__('Azienda', 'gepafin')}
-
Company #{practice.company_id}
-
+
{__('Riepilogo finanziario', 'gepafin')}
+
{__('Erogato', 'gepafin')}
{euro(practice.amount_erogato)}
@@ -235,16 +341,24 @@ const IstruttoriaPratica = () => {
{practice.iva_regime || '—'}
-
{__('Totale fatture', 'gepafin')}
-
{euro(totals.grand_total || 0)}
+
{__('Totale dichiarato', 'gepafin')}
+
{euro(totals.grand_total_declared)}
+
+
+
{__('Totale verificato', 'gepafin')}
+
{euro(totals.grand_total_verified)}
{__('Cap remissione', 'gepafin')}
-
{euro(totals.max_remission || 0)}
+
{euro(totals.max_remission)}
-
{__('Remissione calcolata', 'gepafin')}
-
{euro(totals.remission_due || 0)}
+
{__('Remissione da riconoscere', 'gepafin')}
+
{euro(totals.remission_due)}
+
+
+
{__('Residuo da restituire', 'gepafin')}
+
{euro(totals.residuo_da_restituire)}
{practice.approved_remission != null && (
@@ -290,9 +404,7 @@ const IstruttoriaPratica = () => {
{__('Richiesta istruttore:', 'gepafin')}
{a.request_text}
{a.response_text && (<>
-
- {__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):
-
+
{__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):
{a.response_text}
>)}
@@ -304,118 +416,382 @@ const IstruttoriaPratica = () => {
>)}
- {/* GATE CHECKS */}
- {gate && (
-
-
{__('Requisiti di validità', 'gepafin')}
-
- {gate.checks.map((c, i) => (
-
-
-
-
{c.label}
-
{c.detail}
-
-
- ))}
-
-
- )}
-
-
-
- {/* FATTURE PER CATEGORIA */}
+ {/* FATTURE PER CATEGORIA — pattern ListOfFiles */}
-
{__('Fatture rendicontate', 'gepafin')}
-
- {categories.map(cat => {
- const invs = invoicesOfCat(cat.code);
- const total = perCat[cat.code] || 0;
- return (
-
-
-
- {cat.code} — {cat.label}
-
-
-
{euro(total)}
-
{invs.length} {__('fatture', 'gepafin')}
-
+
{__('Verifica fatture', 'gepafin')}
+
+ {__('Per ogni fattura: anteprima, download, pollice su per ammettere, pollice giù per respingere, icona matita per rettificare importi ammissibili.', 'gepafin')}
+
+
+ {categories.map(cat => {
+ const invs = invoicesOfCat(cat.code);
+ const totalDecl = perDecl[cat.code] || 0;
+ const totalVerif = perVerif[cat.code] || 0;
+ return (
+
+
+
+ {cat.code} — {cat.label}
+
+
+
{__('Dichiarato:', 'gepafin')} {euro(totalDecl)}
+
{__('Verificato:', 'gepafin')} {euro(totalVerif)}
- {invs.length > 0 ? (
-
-
- formatDate(r.invoice_date)} />
- formatDate(r.payment_date)} />
-
- {r.description.slice(0, 40)}{r.description.length > 40 ? '…' : ''} } />
- euro(r.taxable)} />
- euro(r.vat)} />
- euro(r.total)} />
-
- ) :
{__('Nessuna fattura', 'gepafin')} }
- );
- })}
-
+
+ {invs.length === 0 ? (
+
{__('Nessuna fattura in questa categoria', 'gepafin')}
+ ) : (
+
+ {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 (
+
+
+
+
+ {inv.invoice_number}
+ —
+ {inv.supplier_name}
+
+ {invalidDates && }
+
+
+ {__('Emessa', 'gepafin')} {formatDate(inv.invoice_date)} · {__('pagata', 'gepafin')} {formatDate(inv.payment_date)} · {inv.description}
+
+
+ {__('Dichiarato imp.:', 'gepafin')} {euro(inv.taxable)} · IVA {euro(inv.vat)} · tot {euro(inv.total)}
+ {hasRect && (
+
+ {__('Verificato imp.:', 'gepafin')} {euro(inv.taxable_verified)} · IVA {euro(inv.vat_verified)} · tot {euro(inv.total_verified)}
+
+ )}
+
+ {inv.verification_notes && (
+
+ {inv.verification_notes}
+
+ )}
+
+ {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`,
+
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')} />
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
- {/* ULA */}
- {ulaSection.enabled && (<>
+ {/* ULA DIPENDENTI */}
+ {ulaSection.enabled && practice.ula_employees.length > 0 && (<>
-
{__('Dipendenti ULA', 'gepafin')}
- {practice.ula_employees.length > 0 ? (
-
-
-
- CONTRACT_TYPES[r.contract_type] || r.contract_type} />
-
- Number(r.fte_pct).toFixed(2)} />
- `${formatDate(r.period_start_date)} → ${formatDate(r.period_end_date)}`} />
- r.supporting_doc_filename
- ? {r.supporting_doc_filename}
- : — } />
-
- ) :
{__('Nessun dipendente caricato', 'gepafin')}
}
+
{__('Verifica dipendenti ULA', 'gepafin')}
+
+ {practice.ula_employees.map(e => {
+ const cfg = VERIFICATION_INVOICE_TAG[e.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
+ return (
+
+
+
+
+ {e.full_name}
+ ({e.codice_fiscale})
+
+
+
+ {CONTRACT_TYPES[e.contract_type] || e.contract_type}
+ {e.role_description && ` · ${e.role_description}`}
+ {' · '}{formatDate(e.period_start_date)} → {formatDate(e.period_end_date)}
+
+
+ {__('FTE dichiarato:', 'gepafin')} {Number(e.fte_pct).toFixed(2)}
+ {e.fte_pct_verified != null && (
+
+ {__('FTE verificato:', 'gepafin')} {Number(e.fte_pct_verified).toFixed(2)}
+
+ )}
+
+ {e.verification_notes && (
+
+ {e.verification_notes}
+
+ )}
+
+ {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,
+
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')} />
+ )}
+
+
+ );
+ })}
+
>)}
-
-
{/* DOCUMENTI */}
+
-
{__('Documenti', 'gepafin')}
-
+
{__('Verifica documenti', 'gepafin')}
+
{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 (
-
-
-
-
{dr.label}
-
{dr.code}
+
+
+
+
+ {dr.label}
+ {dr.code}
+ {doc.filename ? : }
+
+ {doc.filename && (
+
+ {doc.filename} · {__('caricato il', 'gepafin')} {formatDateTime(doc.uploaded_at)}
+
+ )}
+ {doc.verification_notes && (
+
+ {doc.verification_notes}
+
+ )}
+
+ {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,
+
setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'SCADUTO' })}
+ tooltip={__('Segna come scaduto', 'gepafin')} tooltipOptions={{ position: 'top' }}
+ aria-label={__('Scaduto', 'gepafin')} />
+ )}
-
- {existing?.filename
- ? {existing.filename} — {__('caricato il', 'gepafin')} {formatDateTime(existing.uploaded_at)}
- : {__('Non caricato', 'gepafin')} }
-
-
+
);
})}
-
+
- {/* ---------- DIALOG APPROVA ---------- */}
+ {/* VERBALE ISTRUTTORIA */}
+ {isVerifiable && (<>
+
+
+
{__('Verbale istruttoria', 'gepafin')}
+
+
+
{__('Checklist finale', 'gepafin')}
+
+ {[
+ { 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 => (
+
+ saveFinalNotes({
+ instructor_checklist: { ...checklist, [item.id]: e.checked }
+ })} />
+ {item.label}
+
+ ))}
+
+
+
+
+
+ {__('Note sintetiche di istruttoria', 'gepafin')}
+ {
+ 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')} />
+ {__('Le note si salvano quando esci dal campo.', 'gepafin')}
+
+
+
+ >)}
+
+ {/* ============ DIALOGS ============ */}
+
+ {/* Preview PDF */}
+
setPreviewDialog({ visible: false, filename: null, title: null })}>
+
+
+
{previewDialog.filename || '—'}
+
+ {__('Anteprima PDF — in sandbox il file reale non è caricato. In produzione qui viene visualizzato il PDF originale della fattura/documento.', 'gepafin')}
+
+
+
+ setPreviewDialog({ visible: false, filename: null, title: null })} />
+
+
+
+ {/* Rettifica fattura */}
+
setInvRectDialog({ visible: false, invoice: null })}>
+ {invRectDialog.invoice && (
+
+ )}
+
+
+ {/* Rettifica ULA */}
+
setUlaRectDialog({ visible: false, employee: null })}>
+ {ulaRectDialog.employee && (
+
+ )}
+
+
+ {/* Note documento (scaduto/non valido) */}
+
setDocNoteDialog({ visible: false, doc: null, status: null })}>
+ {docNoteDialog.doc && (
+
+ )}
+
+
+ {/* DIALOG APPROVA */}
setApproveDialog({ visible: false, amount: null })}>
@@ -425,7 +801,7 @@ const IstruttoriaPratica = () => {
setApproveDialog(d => ({ ...d, amount: e.value }))} />
- {__('Valore calcolato:', 'gepafin')} {euro(totals.remission_due || 0)}. {__('Puoi modificarlo se necessario.', 'gepafin')}
+ {__('Valore calcolato da verificati:', 'gepafin')} {euro(totals.remission_due)}
@@ -435,7 +811,7 @@ const IstruttoriaPratica = () => {
- {/* ---------- DIALOG RESPINGI ---------- */}
+ {/* DIALOG RESPINGI */}
setRejectDialog({ visible: false, reason: '' })}>
@@ -453,7 +829,7 @@ const IstruttoriaPratica = () => {
- {/* ---------- DIALOG SOCCORSO ---------- */}
+ {/* DIALOG SOCCORSO */}
setAmendDialog({ visible: false, text: '', deadline: null })}>
@@ -463,7 +839,6 @@ const IstruttoriaPratica = () => {
setAmendDialog(d => ({ ...d, text: e.target.value }))}
placeholder={__('Descrivi le integrazioni richieste...', 'gepafin')} />
- {__('Sarà visibile al beneficiario, che potrà rispondere integrando la documentazione.', 'gepafin')}
{__('Scadenza risposta', 'gepafin')}
diff --git a/src/modules/rendicontazione/service/rendicontazioneService.js b/src/modules/rendicontazione/service/rendicontazioneService.js
index 7eefc96..23865cc 100644
--- a/src/modules/rendicontazione/service/rendicontazioneService.js
+++ b/src/modules/rendicontazione/service/rendicontazioneService.js
@@ -238,3 +238,38 @@ const extendInstructor = {
};
Object.assign(RendicontazioneService, extendInstructor);
+
+
+// ====================== VERIFICA SINGOLA RIGA ISTRUTTORE ======================
+
+const extendVerify = {
+ verifyInvoice(practiceId, invoiceId, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/invoices/${invoiceId}/verify`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ verifyUlaEmployee(practiceId, empId, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/ula-employees/${empId}/verify`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ verifyDocument(practiceId, docCode, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/documents/${docCode}/verify`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ setInstructorFinalNotes(practiceId, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/final-notes`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ }
+};
+
+Object.assign(RendicontazioneService, extendVerify);