diff --git a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
index 19df8ed..f547e51 100644
--- a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
+++ b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
@@ -31,6 +31,12 @@ const IVA_REGIMES = [
{ value: 'ESENTE', label: 'Esente' }
];
+const AMOUNT_BASIS_OPTIONS = [
+ { value: 'imponibile_always', label: 'Solo imponibile (sempre)' },
+ { value: 'imponibile_only_ordinario', label: 'Imponibile in ordinario, totale in forfettario' },
+ { value: 'totale_always', label: 'Totale IVA inclusa (sempre)' }
+];
+
const PERIOD_START_RULES = [
{ value: 'erogato_date', label: 'Data di erogazione del finanziamento' },
{ value: 'contract_signed_date', label: 'Data firma contratto' },
@@ -63,6 +69,7 @@ const schemaJsonToForm = (j) => {
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,
+ amount_basis: gate.amount_basis || (gate.iva_ordinario_imponibile_only === false ? 'totale_always' : 'imponibile_only_ordinario'),
period_start_rule: gate.period_start_rule ?? 'erogato_date',
iva_regimes_allowed: ivaAllowed,
iva_ordinario_imponibile_only: gate.iva_ordinario_imponibile_only ?? true,
@@ -128,6 +135,7 @@ const formToSchemaJson = (f, base = null) => {
period_start_rule: f.period_start_rule,
period_start: fmtDate(f.period_start),
period_end: fmtDate(f.period_end),
+ amount_basis: f.amount_basis,
require_at_least_one_invoice_per_nonzero_category: f.require_invoice_per_category,
require_ula_above_threshold: f.require_ula_above_threshold,
require_all_documents_resolved: f.require_all_documents_resolved
@@ -352,6 +360,16 @@ const BandoRendicontazioneSchemaEdit = () => {
dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
+
+
+ {__('Base di calcolo ammissibile','gepafin')}
+ update({amount_basis: e.value})}
+ options={AMOUNT_BASIS_OPTIONS} disabled={readOnly} />
+
+ {__("Determina su quale importo delle fatture si calcola la remissione. La norma del bando può prevedere regimi diversi.", 'gepafin')}
+
+
diff --git a/src/modules/rendicontazione/pages/IstruttoriaPratica.js b/src/modules/rendicontazione/pages/IstruttoriaPratica.js
index e9914c6..f294583 100644
--- a/src/modules/rendicontazione/pages/IstruttoriaPratica.js
+++ b/src/modules/rendicontazione/pages/IstruttoriaPratica.js
@@ -7,13 +7,13 @@ import { Toast } from 'primereact/toast';
import { Tag } from 'primereact/tag';
import { Skeleton } from 'primereact/skeleton';
import { Dialog } from 'primereact/dialog';
-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';
@@ -68,9 +68,12 @@ const IstruttoriaPratica = () => {
// 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 });
+ // tabelle: expanded rows + buffer modifiche inline
+ const [expandedInv, setExpandedInv] = useState({});
+ const [expandedUla, setExpandedUla] = useState({});
+ const [invDraft, setInvDraft] = useState({}); // { invoiceId: { amount_verified, notes } }
+ const [ulaDraft, setUlaDraft] = useState({}); // { employeeId: { fte_pct_verified, notes } }
const [approveDialog, setApproveDialog] = useState({ visible: false, amount: null });
const [rejectDialog, setRejectDialog] = useState({ visible: false, reason: '' });
@@ -140,31 +143,97 @@ const IstruttoriaPratica = () => {
const quickVerifyDoc = (doc, newStatus) => {
RendicontazioneService.verifyDocument(practiceId, doc.doc_code,
{ verification_status: newStatus, verification_notes: null },
- afterOk(__(`Documento ${newStatus.toLowerCase()}`, 'gepafin')), onErr);
+ (resp) => {
+ const updated = resp?.data || {};
+ setBundle(b => {
+ if (!b) return b;
+ const exists = b.practice.documents.find(d => d.doc_code === doc.doc_code);
+ const newDocs = exists
+ ? b.practice.documents.map(d => d.doc_code === doc.doc_code ? { ...d, ...updated } : d)
+ : [...b.practice.documents, updated];
+ return { ...b, practice: { ...b.practice, documents: newDocs } };
+ });
+ toast.current?.show({ severity: 'success', summary: __(`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);
+ // Refresh solo gate_check (totali) senza rileggere tutta la pratica
+ const refreshGateOnly = () => {
+ RendicontazioneService.gateCheck(practiceId,
+ (resp) => setBundle(b => b ? { ...b, gate_check: resp?.data } : b),
+ () => {});
};
- 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);
+ // Save INLINE fattura con update LOCALE (no reload pagina)
+ const saveInvoiceInline = (invoice, explicitStatus = null) => {
+ const draft = invDraft[invoice.id] || {};
+ const useTaxable = (gate?.totals?.use_taxable_only ?? true);
+ const declared = Number(useTaxable ? invoice.taxable : invoice.total);
+ const verified = draft.amount_verified != null ? Number(draft.amount_verified) : declared;
+ const notes = draft.notes != null ? draft.notes : invoice.verification_notes;
+
+ let status = explicitStatus;
+ if (!status) {
+ if (verified <= 0) status = 'RESPINTA';
+ else if (Math.abs(verified - declared) < 0.005) status = 'AMMESSA';
+ else status = 'PARZIALE';
+ }
+ const body = { verification_status: status, verification_notes: notes || null };
+ if (status !== 'RESPINTA' && status !== 'PENDING') {
+ if (useTaxable) {
+ body.taxable_verified = verified;
+ body.vat_verified = invoice.vat;
+ body.total_verified = Number(verified) + Number(invoice.vat || 0);
+ } else {
+ body.total_verified = verified;
+ body.vat_verified = invoice.vat;
+ body.taxable_verified = Number(verified) - Number(invoice.vat || 0);
+ }
+ }
+ RendicontazioneService.verifyInvoice(practiceId, invoice.id, body,
+ (resp) => {
+ setInvDraft(prev => { const n = {...prev}; delete n[invoice.id]; return n; });
+ const updated = resp?.data || {};
+ // update LOCALE della singola fattura (no full page reload!)
+ setBundle(b => {
+ if (!b) return b;
+ const newInvoices = b.practice.invoices.map(i => i.id === invoice.id ? { ...i, ...updated } : i);
+ return { ...b, practice: { ...b.practice, invoices: newInvoices } };
+ });
+ refreshGateOnly();
+ toast.current?.show({ severity: 'success', summary: __(`Fattura ${status.toLowerCase()}`, 'gepafin') });
+ }, onErr);
+ };
+
+ // Save INLINE ULA con update LOCALE (no reload pagina)
+ const saveUlaInline = (emp, explicitStatus = null) => {
+ const draft = ulaDraft[emp.id] || {};
+ const declared = Number(emp.fte_pct);
+ const verified = draft.fte_pct_verified != null ? Number(draft.fte_pct_verified) : declared;
+ const notes = draft.notes != null ? draft.notes : emp.verification_notes;
+
+ let status = explicitStatus;
+ if (!status) {
+ if (verified <= 0) status = 'RESPINTA';
+ else if (Math.abs(verified - declared) < 0.0005) status = 'AMMESSA';
+ else status = 'PARZIALE';
+ }
+ const body = { verification_status: status, verification_notes: notes || null };
+ if (status !== 'RESPINTA' && status !== 'PENDING') {
+ body.fte_pct_verified = verified;
+ }
+ RendicontazioneService.verifyUlaEmployee(practiceId, emp.id, body,
+ (resp) => {
+ setUlaDraft(prev => { const n = {...prev}; delete n[emp.id]; return n; });
+ const updated = resp?.data || {};
+ setBundle(b => {
+ if (!b) return b;
+ const newUlas = b.practice.ula_employees.map(x => x.id === emp.id ? { ...x, ...updated } : x);
+ return { ...b, practice: { ...b.practice, ula_employees: newUlas } };
+ });
+ refreshGateOnly();
+ toast.current?.show({ severity: 'success', summary: __(`Dipendente ${status.toLowerCase()}`, 'gepafin') });
+ }, onErr);
};
const saveDocNote = () => {
@@ -172,8 +241,19 @@ const IstruttoriaPratica = () => {
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);
+ (resp) => {
+ setDocNoteDialog({ visible: false, doc: null, status: null });
+ const updated = resp?.data || {};
+ setBundle(b => {
+ if (!b) return b;
+ const exists = b.practice.documents.find(x => x.doc_code === d.doc_code);
+ const newDocs = exists
+ ? b.practice.documents.map(x => x.doc_code === d.doc_code ? { ...x, ...updated } : x)
+ : [...b.practice.documents, updated];
+ return { ...b, practice: { ...b.practice, documents: newDocs } };
+ });
+ toast.current?.show({ severity: 'success', summary: __('Documento aggiornato', 'gepafin') });
+ }, onErr);
};
// Final notes + checklist (debounced inline save)
@@ -328,6 +408,32 @@ const IstruttoriaPratica = () => {
+ {/* BANNER pratica non presa in carico */}
+ {practice.status === 'SUBMITTED' && (
+
+
+
+
+
{__("Pratica non ancora presa in carico", 'gepafin')}
+
+ {__("Per poter verificare fatture, dipendenti ULA e documenti, clicca su «Prendi in carico» qui sopra. Lo stato della pratica passerà a «In lavorazione».", 'gepafin')}
+
+
+
+
+
+ )}
+
+ {practice.status === 'SUBMITTED' &&
}
+
{/* RIEPILOGO */}
{__('Riepilogo finanziario', 'gepafin')}
@@ -416,158 +522,328 @@ const IstruttoriaPratica = () => {
>)}
- {/* FATTURE PER CATEGORIA — pattern ListOfFiles */}
+ {/* FATTURE — tabella inline unica con raggruppamento per categoria */}
{__('Verifica fatture', 'gepafin')}
- {__('Per ogni fattura: anteprima, download, pollice su per ammettere, pollice giù per respingere, icona matita per rettificare importi ammissibili.', 'gepafin')}
+ {__("Modifica l'importo ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", '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)}
-
-
+ {(() => {
+ const useTaxable = totals.use_taxable_only !== false;
+ const declaredLabel = useTaxable ? __('Imponibile dichiarato', 'gepafin') : __('Totale dichiarato', 'gepafin');
+ const ammessoLabel = useTaxable ? __('Imponibile ammesso', 'gepafin') : __('Totale ammesso', 'gepafin');
+ // Ordino le fatture secondo l'ordine definito dallo schema (B1 < B2 < B3)
+ const catOrder = Object.fromEntries(categories.map((c, i) => [c.code, i]));
+ const sortedInvoices = [...practice.invoices].sort((a, b) => {
+ const oa = catOrder[a.category_code] ?? 999;
+ const ob = catOrder[b.category_code] ?? 999;
+ if (oa !== ob) return oa - ob;
+ return (a.invoice_number || '').localeCompare(b.invoice_number || '');
+ });
- {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')} />
- )}
-
-
- );
- })}
-
- )}
-
+ if (sortedInvoices.length === 0) {
+ return
{__('Nessuna fattura caricata', 'gepafin')}
;
+ }
+
+ return (
+
setExpandedInv(e.data)}
+ rowGroupHeaderTemplate={(row) => {
+ const cat = categories.find(c => c.code === row.category_code) || { code: row.category_code, label: '—' };
+ const totalDecl = perDecl[row.category_code] || 0;
+ const totalVerif = perVerif[row.category_code] || 0;
+ return (
+
+
+ {cat.code}
+ {cat.label}
+
+
+
{__('Dichiarato:', 'gepafin')} {euro(totalDecl)}
+
{__('Ammesso:', 'gepafin')} {euro(totalVerif)}
+
+
+ );
+ }}
+ rowExpansionTemplate={(inv) => {
+ const draft = invDraft[inv.id] || {};
+ return (
+
+
+
+ {__("Note dell'istruttore (motivazione rettifica o rigetto)", 'gepafin')}
+ setInvDraft(d => ({ ...d, [inv.id]: { ...(d[inv.id]||{}), notes: ev.target.value } }))}
+ onBlur={() => {
+ const d = invDraft[inv.id] || {};
+ if (d.notes != null && d.notes !== (inv.verification_notes || '')) {
+ saveInvoiceInline(inv);
+ }
+ }}
+ placeholder={__('Es: decurtata quota di 400€ per assicurazione accessoria non ammissibile...', 'gepafin')} />
+
+
+
{__('Dettaglio dichiarato:', 'gepafin')}
+
{__('Imponibile:', 'gepafin')} {euro(inv.taxable)}
+
IVA: {euro(inv.vat)} — {__('Totale:', 'gepafin')} {euro(inv.total)}
+
+
+
+ );
+ }}
+ >
+
+
+ {
+ const bad = r.date_checks && (r.date_checks.invoice_in_period === false || r.date_checks.payment_in_period === false);
+ return
+ {formatDate(r.invoice_date)}
+ {bad && }
+ ;
+ }} />
+
+ {r.description.length > 50 ? r.description.slice(0, 50) + '…' : r.description} } />
+ {euro(useTaxable ? r.taxable : r.total)} } />
+ {
+ const draft = invDraft[r.id] || {};
+ const declared = Number(useTaxable ? r.taxable : r.total);
+ const currentVerified = r.verification_status === 'PENDING'
+ ? declared
+ : (useTaxable ? (r.taxable_verified != null ? Number(r.taxable_verified) : declared)
+ : (r.total_verified != null ? Number(r.total_verified) : declared));
+ const displayVal = draft.amount_verified != null ? draft.amount_verified : currentVerified;
+ return (
+ setInvDraft(d => ({ ...d, [r.id]: { ...(d[r.id]||{}), amount_verified: ev.value } }))}
+ onBlur={() => {
+ const d = invDraft[r.id];
+ if (d && d.amount_verified != null && Math.abs(d.amount_verified - currentVerified) > 0.005) {
+ saveInvoiceInline(r);
+ }
+ }}
+ />
+ );
+ }} />
+ {
+ const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
+ return ;
+ }} />
+ (
+
+ openPreview(r.pdf_filename || `fattura_${r.invoice_number}.pdf`, `Fattura ${r.invoice_number}`)}
+ tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }} />
+ downloadStub(r.pdf_filename || `fattura_${r.invoice_number}.pdf`)}
+ tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }} />
+ {
+ // toggle: se già AMMESSA torno a PENDING
+ if (r.verification_status === 'AMMESSA') {
+ saveInvoiceInline(r, 'PENDING');
+ return;
+ }
+ const draft = invDraft[r.id];
+ if (draft && draft.amount_verified != null) {
+ saveInvoiceInline(r);
+ } else {
+ saveInvoiceInline(r, 'AMMESSA');
+ }
+ }}
+ tooltip={r.verification_status === 'AMMESSA' ? __('Annulla conferma', 'gepafin') : __('Conferma', 'gepafin')}
+ tooltipOptions={{ position: 'top' }} />
+ {
+ if (r.verification_status === 'RESPINTA') {
+ saveInvoiceInline(r, 'PENDING');
+ } else {
+ saveInvoiceInline(r, 'RESPINTA');
+ }
+ }}
+ tooltip={r.verification_status === 'RESPINTA' ? __('Annulla rifiuto', 'gepafin') : __('Respingi', 'gepafin')}
+ tooltipOptions={{ position: 'top' }} />
+
+ )} />
+
);
- })}
+ })()}
- {/* ULA DIPENDENTI */}
+ {/* ULA DIPENDENTI — DataTable semplice con box header sopra */}
{ulaSection.enabled && practice.ula_employees.length > 0 && (<>
{__('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')} />
- )}
+
+ {__("Modifica l'FTE ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", 'gepafin')}
+
+
+ {(() => {
+ const totalFteDecl = practice.ula_employees.reduce((a, e) => a + Number(e.fte_pct || 0), 0);
+ const totalFteVerif = practice.ula_employees
+ .filter(e => ['AMMESSA', 'PARZIALE'].includes(e.verification_status))
+ .reduce((a, e) => a + Number((e.fte_pct_verified != null ? e.fte_pct_verified : e.fte_pct) || 0), 0);
+ const thresholdOK = totalFteVerif >= Number(ulaSection.threshold || 1);
+
+ return (
+
+ {/* Header-box fuori dalla tabella, stesso stile del subheader delle fatture */}
+
+
+ ULA
+ {__('Incremento occupazione (soglia richiesta ≥', 'gepafin')} {Number(ulaSection.threshold || 1).toFixed(2)})
-
- );
- })}
-
+
+
{__('FTE dichiarato:', 'gepafin')} {totalFteDecl.toFixed(2)}
+
{__('FTE ammesso:', 'gepafin')} {totalFteVerif.toFixed(2)}
+
+
+
+
setExpandedUla(e.data)}
+ rowExpansionTemplate={(emp) => {
+ const draft = ulaDraft[emp.id] || {};
+ return (
+
+
+
+ {__("Note dell'istruttore (motivazione rettifica)", 'gepafin')}
+ setUlaDraft(d => ({ ...d, [emp.id]: { ...(d[emp.id]||{}), notes: ev.target.value } }))}
+ onBlur={() => {
+ const d = ulaDraft[emp.id] || {};
+ if (d.notes != null && d.notes !== (emp.verification_notes || '')) {
+ saveUlaInline(emp);
+ }
+ }}
+ placeholder={__('Es: dipendente verificato part-time 50% su LUL, non full-time come dichiarato...', 'gepafin')} />
+
+
+ {__('Dettaglio:', 'gepafin')} {CONTRACT_TYPES[emp.contract_type] || emp.contract_type}
+ {emp.role_description && ` · ${emp.role_description}`}
+ {' · '}{formatDate(emp.period_start_date)} → {formatDate(emp.period_end_date)}
+
+
+
+ );
+ }}
+ >
+
+
+
+ {CONTRACT_TYPES[r.contract_type] || r.contract_type} } />
+ {formatDate(r.period_start_date)} → {formatDate(r.period_end_date)} } />
+ {Number(r.fte_pct).toFixed(2)} } />
+ {
+ const draft = ulaDraft[r.id] || {};
+ const declared = Number(r.fte_pct);
+ const currentVerified = r.verification_status === 'PENDING'
+ ? declared
+ : (r.fte_pct_verified != null ? Number(r.fte_pct_verified) : declared);
+ const displayVal = draft.fte_pct_verified != null ? draft.fte_pct_verified : currentVerified;
+ return (
+ setUlaDraft(d => ({ ...d, [r.id]: { ...(d[r.id]||{}), fte_pct_verified: ev.value } }))}
+ onBlur={() => {
+ const d = ulaDraft[r.id];
+ if (d && d.fte_pct_verified != null && Math.abs(d.fte_pct_verified - currentVerified) > 0.0005) {
+ saveUlaInline(r);
+ }
+ }}
+ />
+ );
+ }} />
+ {
+ const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
+ return ;
+ }} />
+ (
+
+ openPreview(r.supporting_doc_filename, `${r.full_name}`)}
+ tooltip={__('Anteprima allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
+ downloadStub(r.supporting_doc_filename)}
+ tooltip={__('Scarica allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
+ {
+ const draft = ulaDraft[r.id];
+ if (draft && draft.fte_pct_verified != null) {
+ saveUlaInline(r);
+ } else {
+ saveUlaInline(r, 'AMMESSA');
+ }
+ }}
+ tooltip={__('Conferma', 'gepafin')} tooltipOptions={{ position: 'top' }} />
+ saveUlaInline(r, 'RESPINTA')}
+ tooltip={__('Respingi', 'gepafin')} tooltipOptions={{ position: 'top' }} />
+
+ )} />
+
+
+ );
+ })()}
>)}
@@ -599,19 +875,43 @@ const IstruttoriaPratica = () => {
)}
- {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,
+
+ openPreview(doc.filename, `${dr.label} — ${doc.filename || ''}`)}
+ tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }}
+ aria-label={__('Anteprima', 'gepafin')} />
+ downloadStub(doc.filename)}
+ tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }}
+ aria-label={__('Scarica', 'gepafin')} />
setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'SCADUTO' })}
tooltip={__('Segna come scaduto', 'gepafin')} tooltipOptions={{ position: 'top' }}
aria-label={__('Scaduto', 'gepafin')} />
- )}
+ quickVerifyDoc(doc, doc.verification_status === 'VALIDO' ? 'PENDING' : 'VALIDO')}
+ tooltip={doc.verification_status === 'VALIDO' ? __('Annulla valido', 'gepafin') : __('Valido', 'gepafin')}
+ tooltipOptions={{ position: 'top' }}
+ aria-label={__('Valido', 'gepafin')} />
+ {
+ if (doc.verification_status === 'NON_VALIDO') {
+ quickVerifyDoc(doc, 'PENDING');
+ } else {
+ setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'NON_VALIDO' });
+ }
+ }}
+ tooltip={doc.verification_status === 'NON_VALIDO' ? __('Annulla (rimuovi stato)', 'gepafin') : __('Non valido', 'gepafin')}
+ tooltipOptions={{ position: 'top' }}
+ aria-label={__('Non valido', 'gepafin')} />
+
);
@@ -681,92 +981,10 @@ const IstruttoriaPratica = () => {
{/* Rettifica fattura */}
- setInvRectDialog({ visible: false, invoice: null })}>
- {invRectDialog.invoice && (
-
- )}
-
+
{/* Rettifica ULA */}
- setUlaRectDialog({ visible: false, employee: null })}>
- {ulaRectDialog.employee && (
-
- )}
-
+
{/* Note documento (scaduto/non valido) */}