feat(rendicontazione): lato istruttore - queue + review + soccorso istruttorio

Backend (rendicontazione-api):
- 4 nuove colonne su remission_practice: assigned_instructor_id, reviewed_at,
  reviewed_by, rejection_reason, approved_remission
- Nuova tabella remission_amendment_request (id, practice_id, request_text,
  scope jsonb, deadline, status AWAITING/RESPONSE_RECEIVED/CLOSED/EXPIRED,
  response_text, audit cols)
- Router instructor.py con 8 endpoint:
  GET /instructor/queue (SUBMITTED pool + UNDER_REVIEW/AWAITING_AMENDMENT assigned,
  o tutto se manager/superadmin)
  GET /instructor/{id} (practice + gate_check + amendments)
  POST /instructor/{id}/claim (SUBMITTED -> UNDER_REVIEW)
  POST /instructor/{id}/approve (approved_remission opz, default = remission_due calcolato)
  POST /instructor/{id}/reject (rejection_reason min 10 char)
  POST /instructor/{id}/amendment (crea soccorso: request_text + deadline)
  POST /instructor/{id}/amendment/{aid}/close (chiude soccorso, pratica torna UNDER_REVIEW)
  POST /instructor/{id}/amendment/{aid}/respond-beneficiary (benef risponde)
- GET /{id} ora ritorna anche amendments (per beneficiario)

Frontend:
- Pagina IstruttoriaQueue (125 righe): coda pratiche con stato, istruttore
  assegnato, erogato, remission_due calcolata, azioni contestuali
- Pagina IstruttoriaPratica (483 righe): dettaglio pratica readonly per istruttore,
  riepilogo esteso, amendments panel con chiudi, gate check, fatture/ULA/docs,
  3 Dialog per approva/respingi/soccorso
- PraticaRendicontazioneEdit esteso con sezione 'Richieste di soccorso istruttorio'
  visibile al beneficiario + Dialog rispondi con request_text dell'istruttore
- Sidebar: voce 'Istruttoria rendicontazioni' per EVALUATE_APPLICATIONS
  (pre_instructor + instructor_manager)
- Routes /istruttoria e /istruttoria/:id con gate sui tre ruoli

Test end-to-end OK: benef crea+submit, istruttore claim+amendment, benef risponde,
istruttore chiude+approva -> APPROVED remission 8500 EUR su NAPOLI SAS (erogato 17000).

Utenti sandbox creati:
- istruttore@sandbox.local / istruttore123 (ROLE_PRE_INSTRUCTOR)
- manager@sandbox.local / manager123 (ROLE_INSTRUCTOR_MANAGER)
This commit is contained in:
BFLOWS Sandbox
2026-04-18 10:15:22 +02:00
parent 9c483ade34
commit 115f31bdef
6 changed files with 775 additions and 0 deletions

View File

