import React, { useEffect, useState, useRef, useMemo } from 'react'; import { __ } from '@wordpress/i18n'; import { useNavigate, useParams } from 'react-router-dom'; // store import { useStoreValue } from '../../../store'; // components import { Button } from 'primereact/button'; import { Toast } from 'primereact/toast'; import { Tag } from 'primereact/tag'; import { Skeleton } from 'primereact/skeleton'; import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup'; import { InputText } from 'primereact/inputtext'; import { InputNumber } from 'primereact/inputnumber'; import { InputSwitch } from 'primereact/inputswitch'; import { MultiSelect } from 'primereact/multiselect'; import { Dropdown } from 'primereact/dropdown'; import { Calendar } from 'primereact/calendar'; import { InputTextarea } from 'primereact/inputtextarea'; import BlockingOverlay from '../../../components/BlockingOverlay'; // api import RendicontazioneService from '../service/rendicontazioneService'; import SchemaTemplatePicker from '../components/SchemaTemplatePicker'; import BandoService from '../../../service/bando-service'; // ---------- costanti ---------- const IVA_REGIMES = [ { value: 'ORDINARIO', label: 'Ordinario' }, { value: 'FORFETTARIO', label: 'Forfettario' }, { 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' }, { value: 'custom', label: 'Data personalizzata' } ]; const ULA_DOC_TYPES = [ { value: 'LUL', label: 'Libro Unico del Lavoro (LUL)' }, { value: 'GESTIONALE_PAGHE', label: 'Estratto gestionale paghe' }, { value: 'DICHIARAZIONE_CDL', label: 'Dichiarazione Consulente del Lavoro' }, { value: 'ALTRO', label: 'Altro documento di supporto' } ]; // ---------- helpers JSON <-> form ---------- const schemaJsonToForm = (j) => { if (!j || !j.sections) return null; const general = j.sections.find(s => s.type === 'static_fields') || {}; const expenses = j.sections.find(s => s.type === 'category_grid') || {}; const ula = j.sections.find(s => s.type === 'ula_block') || {}; const docs = j.sections.find(s => s.type === 'document_checklist') || {}; const gate = j.gate_rules || {}; const ivaField = (general.fields || []).find(f => f.id === 'iva_regime'); const ivaAllowed = ivaField && ivaField.options ? ivaField.options.map(o => typeof o === 'string' ? o : o.value) : ['ORDINARIO','FORFETTARIO','ESENTE']; const parseList = (list) => (list || []).map(x => typeof x === 'string' ? { code: x, label: x } : { code: x.code || '', label: x.label || x.code || '' }); 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, 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, categories: (expenses.categories || []).map(c => ({ code: c.code || '', label: c.label || '', description: c.description || '', cap_amount: c.cap_amount ?? null })), ula_enabled: ula.enabled ?? false, ula_threshold: ula.threshold ?? 1.0, ula_period_start_rule: ula.period_start_rule ?? 'erogato_date', ula_period_end: ula.period_end ? new Date(ula.period_end) : null, ula_supporting_doc_required: ula.supporting_doc_required ?? true, ula_supporting_doc_types: (ula.supporting_doc_types || []).map(t => typeof t === 'string' ? t : t.code), docs_required: parseList(docs.required_types), cap_pct_erogato: gate.cap_pct_erogato != null ? Math.round(gate.cap_pct_erogato * 100) : 50, cap_absolute: gate.cap_absolute ?? 12500, require_invoice_per_category: gate.require_at_least_one_invoice_per_nonzero_category ?? true, require_ula_above_threshold: gate.require_ula_above_threshold ?? true, require_all_documents_resolved: gate.require_all_documents_resolved ?? true, // v2 multi-tranche + custom_checks max_tranches: gate.max_tranches ?? 1, custom_checks: (j.custom_checks || []).map(cc => ({ code: cc.code || '', label: cc.label || '', description: cc.description || '', requires_document: !!cc.requires_document, required: !!cc.required, })) }; }; const formToSchemaJson = (f, base = null) => { const orig = base || {}; const fmtDate = (d) => d ? (typeof d === 'string' ? d : d.toISOString().slice(0, 10)) : null; return { version: orig.version || '1.0', template_id: orig.template_id || 'CUSTOM', template_label: orig.template_label || 'Schema personalizzato', sections: [ { type: 'static_fields', id: 'general', label: 'Dati generali', fields: [{ id: 'iva_regime', type: 'select', label: 'Regime IVA', required: true, options: IVA_REGIMES.filter(o => f.iva_regimes_allowed.includes(o.value)) }] }, { type: 'category_grid', id: 'expenses', label: 'Spese ammissibili per categoria', categories: f.categories, invoice_schema: { required_fields: ['invoice_number','invoice_date','payment_date','supplier_name','supplier_vat','description','taxable','vat','total','pdf'] } }, { type: 'ula_block', id: 'ula', label: 'Calcolo ULA', enabled: f.ula_enabled, threshold: f.ula_threshold, period_start_rule: f.ula_period_start_rule, period_end: fmtDate(f.ula_period_end), supporting_doc_required: f.ula_supporting_doc_required, supporting_doc_types: ULA_DOC_TYPES .filter(t => f.ula_supporting_doc_types.includes(t.value)) .map(t => ({ code: t.value, label: t.label })) }, { type: 'document_checklist', id: 'docs', label: 'Documenti richiesti', required_types: f.docs_required } ], gate_rules: { amount_range: { min: f.amount_min, max: f.amount_max }, cap_pct_erogato: f.cap_pct_erogato / 100, 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), 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, max_tranches: f.max_tranches || 1 }, custom_checks: (f.custom_checks || []).map(cc => ({ code: cc.code, label: cc.label, description: cc.description, requires_document: !!cc.requires_document, required: !!cc.required, })), schema_version: 2 }; }; const BandoRendicontazioneSchemaEdit = () => { const { id } = useParams(); const navigate = useNavigate(); const isAsyncRequest = useStoreValue('isAsyncRequest'); const callId = parseInt(id); const [bando, setBando] = useState(null); const [bandoLoading, setBandoLoading] = useState(true); const [schemaRecord, setSchemaRecord] = useState(null); const [schemaLoading, setSchemaLoading] = useState(true); const [form, setForm] = useState(null); const [dirty, setDirty] = useState(false); const toast = useRef(null); // ---------- load ---------- const loadBando = () => { setBandoLoading(true); BandoService.getBando(callId, (r) => { setBando(r?.data || null); setBandoLoading(false); }, () => setBandoLoading(false)); }; const loadSchema = () => { setSchemaLoading(true); RendicontazioneService.getSchemaByCallId(callId, (resp) => { const rec = resp?.data || null; setSchemaRecord(rec); setForm(rec ? schemaJsonToForm(rec.schema_json) : null); setDirty(false); setSchemaLoading(false); }, (err) => { if (err?.status === 404) { setSchemaRecord(null); setForm(null); } else toast.current?.show({ severity: 'error', summary: __('Errore caricamento schema','gepafin'), detail: err?.detail }); setSchemaLoading(false); }); }; useEffect(() => { if (!isNaN(callId)) { loadBando(); loadSchema(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [callId]); // ---------- updates ---------- const update = (patch) => { setForm(p => ({ ...p, ...patch })); setDirty(true); }; const updateCategory = (idx, patch) => { setForm(p => ({ ...p, categories: p.categories.map((c,i) => i===idx ? {...c, ...patch} : c) })); setDirty(true); }; const addCategory = () => { setForm(p => ({ ...p, categories: [...p.categories, { code:'', label:'', description:'', cap_amount:null }] })); setDirty(true); }; const removeCategory = (idx) => { setForm(p => ({ ...p, categories: p.categories.filter((_,i) => i!==idx) })); setDirty(true); }; const updateDoc = (idx, patch) => { setForm(p => ({ ...p, docs_required: p.docs_required.map((d,i) => i===idx ? {...d,...patch} : d) })); setDirty(true); }; const addDoc = () => { setForm(p => ({ ...p, docs_required: [...p.docs_required, { code:'', label:'' }] })); setDirty(true); }; const removeDoc = (idx) => { setForm(p => ({ ...p, docs_required: p.docs_required.filter((_,i) => i!==idx) })); setDirty(true); }; // v2 custom_checks const updateCheck = (idx, patch) => { setForm(p => ({ ...p, custom_checks: p.custom_checks.map((c,i) => i===idx ? {...c, ...patch} : c) })); setDirty(true); }; const addCheck = () => { setForm(p => ({ ...p, custom_checks: [...(p.custom_checks || []), { code:'', label:'', description:'', requires_document:false, required:false }] })); setDirty(true); }; const removeCheck = (idx) => { setForm(p => ({ ...p, custom_checks: p.custom_checks.filter((_,i) => i!==idx) })); setDirty(true); }; // ---------- actions ---------- const handleInitializeRestart = (e) => { confirmPopup({ target: e.currentTarget, message: __('Inizializzo lo schema con il template RE-START? Sarà modificabile finché non verrà pubblicato.','gepafin'), icon: 'pi pi-info-circle', acceptLabel: __('Inizializza','gepafin'), rejectLabel: __('Annulla','gepafin'), accept: () => RendicontazioneService.initializeRestartTemplate(callId, () => { toast.current?.show({severity:'success', summary: __('Schema inizializzato','gepafin')}); loadSchema(); }, (err) => toast.current?.show({severity:'error', summary:__('Inizializzazione fallita','gepafin'), detail: err?.detail})) }); }; const handleSave = () => { const newJson = formToSchemaJson(form, schemaRecord?.schema_json); RendicontazioneService.updateSchema(callId, newJson, (resp) => { toast.current?.show({severity:'success', summary: __('Schema salvato','gepafin')}); setSchemaRecord(resp?.data); setForm(schemaJsonToForm(resp?.data?.schema_json)); setDirty(false); }, (err) => toast.current?.show({severity:'error', summary:__('Salvataggio fallito','gepafin'), detail: err?.detail})); }; const handlePublish = (e) => { confirmPopup({ target: e.currentTarget, message: __('Dopo la pubblicazione lo schema non sarà più modificabile e diventerà visibile ai beneficiari. Confermi?','gepafin'), icon: 'pi pi-exclamation-triangle', acceptLabel: __('Pubblica','gepafin'), rejectLabel: __('Annulla','gepafin'), acceptClassName: 'p-button-success', accept: () => RendicontazioneService.publishSchema(callId, (resp) => { toast.current?.show({severity:'success', summary:__('Schema pubblicato','gepafin')}); setSchemaRecord(resp?.data); }, (err) => toast.current?.show({severity:'error', summary:__('Pubblicazione fallita','gepafin'), detail: err?.detail})) }); }; // ---------- render ---------- const isPublished = schemaRecord?.status === 'PUBLISHED'; const readOnly = isPublished; const hasSchema = !!schemaRecord; const statusTag = useMemo(() => { if (!hasSchema) return ; if (isPublished) return ; return ; }, [hasSchema, isPublished]); return (
{/* HEADER — flex column, border-left */}

