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