feat(istruttoria UI): tabelle inline editabili, toggle check/reject, no-reload, allineamento ULA
Refactor completo della UI istruttore su pattern Excel-like dichiarato/verificato. Editor schema bando (BandoRendicontazioneSchemaEdit): - Nuovo dropdown 'Base di calcolo ammissibile' (imponibile/totale/regime-dependent) - Nuovo Calendar 'Inizio periodo' accanto al period_start_rule esistente IstruttoriaPratica — refactor totale: - FATTURE: 1 sola DataTable con rowGroupMode='subheader' raggruppato per B1/B2/B3, header colorato per categoria con totali dichiarato/ammesso live - Colonne inline editabili: 'Imponibile ammesso' con InputNumber + save onBlur. Stato auto-calcolato: = dichiarato -> AMMESSA; 0 < x < dichiarato -> PARZIALE; x == 0 -> RESPINTA - Label dinamiche 'Imponibile' vs 'Totale' in base a use_taxable_only - Riga espandibile (pi-chevron) con textarea note istruttore + dettaglio IVA/totale - Toggle icon ✓: se AMMESSA -> PENDING; altrimenti -> AMMESSA - Toggle icon ✗: se RESPINTA -> PENDING; altrimenti -> RESPINTA - Tooltip dinamici 'Conferma' / 'Annulla conferma' - Badge rosso automatico 'Data fuori periodo' su invoice_in_period=false ULA: stesso pattern inline (FTE dichiarato vs FTE ammesso) con header-box manuale SOPRA la DataTable (non rowGroupMode, un solo gruppo) e forzatura tableStyle width:100% per allineamento perfetto con fatture. Documenti: lista con toggle ✓ VALIDO ↔ PENDING, ✗ NON_VALIDO/SCADUTO via dialog. Performance critica — NO FULL RELOAD su verify: - saveInvoiceInline/saveUlaInline/quickVerifyDoc/saveDocNote ora fanno setBundle() con update locale della singola riga - refreshGateOnly() ricarica solo il gate_check (totali) in background - Eliminato il load() completo che faceva sfarfallare la pagina Banner arancione automatico quando status=SUBMITTED: 'Pratica non presa in carico' con CTA 'Prendi in carico'. Bugfix: - Rimossi import inutilizzati (InputText, isNil) - Aggiunti import DataTable, Column UX testata su NAPOLI SAS: 5 fatture 3 categorie, 2 ULA, 4 docs. Totali si aggiornano live, toggle funzionanti, nessuno sfarfallio.
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* BANNER pratica non presa in carico */}
|
||||
{practice.status === 'SUBMITTED' && (
|
||||
<div className="appPageSection">
|
||||
<div style={{
|
||||
padding: '1rem 1.25rem',
|
||||
background: 'var(--orange-50)',
|
||||
border: '1px solid var(--orange-300)',
|
||||
borderLeft: '4px solid var(--orange-500)',
|
||||
borderRadius: '6px',
|
||||
display: 'flex', alignItems: 'center', gap: '1rem', width: '100%'
|
||||
}}>
|
||||
<i className="pi pi-info-circle" style={{ fontSize: '1.5rem', color: 'var(--orange-600)' }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<strong>{__("Pratica non ancora presa in carico", 'gepafin')}</strong>
|
||||
<div style={{ fontSize: '0.9em', color: 'var(--text-color-secondary)', marginTop: '0.25rem' }}>
|
||||
{__("Per poter verificare fatture, dipendenti ULA e documenti, clicca su «Prendi in carico» qui sopra. Lo stato della pratica passerà a «In lavorazione».", 'gepafin')}
|
||||
</div>
|
||||
</div>
|
||||
<Button icon="pi pi-user-plus" iconPos="right"
|
||||
label={__('Prendi in carico', 'gepafin')} onClick={handleClaim} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{practice.status === 'SUBMITTED' && <div className="appPage__spacer"></div>}
|
||||
|
||||
{/* RIEPILOGO */}
|
||||
<div className="appPageSection" style={{ background: 'var(--surface-50)', padding: '1.25rem', borderRadius: '6px' }}>
|
||||
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Riepilogo finanziario', 'gepafin')}</h2>
|
||||
@@ -416,158 +522,328 @@ const IstruttoriaPratica = () => {
|
||||
<div className="appPage__spacer"></div>
|
||||
</>)}
|
||||
|
||||
{/* FATTURE PER CATEGORIA — pattern ListOfFiles */}
|
||||
{/* FATTURE — tabella inline unica con raggruppamento per categoria */}
|
||||
<div className="appPageSection">
|
||||
<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')}
|
||||
{__("Modifica l'importo ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", '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>
|
||||
</div>
|
||||
{(() => {
|
||||
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 ? (
|
||||
<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>
|
||||
if (sortedInvoices.length === 0) {
|
||||
return <p className="text-color-secondary">{__('Nessuna fattura caricata', 'gepafin')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
value={sortedInvoices}
|
||||
dataKey="id"
|
||||
size="small"
|
||||
rowGroupMode="subheader"
|
||||
groupRowsBy="category_code"
|
||||
sortMode="single"
|
||||
sortField="category_code"
|
||||
sortOrder={1}
|
||||
tableStyle={{ minWidth: '1100px', width: '100%' }}
|
||||
expandedRows={expandedInv}
|
||||
onRowToggle={(e) => 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 (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
||||
padding: '0.4rem 0.75rem', background: 'var(--surface-100)',
|
||||
borderLeft: '4px solid var(--primary-color)'
|
||||
}}>
|
||||
<div>
|
||||
<strong style={{ color: 'var(--primary-color)', marginRight: '0.5rem' }}>{cat.code}</strong>
|
||||
<span>{cat.label}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '1.5rem', fontSize: '0.9em' }}>
|
||||
<div><small className="text-color-secondary">{__('Dichiarato:', 'gepafin')}</small> <strong>{euro(totalDecl)}</strong></div>
|
||||
<div><small className="text-color-secondary">{__('Ammesso:', 'gepafin')}</small> <strong style={{ color: 'var(--green-700)' }}>{euro(totalVerif)}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
rowExpansionTemplate={(inv) => {
|
||||
const draft = invDraft[inv.id] || {};
|
||||
return (
|
||||
<div style={{ padding: '0.75rem 1rem', background: 'var(--surface-50)' }}>
|
||||
<div className="appForm p-fluid" style={{ maxWidth: '800px' }}>
|
||||
<div className="appForm__field">
|
||||
<label>{__("Note dell'istruttore (motivazione rettifica o rigetto)", 'gepafin')}</label>
|
||||
<InputTextarea rows={3} autoResize
|
||||
disabled={!isVerifiable}
|
||||
value={draft.notes != null ? draft.notes : (inv.verification_notes || '')}
|
||||
onChange={(ev) => 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')} />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.75rem', marginTop: '0.5rem', fontSize: '0.9em' }}>
|
||||
<div><small className="text-color-secondary">{__('Dettaglio dichiarato:', 'gepafin')}</small></div>
|
||||
<div>{__('Imponibile:', 'gepafin')} <strong>{euro(inv.taxable)}</strong></div>
|
||||
<div>IVA: <strong>{euro(inv.vat)}</strong> — {__('Totale:', 'gepafin')} <strong>{euro(inv.total)}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Column expander style={{ width: '3.5rem', minWidth: '3.5rem' }} />
|
||||
<Column field="invoice_number" header={__('N°', 'gepafin')} style={{ width: '110px', minWidth: '110px' }} />
|
||||
<Column header={__('Data', 'gepafin')} style={{ width: '120px', minWidth: '120px' }}
|
||||
body={(r) => {
|
||||
const bad = r.date_checks && (r.date_checks.invoice_in_period === false || r.date_checks.payment_in_period === false);
|
||||
return <span style={bad ? { color: 'var(--red-600)', fontWeight: 600 } : {}}>
|
||||
{formatDate(r.invoice_date)}
|
||||
{bad && <i className="pi pi-exclamation-triangle" style={{ marginLeft: 4 }} title={__('Fuori periodo','gepafin')} />}
|
||||
</span>;
|
||||
}} />
|
||||
<Column field="supplier_name" header={__('Fornitore', 'gepafin')} style={{ width: '180px', minWidth: '180px' }} />
|
||||
<Column header={__('Descrizione', 'gepafin')}
|
||||
body={(r) => <span title={r.description}>{r.description.length > 50 ? r.description.slice(0, 50) + '…' : r.description}</span>} />
|
||||
<Column header={declaredLabel} style={{ width: '150px', minWidth: '150px', textAlign: 'right' }}
|
||||
body={(r) => <strong>{euro(useTaxable ? r.taxable : r.total)}</strong>} />
|
||||
<Column header={ammessoLabel} style={{ width: '170px', minWidth: '170px' }}
|
||||
body={(r) => {
|
||||
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 (
|
||||
<InputNumber
|
||||
value={displayVal}
|
||||
mode="currency" currency="EUR" locale="it-IT"
|
||||
disabled={!isVerifiable}
|
||||
inputStyle={{ textAlign: 'right', padding: '0.25rem 0.5rem' }}
|
||||
onValueChange={(ev) => 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}} />
|
||||
<Column header={__('Stato', 'gepafin')} style={{ width: '130px', minWidth: '130px' }}
|
||||
body={(r) => {
|
||||
const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
|
||||
return <Tag severity={cfg.severity} value={cfg.label} />;
|
||||
}} />
|
||||
<Column header={__('Azioni', 'gepafin')} style={{ width: '190px', minWidth: '190px' }}
|
||||
body={(r) => (
|
||||
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
||||
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
|
||||
onClick={() => openPreview(r.pdf_filename || `fattura_${r.invoice_number}.pdf`, `Fattura ${r.invoice_number}`)}
|
||||
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
||||
<Button icon="pi pi-download" rounded outlined size="small" severity="info"
|
||||
onClick={() => downloadStub(r.pdf_filename || `fattura_${r.invoice_number}.pdf`)}
|
||||
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
||||
<Button icon="pi pi-check" rounded outlined size="small"
|
||||
severity={r.verification_status === 'AMMESSA' ? 'success' : 'secondary'}
|
||||
disabled={!isVerifiable}
|
||||
onClick={() => {
|
||||
// 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' }} />
|
||||
<Button icon="pi pi-times" rounded outlined size="small"
|
||||
severity={r.verification_status === 'RESPINTA' ? 'danger' : 'secondary'}
|
||||
disabled={!isVerifiable}
|
||||
onClick={() => {
|
||||
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' }} />
|
||||
</div>
|
||||
)} />
|
||||
</DataTable>
|
||||
);
|
||||
})}
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* ULA DIPENDENTI */}
|
||||
{/* ULA DIPENDENTI — DataTable semplice con box header sopra */}
|
||||
{ulaSection.enabled && practice.ula_employees.length > 0 && (<>
|
||||
<div className="appPage__spacer"></div>
|
||||
<div className="appPageSection">
|
||||
<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')} />
|
||||
)}
|
||||
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
||||
{__("Modifica l'FTE ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", 'gepafin')}
|
||||
</p>
|
||||
|
||||
{(() => {
|
||||
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 (
|
||||
<div style={{ width: '100%' }}>
|
||||
{/* Header-box fuori dalla tabella, stesso stile del subheader delle fatture */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
||||
padding: '0.5rem 0.75rem', background: 'var(--surface-100)',
|
||||
borderLeft: '4px solid var(--primary-color)',
|
||||
borderTopLeftRadius: '4px', borderTopRightRadius: '4px',
|
||||
borderBottom: '1px solid var(--surface-border)'
|
||||
}}>
|
||||
<div>
|
||||
<strong style={{ color: 'var(--primary-color)', marginRight: '0.5rem' }}>ULA</strong>
|
||||
<span>{__('Incremento occupazione (soglia richiesta ≥', 'gepafin')} {Number(ulaSection.threshold || 1).toFixed(2)})</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<div style={{ display: 'flex', gap: '1.5rem', fontSize: '0.9em' }}>
|
||||
<div><small className="text-color-secondary">{__('FTE dichiarato:', 'gepafin')}</small> <strong>{totalFteDecl.toFixed(2)}</strong></div>
|
||||
<div><small className="text-color-secondary">{__('FTE ammesso:', 'gepafin')}</small> <strong style={{ color: thresholdOK ? 'var(--green-700)' : 'var(--red-600)' }}>{totalFteVerif.toFixed(2)}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
value={practice.ula_employees}
|
||||
dataKey="id"
|
||||
size="small"
|
||||
tableStyle={{ minWidth: '1100px', width: '100%' }}
|
||||
expandedRows={expandedUla}
|
||||
onRowToggle={(e) => setExpandedUla(e.data)}
|
||||
rowExpansionTemplate={(emp) => {
|
||||
const draft = ulaDraft[emp.id] || {};
|
||||
return (
|
||||
<div style={{ padding: '0.75rem 1rem', background: 'var(--surface-50)' }}>
|
||||
<div className="appForm p-fluid" style={{ maxWidth: '800px' }}>
|
||||
<div className="appForm__field">
|
||||
<label>{__("Note dell'istruttore (motivazione rettifica)", 'gepafin')}</label>
|
||||
<InputTextarea rows={3} autoResize
|
||||
disabled={!isVerifiable}
|
||||
value={draft.notes != null ? draft.notes : (emp.verification_notes || '')}
|
||||
onChange={(ev) => 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')} />
|
||||
</div>
|
||||
<div style={{ fontSize: '0.9em', marginTop: '0.5rem' }}>
|
||||
<small className="text-color-secondary">{__('Dettaglio:', 'gepafin')}</small> {CONTRACT_TYPES[emp.contract_type] || emp.contract_type}
|
||||
{emp.role_description && ` · ${emp.role_description}`}
|
||||
{' · '}{formatDate(emp.period_start_date)} → {formatDate(emp.period_end_date)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Column expander style={{ width: '3.5rem', minWidth: '3.5rem' }} />
|
||||
<Column field="codice_fiscale" header="CF" style={{ width: '170px', minWidth: '170px' }} />
|
||||
<Column field="full_name" header={__('Nome', 'gepafin')} style={{ width: '150px', minWidth: '150px' }} />
|
||||
<Column header={__('Contratto', 'gepafin')} style={{ width: '160px', minWidth: '160px' }}
|
||||
body={(r) => <small>{CONTRACT_TYPES[r.contract_type] || r.contract_type}</small>} />
|
||||
<Column header={__('Periodo', 'gepafin')}
|
||||
body={(r) => <small>{formatDate(r.period_start_date)} → {formatDate(r.period_end_date)}</small>} />
|
||||
<Column header={__('FTE dichiarato', 'gepafin')} style={{ width: '150px', minWidth: '150px', textAlign: 'right' }}
|
||||
body={(r) => <strong>{Number(r.fte_pct).toFixed(2)}</strong>} />
|
||||
<Column header={__('FTE ammesso', 'gepafin')} style={{ width: '170px', minWidth: '170px' }}
|
||||
body={(r) => {
|
||||
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 (
|
||||
<InputNumber
|
||||
value={displayVal}
|
||||
mode="decimal" minFractionDigits={2} maxFractionDigits={4} min={0} max={1}
|
||||
disabled={!isVerifiable}
|
||||
inputStyle={{ textAlign: 'right', padding: '0.25rem 0.5rem' }}
|
||||
onValueChange={(ev) => 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}} />
|
||||
<Column header={__('Stato', 'gepafin')} style={{ width: '130px', minWidth: '130px' }}
|
||||
body={(r) => {
|
||||
const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
|
||||
return <Tag severity={cfg.severity} value={cfg.label} />;
|
||||
}} />
|
||||
<Column header={__('Azioni', 'gepafin')} style={{ width: '190px', minWidth: '190px' }}
|
||||
body={(r) => (
|
||||
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
||||
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
|
||||
disabled={!r.supporting_doc_filename}
|
||||
onClick={() => openPreview(r.supporting_doc_filename, `${r.full_name}`)}
|
||||
tooltip={__('Anteprima allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
||||
<Button icon="pi pi-download" rounded outlined size="small" severity="info"
|
||||
disabled={!r.supporting_doc_filename}
|
||||
onClick={() => downloadStub(r.supporting_doc_filename)}
|
||||
tooltip={__('Scarica allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
||||
<Button icon="pi pi-check" rounded outlined size="small"
|
||||
severity={r.verification_status === 'AMMESSA' ? 'success' : 'secondary'}
|
||||
disabled={!isVerifiable}
|
||||
onClick={() => {
|
||||
const draft = ulaDraft[r.id];
|
||||
if (draft && draft.fte_pct_verified != null) {
|
||||
saveUlaInline(r);
|
||||
} else {
|
||||
saveUlaInline(r, 'AMMESSA');
|
||||
}
|
||||
}}
|
||||
tooltip={__('Conferma', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
||||
<Button icon="pi pi-times" rounded outlined size="small"
|
||||
severity={r.verification_status === 'RESPINTA' ? 'danger' : 'secondary'}
|
||||
disabled={!isVerifiable}
|
||||
onClick={() => saveUlaInline(r, 'RESPINTA')}
|
||||
tooltip={__('Respingi', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
||||
</div>
|
||||
)} />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
@@ -599,19 +875,43 @@ const IstruttoriaPratica = () => {
|
||||
</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,
|
||||
<div className="appPageSection__iconActions">
|
||||
<Button icon="pi pi-eye" rounded outlined severity="info"
|
||||
disabled={!doc.filename}
|
||||
onClick={() => openPreview(doc.filename, `${dr.label} — ${doc.filename || ''}`)}
|
||||
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
||||
aria-label={__('Anteprima', 'gepafin')} />
|
||||
<Button icon="pi pi-download" rounded outlined severity="info"
|
||||
disabled={!doc.filename}
|
||||
onClick={() => downloadStub(doc.filename)}
|
||||
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
||||
aria-label={__('Scarica', 'gepafin')} />
|
||||
<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')} />
|
||||
)}
|
||||
<Button icon="pi pi-thumbs-up" rounded outlined
|
||||
severity={doc.verification_status === 'VALIDO' ? 'success' : 'secondary'}
|
||||
disabled={!isVerifiable || !doc.filename}
|
||||
onClick={() => 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')} />
|
||||
<Button icon="pi pi-thumbs-down" rounded outlined
|
||||
severity={doc.verification_status === 'NON_VALIDO' ? 'danger' : 'secondary'}
|
||||
disabled={!isVerifiable || !doc.filename}
|
||||
onClick={() => {
|
||||
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')} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
@@ -681,92 +981,10 @@ const IstruttoriaPratica = () => {
|
||||
</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' }}
|
||||
|
||||
Reference in New Issue
Block a user