{__('Schema rendicontazione','gepafin')}

{bandoLoading ? : <> {(bando && bando.name) || `Bando #${callId}`} {statusTag} }

{/* ACTIONS — torna indietro + salva/pubblica */}
{/* CONTENT */} {schemaLoading && (
)} {!schemaLoading && !hasSchema && (
{ setSchemaRecord(data); setForm(schemaJsonToForm(data.schema_json)); setDirty(false); toast.current?.show({ severity: 'success', summary: __('Schema inizializzato', 'gepafin'), detail: __('Puoi ora configurare le sezioni e salvare come bozza.', 'gepafin') }); }} onError={(err) => toast.current?.show({ severity: 'error', summary: __('Inizializzazione fallita', 'gepafin'), detail: err?.detail || err?.message })} />
)} {!schemaLoading && hasSchema && form && (
e.preventDefault()}> {/* 1 - IMPORTI E PERIODO */}

{__('1. Importi ammissibili e periodo','gepafin')}

update({amount_min: e.value})} mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} />
update({amount_max: e.value})} mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} />
update({period_start_rule: e.value})} options={PERIOD_START_RULES} disabled={readOnly} />
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')}
update({period_end: e.value})} dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
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')}
{/* 2 - IVA */}

{__('2. Regime IVA','gepafin')}

