feat(amendment): ROUND 3.B UI benef completa — response upload + render HTML + dialog rich
Completamento lato beneficiario dopo R3.A (IstruttoriaPratica completo).
Modulo rendicontazione speculare al pattern piattaforma FE.
==5 PATCH APPLICATE==
1. submitAmendmentResponse — callback-chain con upload response_document
Se l'utente ha selezionato un file nel FileUpload, dopo il submit del
testo chiama uploadResponseDocument. Se l'upload fallisce, il testo
resta salvato (toast warn). Success unificato via afterMutation.
2. Sezione amendments benef — filtro DRAFT
Il benef non deve vedere le bozze: le DRAFT vivono solo lato istruttore
finche non viene chiamato /send. Doppio filtro (count + map).
3. Sezione amendments benef — render HTML + metadata
Il request_text ora viene da Editor lato istruttore (HTML), quindi
serve dangerouslySetInnerHTML. Mostra inoltre response_days, badge
'Allegato istruttore presente' se amendment_document_path, badge
'Allegato inviato con la risposta' se response_document_path.
4. Dialog risposta — Editor rich text
Sostituita InputTextarea con Editor (primereact) coerente con il
pattern del lato istruttore. height=180px.
5. Dialog risposta — FileUpload response_document + visualizzazione allegato istruttore
- Header del dialog mostra: richiesta HTML, badge 'Istruttore ha
allegato un documento' se presente, scadenza con icona calendario
e response_days in testo di aiuto.
- Nuovo campo FileUpload basic (PDF max 10MB) agganciato a
amendDialog.response_file.
- Width dialog aumentato da 560px a 720px (coerente con
IstruttoriaPratica dialog create/edit).
==VALIDAZIONE==
@babel/parser JSX: 31 nodes, no errori. File 69148 chars.
==STATO COMPLESSIVO SOCCORSO ISTRUTTORIO v3==
Backend (rendicontazione-api): COMPLETO — da13ca7 R1 + 34c4a47 R2
Frontend (bflows-bandi-fe): COMPLETO — 4982df4 R3.A + questo commit
Documento integrazione Cecilia: TODO (prossima sessione)
==NEXT==
- Test E2E UI sandbox (crea DRAFT con allegato istruttore -> modifica ->
invia -> simula mark-pec-sent via SQL -> benef vede soccorso con badge
allegato -> benef risponde con response_file -> istruttore vede
response con badge e chiude)
- Scrivere /opt/docs/gepafin-rendicontazione-amendment-spec-per-BE.md
per Cecilia Moretti con: spec endpoint /internal (pending-pec,
pending-reminder, mark-pec-sent, mark-pec-failed), poller cron BE,
tenant routing hub=1 PEC Massiva + ProtocolService 65.108.55.96:8080,
hub=2 Mailgun. 5 domande aperte (classifica, SviluppUmbria PEC,
allegati protocollati, ruoli autorizzati, firma digitale response).
This commit is contained in:
@@ -360,10 +360,33 @@ const PraticaRendicontazioneEdit = () => {
|
||||
toast.current?.show({ severity: 'warn', summary: __('Risposta troppo corta', 'gepafin') });
|
||||
return;
|
||||
}
|
||||
const fileToUpload = amendDialog.response_file;
|
||||
const amendmentId = amendDialog.amendment.id;
|
||||
|
||||
RendicontazioneService.respondAmendmentBeneficiary(
|
||||
practiceId, amendDialog.amendment.id, amendDialog.responseText,
|
||||
(resp) => { setAmendDialog({ visible: false, amendment: null, responseText: '' });
|
||||
afterMutation(__('Risposta inviata all\'istruttore', 'gepafin'))(resp); },
|
||||
practiceId, amendmentId, amendDialog.responseText,
|
||||
(resp) => {
|
||||
if (fileToUpload) {
|
||||
RendicontazioneService.uploadResponseDocument(practiceId, amendmentId, fileToUpload,
|
||||
() => {
|
||||
setAmendDialog({ visible: false, amendment: null, responseText: '', response_file: null });
|
||||
afterMutation(__('Risposta trasmessa con allegato', 'gepafin'))(resp);
|
||||
},
|
||||
(err) => {
|
||||
// testo salvato ma upload fallito — avviso e ricarico
|
||||
setAmendDialog({ visible: false, amendment: null, responseText: '', response_file: null });
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: __('Risposta salvata, upload allegato fallito', 'gepafin'),
|
||||
detail: err?.message || ''
|
||||
});
|
||||
afterMutation(null)(resp);
|
||||
});
|
||||
} else {
|
||||
setAmendDialog({ visible: false, amendment: null, responseText: '', response_file: null });
|
||||
afterMutation(__('Risposta inviata all\'istruttore', 'gepafin'))(resp);
|
||||
}
|
||||
},
|
||||
onMutationError);
|
||||
};
|
||||
|
||||
@@ -465,8 +488,8 @@ const PraticaRendicontazioneEdit = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SOCCORSO ISTRUTTORIO (se presente) */}
|
||||
{practice.amendments && practice.amendments.length > 0 && (<>
|
||||
{/* SOCCORSO ISTRUTTORIO (se presente — esclude DRAFT, visibile solo quando PEC partita) */}
|
||||
{practice.amendments && practice.amendments.filter(a => a.status !== 'DRAFT').length > 0 && (<>
|
||||
<div className="appPage__spacer"></div>
|
||||
<div className="appPageSection">
|
||||
<h2>{__('Richieste di soccorso istruttorio', 'gepafin')}</h2>
|
||||
@@ -474,7 +497,7 @@ const PraticaRendicontazioneEdit = () => {
|
||||
{__('L\'istruttore ha chiesto integrazioni o chiarimenti. Rispondi al più presto.', 'gepafin')}
|
||||
</p>
|
||||
<div className="fieldsRepeater">
|
||||
{practice.amendments.map(a => {
|
||||
{practice.amendments.filter(a => a.status !== 'DRAFT').map(a => {
|
||||
const statusCfg = {
|
||||
AWAITING: { sev: 'warning', label: 'In attesa della tua risposta' },
|
||||
RESPONSE_RECEIVED: { sev: 'info', label: 'Risposta inviata, in attesa di chiusura' },
|
||||
@@ -484,25 +507,41 @@ const PraticaRendicontazioneEdit = () => {
|
||||
return (
|
||||
<div key={a.id} className="fieldsRepeater__panel"
|
||||
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem',
|
||||
background: a.status === 'AWAITING' ? 'var(--orange-50)' : 'var(--surface-50)' }}>
|
||||
<div className="fieldsRepeater__heading" style={{ marginBottom: '0.5rem' }}>
|
||||
background: a.status === 'AWAITING' ? 'var(--orange-50, #fff7ed)' : 'var(--surface-50)',
|
||||
marginBottom: '0.75rem' }}>
|
||||
<div className="fieldsRepeater__heading" style={{ marginBottom: '0.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
<div>
|
||||
<Tag severity={statusCfg.sev} value={statusCfg.label} />
|
||||
<span style={{ marginLeft: '0.75rem', color: 'var(--text-color-secondary)' }}>
|
||||
{__('Scadenza:', 'gepafin')} {new Date(a.deadline).toLocaleDateString('it-IT')}
|
||||
<span style={{ marginLeft: '0.75rem', color: 'var(--text-color-secondary)', fontSize: '0.9em' }}>
|
||||
{__('Scadenza:', 'gepafin')} <strong>{new Date(a.deadline).toLocaleDateString('it-IT')}</strong>
|
||||
{a.response_days ? ` (${a.response_days}gg dalla richiesta)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{a.status === 'AWAITING' && (
|
||||
<Button icon="pi pi-reply" label={__('Rispondi', 'gepafin')} size="small" severity="warning"
|
||||
onClick={() => setAmendDialog({ visible: true, amendment: a, responseText: '' })} />
|
||||
onClick={() => setAmendDialog({ visible: true, amendment: a, responseText: '', response_file: null })} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<small className="text-color-secondary">{__('Richiesta istruttore:', 'gepafin')}</small>
|
||||
<div style={{ whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}>{a.request_text}</div>
|
||||
<div style={{ padding: '0.5rem', background: 'white', borderRadius: '4px', marginTop: '0.25rem', marginBottom: '0.5rem' }}
|
||||
dangerouslySetInnerHTML={{ __html: a.request_text || '' }} />
|
||||
{a.amendment_document_path && (
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<i className="pi pi-paperclip" style={{ marginRight: '0.25rem' }} />
|
||||
<small className="text-color-secondary">{__('Allegato istruttore presente (disponibile via PEC)', 'gepafin')}</small>
|
||||
</div>
|
||||
)}
|
||||
{a.response_text && (<>
|
||||
<small className="text-color-secondary">{__('Tua risposta:', 'gepafin')}</small>
|
||||
<div style={{ whiteSpace: 'pre-wrap', padding: '0.5rem', background: 'white', borderRadius: '4px', marginTop: '0.25rem' }}>{a.response_text}</div>
|
||||
<div style={{ padding: '0.5rem', background: 'white', borderRadius: '4px', marginTop: '0.25rem' }}
|
||||
dangerouslySetInnerHTML={{ __html: a.response_text }} />
|
||||
{a.response_document_path && (
|
||||
<div style={{ marginTop: '0.25rem' }}>
|
||||
<i className="pi pi-paperclip" style={{ marginRight: '0.25rem' }} />
|
||||
<small className="text-color-secondary">{__('Allegato inviato con la risposta', 'gepafin')}</small>
|
||||
</div>
|
||||
)}
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -911,23 +950,52 @@ const PraticaRendicontazioneEdit = () => {
|
||||
</Dialog>
|
||||
|
||||
{/* ---------- DIALOG RISPOSTA SOCCORSO ---------- */}
|
||||
<Dialog visible={amendDialog.visible} style={{ width: '560px' }}
|
||||
<Dialog visible={amendDialog.visible} style={{ width: '720px' }}
|
||||
header={__('Rispondi al soccorso istruttorio', 'gepafin')} modal
|
||||
onHide={() => setAmendDialog({ visible: false, amendment: null, responseText: '' })}>
|
||||
onHide={() => setAmendDialog({ visible: false, amendment: null, responseText: '', response_file: null })}>
|
||||
{amendDialog.amendment && (
|
||||
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); submitAmendmentResponse(); }}>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--surface-50)', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||
<small className="text-color-secondary">{__('Richiesta istruttore:', 'gepafin')}</small>
|
||||
<div style={{ whiteSpace: 'pre-wrap', marginTop: '0.25rem' }}>{amendDialog.amendment.request_text}</div>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--surface-50)', borderRadius: '4px', marginBottom: '1rem', border: '1px solid var(--surface-border)' }}>
|
||||
<small className="text-color-secondary">
|
||||
<strong>{__('Richiesta istruttore', 'gepafin')}</strong>
|
||||
{amendDialog.amendment.response_days ? ` — ${__('hai', 'gepafin')} ${amendDialog.amendment.response_days} ${__('giorni per rispondere', 'gepafin')}` : ''}
|
||||
</small>
|
||||
<div style={{ marginTop: '0.25rem' }}
|
||||
dangerouslySetInnerHTML={{ __html: amendDialog.amendment.request_text || '' }} />
|
||||
{amendDialog.amendment.amendment_document_path && (
|
||||
<div style={{ marginTop: '0.5rem', fontSize: '0.9em' }}>
|
||||
<i className="pi pi-paperclip" style={{ marginRight: '0.25rem' }} />
|
||||
<span className="text-color-secondary">{__('L\'istruttore ha allegato un documento (trasmesso via PEC)', 'gepafin')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '0.5rem', fontSize: '0.9em', color: 'var(--text-color-secondary)' }}>
|
||||
<i className="pi pi-calendar" style={{ marginRight: '0.25rem' }} />
|
||||
{__('Scadenza:', 'gepafin')} <strong>{new Date(amendDialog.amendment.deadline).toLocaleDateString('it-IT')}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="appForm__field">
|
||||
<label>{__('La tua risposta', 'gepafin')}</label>
|
||||
<InputTextarea value={amendDialog.responseText} rows={5} autoResize
|
||||
onChange={(e) => setAmendDialog(d => ({ ...d, responseText: e.target.value }))}
|
||||
placeholder={__('Descrivi le integrazioni fornite, allegati caricati, chiarimenti...', 'gepafin')} />
|
||||
<label>{__('La tua risposta', 'gepafin')} <span style={{color:'var(--red-500)'}}>*</span></label>
|
||||
<Editor value={amendDialog.responseText} style={{ height: '180px' }}
|
||||
onTextChange={(e) => setAmendDialog(d => ({ ...d, responseText: e.htmlValue || '' }))}
|
||||
placeholder={__('Descrivi le integrazioni fornite, gli allegati caricati, i chiarimenti...', 'gepafin')} />
|
||||
</div>
|
||||
|
||||
<div className="appForm__field">
|
||||
<label>{__('Allegato alla risposta (opzionale, PDF)', 'gepafin')}</label>
|
||||
<FileUpload ref={responseFileRef} mode="basic" auto={false}
|
||||
chooseLabel={__('Scegli file PDF', 'gepafin')}
|
||||
accept="application/pdf" maxFileSize={10*1024*1024}
|
||||
customUpload uploadHandler={() => {}}
|
||||
onSelect={(e) => setAmendDialog(d => ({ ...d, response_file: e.files[0] || null }))} />
|
||||
<small className="text-color-secondary">
|
||||
{__('Documento integrativo (DURC aggiornato, chiarimento contabile, ecc.)', 'gepafin')}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
||||
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setAmendDialog({ visible: false, amendment: null, responseText: '' })} />
|
||||
<Button type="button" outlined label={__('Annulla', 'gepafin')}
|
||||
onClick={() => setAmendDialog({ visible: false, amendment: null, responseText: '', response_file: null })} />
|
||||
<Button type="submit" label={__('Invia risposta', 'gepafin')} icon="pi pi-send" severity="warning" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user