@@ -0,0 +1,483 @@
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { __ } from '@wordpress/i18n';
import { useNavigate, useParams } from 'react-router-dom';
import { Button } from 'primereact/button';
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 { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
import RendicontazioneService from '../service/rendicontazioneService';
const CONTRACT_TYPES = {
T_IND: 'Tempo indeterminato', T_DET: 'Tempo determinato',
APPR: 'Apprendistato', STAGE: 'Tirocinio / Stage',
COLL: 'Collaborazione coordinata', ALTRO: 'Altro'
};
const PRACTICE_STATUS = {
DRAFT: { severity: 'warning', label: 'Bozza beneficiario' },
SUBMITTED: { severity: 'info', label: 'Inviata' },
UNDER_REVIEW: { severity: 'warning', label: 'In valutazione' },
AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso aperto' },
APPROVED: { severity: 'success', label: 'Approvata' },
REJECTED: { severity: 'danger', label: 'Respinta' }
};
const AMENDMENT_STATUS = {
AWAITING: { severity: 'warning', label: 'Attesa risposta' },
RESPONSE_RECEIVED: { severity: 'info', label: 'Risposta ricevuta' },
CLOSED: { severity: 'success', label: 'Chiusa' },
EXPIRED: { severity: 'danger', label: 'Scaduta' }
};
const euro = (v) => '€ ' + Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT') : '—';
const formatDateTime = (d) => d ? new Date(d).toLocaleString('it-IT') : '—';
const IstruttoriaPratica = () => {
const { id: practiceId } = useParams();
const navigate = useNavigate();
const toast = useRef(null);
const [loading, setLoading] = useState(true);
const [bundle, setBundle] = useState(null); // {practice, gate_check, amendments}
const [approveDialog, setApproveDialog] = useState({ visible: false, amount: null });
const [rejectDialog, setRejectDialog] = useState({ visible: false, reason: '' });
const [amendDialog, setAmendDialog] = useState({ visible: false, text: '', deadline: null });
const load = useCallback(() => {
setLoading(true);
RendicontazioneService.instructorViewPractice(practiceId,
(resp) => { setBundle(resp?.data); setLoading(false); },
(err) => {
toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail });
setLoading(false);
}
);
}, [practiceId]);
useEffect(() => { load(); }, [load]);
const practice = bundle?.practice;
const gate = bundle?.gate_check;
const amendments = bundle?.amendments || [];
const sections = practice?.schema_snapshot?.sections || [];
const categories = useMemo(() => {
const s = sections.find(x => x.type === 'category_grid') || {};
return s.categories || [];
}, [sections]);
const ulaSection = useMemo(() => sections.find(x => x.type === 'ula_block') || {}, [sections]);
const docsRequired = useMemo(() => {
const s = sections.find(x => x.type === 'document_checklist') || {};
const raw = s.required_types || [];
return raw.map(r => typeof r === 'string' ? { code: r, label: r } : r);
}, [sections]);
const openAmendments = amendments.filter(a => a.status === 'AWAITING' || a.status === 'RESPONSE_RECEIVED');
const isReviewable = practice && ['SUBMITTED', 'UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status);
const isDecidable = practice && ['UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status);
// ---------- actions ----------
const afterOk = (msg) => () => {
toast.current?.show({ severity: 'success', summary: msg });
load();
};
const onErr = (err) => {
toast.current?.show({ severity: 'error', summary: __('Operazione fallita', 'gepafin'),
detail: typeof err?.detail === 'object' ? JSON.stringify(err.detail) : err?.detail });
};
const handleClaim = (e) => {
confirmPopup({
target: e.currentTarget,
message: __('Prendere in carico la pratica? Lo stato passerà a In valutazione.', 'gepafin'),
icon: 'pi pi-info-circle',
acceptLabel: __('Prendi in carico', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'),
accept: () => RendicontazioneService.claimPractice(practiceId,
afterOk(__('Pratica presa in carico', 'gepafin')), onErr)
});
};
const doApprove = () => {
const body = approveDialog.amount != null ? { approved_remission: approveDialog.amount } : {};
RendicontazioneService.approvePractice(practiceId, body,
(resp) => { setApproveDialog({ visible: false, amount: null }); afterOk(__('Pratica approvata', 'gepafin'))(resp); },
onErr);
};
const doReject = () => {
if (!rejectDialog.reason || rejectDialog.reason.trim().length < 10) {
toast.current?.show({ severity: 'warn', summary: __('Motivazione troppo corta', 'gepafin'), detail: __('Minimo 10 caratteri', 'gepafin') });
return;
}
RendicontazioneService.rejectPractice(practiceId, rejectDialog.reason,
(resp) => { setRejectDialog({ visible: false, reason: '' }); afterOk(__('Pratica respinta', 'gepafin'))(resp); },
onErr);
};
const doAmend = () => {
if (!amendDialog.text || amendDialog.text.trim().length < 10) {
toast.current?.show({ severity: 'warn', summary: __('Testo troppo corto', 'gepafin') });
return;
}
if (!amendDialog.deadline) {
toast.current?.show({ severity: 'warn', summary: __('Deadline obbligatoria', 'gepafin') });
return;
}
const body = {
request_text: amendDialog.text,
deadline: typeof amendDialog.deadline === 'string' ? amendDialog.deadline : amendDialog.deadline.toISOString().slice(0, 10)
};
RendicontazioneService.createAmendment(practiceId, body,
(resp) => { setAmendDialog({ visible: false, text: '', deadline: null }); afterOk(__('Soccorso avviato', 'gepafin'))(resp); },
onErr);
};
const closeAmendment = (e, amendment) => {
confirmPopup({
target: e.currentTarget,
message: __('Chiudi questa richiesta di soccorso? La pratica torna in valutazione.', 'gepafin'),
icon: 'pi pi-info-circle',
acceptLabel: __('Chiudi', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'),
accept: () => RendicontazioneService.closeAmendment(practiceId, amendment.id,
afterOk(__('Soccorso chiuso', 'gepafin')), onErr)
});
};
// ---------- render ----------
if (loading) {
return <div className="appPage"><div className="appPageSection"><Skeleton width="100%" height="15rem" /></div></div>;
}
if (!practice) {
return <div className="appPage"><div className="appPageSection"><p>{__('Pratica non trovata', 'gepafin')}</p></div></div>;
}
const statusCfg = PRACTICE_STATUS[practice.status] || { severity: 'secondary', label: practice.status };
const totals = gate?.totals || {};
const perCat = totals.per_category || {};
const invoicesOfCat = (code) => practice.invoices.filter(i => i.category_code === code);
return (
<div className="appPage">
<Toast ref={toast} />
<ConfirmPopup />
<div className="appPage__pageHeader">
<h1>{__('Istruttoria pratica', 'gepafin')}</h1>
<p>
<span className="companyName">
{practice.schema_snapshot?.template_label || `Call #${practice.call_id}`} · {__('Pratica', 'gepafin')} #{practice.application_id}
</span>
<span style={{ marginLeft: '1rem' }}>
<Tag severity={statusCfg.severity} value={statusCfg.label} />
</span>
</p>
</div>
<div className="appPage__spacer"></div>
{/* ACTIONS */}
<div className="appPageSection">
<div className="appPageSection__actions">
<Button type="button" outlined icon="pi pi-arrow-left"
label={__('Torna alla coda', 'gepafin')} onClick={() => navigate('/istruttoria')} />
{practice.status === 'SUBMITTED' && (
<Button type="button" icon="pi pi-user-plus" iconPos="right"
label={__('Prendi in carico', 'gepafin')} onClick={handleClaim} />
)}
{isDecidable && (<>
<Button type="button" icon="pi pi-check" iconPos="right" severity="success"
label={__('Approva', 'gepafin')}
onClick={() => setApproveDialog({ visible: true, amount: totals.remission_due || 0 })} />
<Button type="button" icon="pi pi-times" iconPos="right" severity="danger" outlined
label={__('Respingi', 'gepafin')}
onClick={() => setRejectDialog({ visible: true, reason: '' })} />
<Button type="button" icon="pi pi-comment" iconPos="right" severity="warning" outlined
label={__('Soccorso istruttorio', 'gepafin')}
disabled={openAmendments.length > 0}
onClick={() => setAmendDialog({ visible: true, text: '', deadline: null })} />
</>)}
</div>
</div>
<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', 'gepafin')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 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>
<small className="text-color-secondary">{__('Erogato', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(practice.amount_erogato)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Regime IVA', 'gepafin')}</small>
<div style={{ fontSize: '1rem', fontWeight: 700 }}>{practice.iva_regime || '—'}</div>
</div>
<div>
<small className="text-color-secondary">{__('Totale fatture', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.grand_total || 0)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Cap remissione', 'gepafin')}</small>
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.max_remission || 0)}</div>
</div>
<div>
<small className="text-color-secondary">{__('Remissione calcolata', 'gepafin')}</small>
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--primary-color)' }}>{euro(totals.remission_due || 0)}</div>
</div>
{practice.approved_remission != null && (
<div>
<small className="text-color-secondary">{__('Remissione approvata', 'gepafin')}</small>
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--green-600)' }}>{euro(practice.approved_remission)}</div>
</div>
)}
</div>
{practice.rejection_reason && (
<div style={{ marginTop: '1rem', padding: '0.75rem', background: 'var(--red-50)', borderRadius: '4px', borderLeft: '4px solid var(--red-500)' }}>
<strong>{__('Motivo rifiuto:', 'gepafin')}</strong>
<div style={{ marginTop: '0.25rem' }}>{practice.rejection_reason}</div>
</div>
)}
</div>
<div className="appPage__spacer"></div>
{/* AMENDMENTS */}
{amendments.length > 0 && (<>
<div className="appPageSection">
<h2>{__('Soccorso istruttorio', 'gepafin')} <span style={{fontWeight:400, color:'var(--text-color-secondary)', fontSize:'0.9em'}}>({amendments.length})</span></h2>
<div className="fieldsRepeater">
{amendments.map(a => {
const cfg = AMENDMENT_STATUS[a.status] || { severity: 'secondary', label: a.status };
return (
<div key={a.id} className="fieldsRepeater__panel"
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem', background: 'var(--surface-50)' }}>
<div className="fieldsRepeater__heading" style={{ marginBottom: '0.5rem' }}>
<div>
<Tag severity={cfg.severity} value={cfg.label} />
<span style={{ marginLeft: '0.75rem', color: 'var(--text-color-secondary)' }}>
{__('Deadline:', 'gepafin')} {formatDate(a.deadline)} · {__('Creata:', 'gepafin')} {formatDateTime(a.created_at)}
</span>
</div>
{a.status !== 'CLOSED' && isReviewable && (
<Button icon="pi pi-check" label={__('Chiudi soccorso', 'gepafin')}
size="small" outlined severity="success"
onClick={(e) => closeAmendment(e, a)} />
)}
</div>
<div>
<small className="text-color-secondary">{__('Richiesta istruttore:', 'gepafin')}</small>
<div style={{ whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}>{a.request_text}</div>
{a.response_text && (<>
<small className="text-color-secondary">
{__('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>
</div>
);
})}
</div>
</div>
<div className="appPage__spacer"></div>
</>)}
{/* GATE CHECKS */}
{gate && (
<div className="appPageSection">
<h2>{__('Requisiti di validità', 'gepafin')}</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', width: '100%' }}>
{gate.checks.map((c, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<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' }} />
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600 }}>{c.label}</div>
<small className="text-color-secondary">{c.detail}</small>
</div>
</div>
))}
</div>
</div>
)}
<div className="appPage__spacer"></div>
{/* FATTURE PER CATEGORIA */}
<div className="appPageSection">
<h2>{__('Fatture rendicontate', 'gepafin')}</h2>
<div className="fieldsRepeater">
{categories.map(cat => {
const invs = invoicesOfCat(cat.code);
const total = perCat[cat.code] || 0;
return (
<div key={cat.code} className="fieldsRepeater__panel"
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem' }}>
<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>
{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 className="appPage__spacer"></div>
{/* DOCUMENTI */}
<div className="appPageSection">
<h2>{__('Documenti', 'gepafin')}</h2>
<div className="fieldsRepeater">
{docsRequired.map(dr => {
const existing = practice.documents.find(d => d.doc_code === dr.code);
return (
<div key={dr.code} className="fieldsRepeater__panel"
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '0.75rem 1rem',
display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<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>
{/* ---------- DIALOG APPROVA ---------- */}
<Dialog visible={approveDialog.visible} style={{ width: '480px' }}
header={__('Approva pratica', 'gepafin')} modal
onHide={() => setApproveDialog({ visible: false, amount: null })}>
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doApprove(); }}>
<div className="appForm__field">
<label>{__('Remissione da approvare (€)', 'gepafin')}</label>
<InputNumber value={approveDialog.amount} mode="currency" currency="EUR" locale="it-IT"
onValueChange={(e) => setApproveDialog(d => ({ ...d, amount: e.value }))} />
<small className="text-color-secondary">
{__('Valore calcolato:', 'gepafin')} {euro(totals.remission_due || 0)}. {__('Puoi modificarlo se necessario.', 'gepafin')}
</small>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setApproveDialog({ visible: false, amount: null })} />
<Button type="submit" label={__('Conferma approvazione', 'gepafin')} icon="pi pi-check" severity="success" />
</div>
</form>
</Dialog>
{/* ---------- DIALOG RESPINGI ---------- */}
<Dialog visible={rejectDialog.visible} style={{ width: '560px' }}
header={__('Respingi pratica', 'gepafin')} modal
onHide={() => setRejectDialog({ visible: false, reason: '' })}>
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doReject(); }}>
<div className="appForm__field">
<label>{__('Motivazione del rifiuto', 'gepafin')}</label>
<InputTextarea value={rejectDialog.reason} rows={4} autoResize
onChange={(e) => setRejectDialog(d => ({ ...d, reason: e.target.value }))} />
<small className="text-color-secondary">{__('Minimo 10 caratteri. Verrà inviata al beneficiario.', 'gepafin')}</small>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setRejectDialog({ visible: false, reason: '' })} />
<Button type="submit" label={__('Conferma rifiuto', 'gepafin')} icon="pi pi-times" severity="danger" />
</div>
</form>
</Dialog>
{/* ---------- DIALOG SOCCORSO ---------- */}
<Dialog visible={amendDialog.visible} style={{ width: '560px' }}
header={__('Avvia soccorso istruttorio', 'gepafin')} modal
onHide={() => setAmendDialog({ visible: false, text: '', deadline: null })}>
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doAmend(); }}>
<div className="appForm__field">
<label>{__('Richiesta al beneficiario', 'gepafin')}</label>
<InputTextarea value={amendDialog.text} rows={5} autoResize
onChange={(e) => setAmendDialog(d => ({ ...d, text: e.target.value }))}
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 className="appForm__field">
<label>{__('Scadenza risposta', 'gepafin')}</label>
<Calendar value={amendDialog.deadline} dateFormat="dd/mm/yy" showIcon
onChange={(e) => setAmendDialog(d => ({ ...d, deadline: e.value }))} />
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setAmendDialog({ visible: false, text: '', deadline: null })} />
<Button type="submit" label={__('Invia richiesta', 'gepafin')} icon="pi pi-send" severity="warning" />
</div>
</form>
</Dialog>
</div>
);
};
export default IstruttoriaPratica;