update({iva_regimes_allowed: e.value})} disabled={readOnly} display="chip" placeholder={__('Seleziona regimi','gepafin')} />
update({iva_ordinario_imponibile_only: e.value})} disabled={readOnly} />
{__('Se attivo, in regime ordinario l\'IVA non viene considerata rendicontabile — vale solo la base imponibile della fattura.','gepafin')}
{/* 3 - CATEGORIE */}

{__('3. Categorie di spesa ammissibili','gepafin')} ({form.categories.length})

{form.categories.map((c, i) => (
{c.code || `#${i+1}`} — {c.label || __('(senza nome)','gepafin')} {!readOnly && (
updateCategory(i,{code:e.target.value})} placeholder="B1" disabled={readOnly} />
updateCategory(i,{cap_amount:e.value})} mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} placeholder="—" />
updateCategory(i,{label:e.target.value})} disabled={readOnly} />
updateCategory(i,{description:e.target.value})} rows={2} disabled={readOnly} autoResize />
))}
{!readOnly && (
)}
{/* 4 - ULA */}

{__('4. Calcolo ULA (incremento occupazione)','gepafin')}

update({ula_enabled: e.value})} disabled={readOnly} />
{form.ula_enabled && ( <>
update({ula_threshold: e.value})} mode="decimal" minFractionDigits={1} maxFractionDigits={2} min={0} disabled={readOnly} />
update({ula_period_end: e.value})} dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
update({ula_supporting_doc_required: e.value})} disabled={readOnly} />
{form.ula_supporting_doc_required && (
update({ula_supporting_doc_types: e.value})} disabled={readOnly} display="chip" placeholder={__('Seleziona tipi','gepafin')} />
)} )}
{/* 5 - DOCUMENTI */}

{__('5. Documenti richiesti','gepafin')} ({form.docs_required.length})

{__('I documenti già in regola nel repository della Company saranno riutilizzati automaticamente. Solo quelli scaduti o mancanti richiederanno caricamento.','gepafin')}

{form.docs_required.map((d, i) => (
updateDoc(i,{code:e.target.value})} placeholder="DURC" disabled={readOnly} />
updateDoc(i,{label:e.target.value})} disabled={readOnly} />
{!readOnly && (
)}
))}
{!readOnly && (
)}
{/* 6 - REGOLE */}

{__('6. Regole di validazione (gate pre-submit)','gepafin')}

update({cap_pct_erogato: e.value})} suffix=" %" min={0} max={100} disabled={readOnly} />
update({cap_absolute: e.value})} mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} />
update({require_invoice_per_category: e.value})} disabled={readOnly} />
update({require_ula_above_threshold: e.value})} disabled={readOnly} />
update({require_all_documents_resolved: e.value})} disabled={readOnly} />
{/* 7 - TRANCHES + CUSTOM CHECKS (v2) */}

{__('7. Tranches di rendicontazione','gepafin')}

{__('Numero massimo di tranche che il beneficiario puo aprire per questo bando. Il default 1 mantiene il comportamento classico a rendicontazione unica. Aumenta il numero per permettere rendicontazioni multi-fase (es. stati di avanzamento).','gepafin')}

update({max_tranches: e.value})} min={1} max={20} showButtons disabled={readOnly} />

{__('8. Controlli aggiuntivi (dichiarazioni beneficiario)','gepafin')} ({(form.custom_checks || []).length})

{__('Dichiarazioni aggiuntive richieste al beneficiario, oltre ai documenti standard. Ogni controllo puo richiedere o meno un documento allegato e puo essere obbligatorio o opzionale. Esempi: dichiarazione antiriciclaggio (senza doc, obbligatoria), polizza fidejussoria (con doc, opzionale).','gepafin')}

{(form.custom_checks || []).map((c, i) => (
{c.code || `check #${i+1}`} — {c.label || __('(senza etichetta)','gepafin')} {!readOnly && (
updateCheck(i,{code:e.target.value.toLowerCase().replace(/[^a-z0-9_]/g,'_')})} placeholder="antiriciclaggio" disabled={readOnly} />
updateCheck(i,{label:e.target.value})} placeholder={__('Dichiarazione antiriciclaggio','gepafin')} disabled={readOnly} />
updateCheck(i,{description:e.target.value})} rows={3} autoResize disabled={readOnly} placeholder={__('Dichiaro che il beneficiario rispetta...','gepafin')} />
updateCheck(i,{requires_document:e.value})} disabled={readOnly} />
{__("Se attivo, il beneficiario puo allegare un PDF (max 15MB).",'gepafin')}
updateCheck(i,{required:e.value})} disabled={readOnly} />
{__("Se attivo, il beneficiario deve dichiararlo prima di poter inviare la pratica.",'gepafin')}
))}
{!readOnly && (
)}
{/* ACTIONS BOTTOM (copia degli action top per comodità) */} {!isPublished && (
)}
)}
); }; export default BandoRendicontazioneSchemaEdit;