Compare commits
19 Commits
1116f96acf
...
fba47c6e77
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba47c6e77 | ||
|
|
09da2b7c25 | ||
|
|
8f9e3d5622 | ||
|
|
3ae5aabe2d | ||
|
|
ec0e7397e6 | ||
|
|
ac1c18c737 | ||
|
|
cad839aea0 | ||
|
|
84ada138f2 | ||
|
|
21c58311e2 | ||
|
|
5bbf39488f | ||
|
|
c481871fa0 | ||
|
|
7ea5d7fd4c | ||
|
|
00ef1eb1e0 | ||
|
|
4a719ded5b | ||
|
|
2028239759 | ||
|
|
dbed5963b2 | ||
|
|
7c508e743b | ||
|
|
c407bd0b0e | ||
|
|
46ee801bd0 |
2
.env
2
.env
@@ -9,3 +9,5 @@ REACT_APP_FAVICON_FILENAME=gepafin-favicon.ico
|
||||
REACT_APP_HUB_ID=p4lk3bcx1RStqTaIVVbXs
|
||||
REACT_APP_EVALUATION_FLOW_ID=1
|
||||
REACT_APP_LOCAL_DEVELOPMENT=1
|
||||
REACT_APP_AR1_API_URL=http://78.46.41.91:18091
|
||||
REACT_APP_RENDICONTAZIONE_API_URL=http://78.46.41.91:18090
|
||||
|
||||
@@ -41,6 +41,13 @@ const AppSidebar = () => {
|
||||
id: 21,
|
||||
enable: intersection(permissions, ['MANAGE_TENDERS']).length
|
||||
},
|
||||
{
|
||||
label: __('Configurazione AR1', 'gepafin'),
|
||||
icon: 'pi pi-id-card',
|
||||
href: '/ar1-admin',
|
||||
id: 23,
|
||||
enable: intersection(permissions, ['MANAGE_TENDERS']).length
|
||||
},
|
||||
{
|
||||
label: __('Dev: cambia utente', 'gepafin'),
|
||||
icon: 'pi pi-user-edit',
|
||||
@@ -55,6 +62,13 @@ const AppSidebar = () => {
|
||||
id: 3,
|
||||
enable: intersection(permissions, ['APPLY_CALLS', 'APPLY_CONFIDI_CALLS']).length
|
||||
},
|
||||
{
|
||||
label: __('Dichiarazione AR1', 'gepafin'),
|
||||
icon: 'pi pi-id-card',
|
||||
href: '/ar1',
|
||||
id: 22,
|
||||
enable: intersection(permissions, ['APPLY_CALLS', 'APPLY_CONFIDI_CALLS']).length
|
||||
},
|
||||
{
|
||||
label: __('Bandi disponibili', 'gepafin'),
|
||||
icon: 'pi pi-bookmark',
|
||||
|
||||
137
src/modules/ar1/components/Ar1ComplianceModal.js
Normal file
137
src/modules/ar1/components/Ar1ComplianceModal.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Message } from 'primereact/message';
|
||||
import Ar1Service from '../service/ar1Service';
|
||||
import Ar1StatusTag from './Ar1StatusTag';
|
||||
|
||||
const DISMISS_SESSION_KEY_PREFIX = 'ar1-compliance-dismissed-';
|
||||
const DISMISS_WINDOW_HOURS = 24;
|
||||
|
||||
/**
|
||||
* Dialog AR1 mostrato al login se l'azienda ha AR1 MISSING/EXPIRED/APPROACHING.
|
||||
* - dismissable=false (EXPIRED/MISSING): bloccante, solo CTA "Compila ora"
|
||||
* - dismissable=true (APPROACHING): X chiude + salva in sessionStorage 24h
|
||||
*
|
||||
* Da montare nel layout principale. Esempio:
|
||||
* <Ar1ComplianceModal companyId={userCompanyId} />
|
||||
*/
|
||||
const Ar1ComplianceModal = ({ companyId }) => {
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!companyId) return;
|
||||
const dismissKey = DISMISS_SESSION_KEY_PREFIX + companyId;
|
||||
const dismissed = sessionStorage.getItem(dismissKey);
|
||||
if (dismissed) {
|
||||
const dismissedAt = parseInt(dismissed, 10);
|
||||
if (Date.now() - dismissedAt < DISMISS_WINDOW_HOURS * 3600 * 1000) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Ar1Service.getStatusForCompany(companyId,
|
||||
(resp) => {
|
||||
setLoading(false);
|
||||
const showFor = ['MISSING', 'EXPIRED', 'APPROACHING'];
|
||||
if (resp && showFor.includes(resp.status)) {
|
||||
setStatus(resp);
|
||||
setVisible(true);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
console.warn('Ar1ComplianceModal: status check failed', err);
|
||||
}
|
||||
);
|
||||
}, [companyId]);
|
||||
|
||||
const handleDismiss = () => {
|
||||
if (!status?.is_popup_dismissible) return;
|
||||
sessionStorage.setItem(DISMISS_SESSION_KEY_PREFIX + companyId, Date.now().toString());
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const goToCompile = () => {
|
||||
setVisible(false);
|
||||
navigate('/ar1');
|
||||
};
|
||||
|
||||
if (loading || !status) return null;
|
||||
|
||||
const canDismiss = status.is_popup_dismissible;
|
||||
const isUrgent = status.status === 'EXPIRED' || status.status === 'MISSING';
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={<div><i className="pi pi-id-card" style={{ marginRight: 8 }} />{__('Dichiarazione AR1 — Adeguata Verifica', 'gepafin')}</div>}
|
||||
visible={visible}
|
||||
modal
|
||||
closable={canDismiss}
|
||||
closeOnEscape={canDismiss}
|
||||
dismissableMask={canDismiss}
|
||||
onHide={handleDismiss}
|
||||
style={{ width: '560px', maxWidth: '95vw' }}
|
||||
>
|
||||
<div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Ar1StatusTag status={status.status} />
|
||||
</div>
|
||||
|
||||
{isUrgent && (
|
||||
<Message
|
||||
severity="error"
|
||||
text={status.must_recompile_reason ||
|
||||
__('Per proseguire nell\'operativita con Gepafin e necessario aggiornare la dichiarazione di adeguata verifica (D.Lgs. 231/2007).', 'gepafin')}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isUrgent && (
|
||||
<Message
|
||||
severity="warn"
|
||||
text={__('La tua dichiarazione AR1 sta per scadere. Ti chiediamo di aggiornarla per tempo.', 'gepafin')}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p style={{ margin: '10px 0', color: '#444' }}>
|
||||
{__('Il modulo AR1 (Aggiornamento Adeguata Verifica) e richiesto dalla normativa antiriciclaggio D.Lgs. 231/2007. La compilazione si svolge via wizard guidato e termina con la firma digitale (FEQ) del modulo.', 'gepafin')}
|
||||
</p>
|
||||
|
||||
{status.days_to_expiry !== null && status.days_to_expiry !== undefined && (
|
||||
<p style={{ margin: '10px 0', fontWeight: 600 }}>
|
||||
{status.days_to_expiry < 0
|
||||
? __(`Scaduta da ${Math.abs(status.days_to_expiry)} giorni`, 'gepafin')
|
||||
: __(`Scadenza tra ${status.days_to_expiry} giorni`, 'gepafin')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
|
||||
{canDismiss && (
|
||||
<Button
|
||||
label={__('Ricordamelo piu tardi', 'gepafin')}
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={handleDismiss}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
label={status.status === 'DRAFT' ? __('Riprendi compilazione', 'gepafin') : __('Compila ora', 'gepafin')}
|
||||
icon="pi pi-arrow-right"
|
||||
iconPos="right"
|
||||
onClick={goToCompile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ar1ComplianceModal;
|
||||
26
src/modules/ar1/components/Ar1StatusTag.js
Normal file
26
src/modules/ar1/components/Ar1StatusTag.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Tag } from 'primereact/tag';
|
||||
|
||||
/**
|
||||
* Badge per lo status AR1. Stati possibili:
|
||||
* MISSING, DRAFT, AWAITING_SIGNATURE, SIGNED, VERIFIED, VALID, APPROACHING, EXPIRED, SUPERSEDED
|
||||
*/
|
||||
const STATUS_CONFIG = {
|
||||
MISSING: { severity: 'danger', label: 'Da compilare', icon: 'pi pi-exclamation-circle' },
|
||||
DRAFT: { severity: 'warning', label: 'Bozza in corso', icon: 'pi pi-pencil' },
|
||||
AWAITING_SIGNATURE: { severity: 'info', label: 'Attesa firma', icon: 'pi pi-hourglass' },
|
||||
SIGNED: { severity: 'info', label: 'Firmato', icon: 'pi pi-verified' },
|
||||
VERIFIED: { severity: 'success', label: 'Verificato', icon: 'pi pi-check-circle' },
|
||||
VALID: { severity: 'success', label: 'Valido', icon: 'pi pi-check-circle' },
|
||||
APPROACHING: { severity: 'warning', label: 'In scadenza', icon: 'pi pi-clock' },
|
||||
EXPIRED: { severity: 'danger', label: 'Scaduto', icon: 'pi pi-times-circle' },
|
||||
SUPERSEDED: { severity: 'secondary', label: 'Sostituito', icon: 'pi pi-history' },
|
||||
};
|
||||
|
||||
const Ar1StatusTag = ({ status }) => {
|
||||
const cfg = STATUS_CONFIG[status] || { severity: 'secondary', label: status || '—', icon: 'pi pi-circle' };
|
||||
return <Tag severity={cfg.severity} icon={cfg.icon} value={__(cfg.label, 'gepafin')} />;
|
||||
};
|
||||
|
||||
export default Ar1StatusTag;
|
||||
442
src/modules/ar1/components/QuadriStructureEditor.js
Normal file
442
src/modules/ar1/components/QuadriStructureEditor.js
Normal file
@@ -0,0 +1,442 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// prime
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { Message } from 'primereact/message';
|
||||
import { Accordion, AccordionTab } from 'primereact/accordion';
|
||||
import { confirmDialog } from 'primereact/confirmdialog';
|
||||
|
||||
const FIELD_TYPE_OPTIONS = [
|
||||
{ label: 'Testo libero', value: 'text' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Data', value: 'date' },
|
||||
{ label: 'Checkbox (si/no)', value: 'checkbox' },
|
||||
{ label: 'Scelta singola (radio)', value: 'radio' },
|
||||
{ label: 'Menu a tendina (enum)', value: 'enum' },
|
||||
{ label: 'Si/No con nota', value: 'yes_no_with_note' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Editor della struttura questions_snapshot di un template AR1.
|
||||
*
|
||||
* Props:
|
||||
* value: { quadri: [...], variant, legal_ref, ... }
|
||||
* onChange: (newValue) => void
|
||||
*
|
||||
* Permette:
|
||||
* - Modificare metadati snapshot (variant_label, variant_description, legal_ref, normative_frame)
|
||||
* - Modificare per ogni quadro: id, title, description
|
||||
* - Aggiungere/rimuovere/modificare fields con tipo, label, required, pattern, max_length, options, prefill_from
|
||||
* - Per Quadro G (is_legal_frame=true): warning visuale + editor del description
|
||||
* - Per Quadri B (row_type=titolare): edit row_fields separato
|
||||
* - Per Quadri C/D (nested_full): edit fields nested
|
||||
* - Aggiungere/rimuovere quadri interi
|
||||
*/
|
||||
const QuadriStructureEditor = ({ value, onChange }) => {
|
||||
const qs = value || { quadri: [] };
|
||||
const quadri = qs.quadri || [];
|
||||
|
||||
const update = (newQs) => onChange(newQs);
|
||||
|
||||
// --- metadati snapshot ---
|
||||
const updateMeta = (key, v) => update({ ...qs, [key]: v });
|
||||
|
||||
// --- quadri ---
|
||||
const updateQuadro = (idx, partial) => {
|
||||
const newQuadri = quadri.map((q, i) => i === idx ? { ...q, ...partial } : q);
|
||||
update({ ...qs, quadri: newQuadri });
|
||||
};
|
||||
|
||||
const moveQuadro = (idx, direction) => {
|
||||
const newIdx = idx + direction;
|
||||
if (newIdx < 0 || newIdx >= quadri.length) return;
|
||||
const newQuadri = [...quadri];
|
||||
[newQuadri[idx], newQuadri[newIdx]] = [newQuadri[newIdx], newQuadri[idx]];
|
||||
update({ ...qs, quadri: newQuadri });
|
||||
};
|
||||
|
||||
const removeQuadro = (idx) => {
|
||||
const q = quadri[idx];
|
||||
confirmDialog({
|
||||
message: `Eliminare il quadro "${q.id} - ${q.title}"? Sara creata una nuova versione.`,
|
||||
header: 'Conferma eliminazione quadro',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'Elimina',
|
||||
rejectLabel: 'Annulla',
|
||||
acceptClassName: 'p-button-danger',
|
||||
accept: () => {
|
||||
const newQuadri = quadri.filter((_, i) => i !== idx);
|
||||
update({ ...qs, quadri: newQuadri });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addQuadro = () => {
|
||||
const nextLetter = String.fromCharCode(65 + quadri.length); // A, B, C...
|
||||
const newQuadro = {
|
||||
id: nextLetter,
|
||||
title: `Quadro ${nextLetter} - Nuovo quadro`,
|
||||
description: '',
|
||||
fields: []
|
||||
};
|
||||
update({ ...qs, quadri: [...quadri, newQuadro] });
|
||||
};
|
||||
|
||||
// --- fields dentro quadro ---
|
||||
const updateField = (qIdx, fIdx, partial) => {
|
||||
const q = quadri[qIdx];
|
||||
const newFields = (q.fields || []).map((f, i) => i === fIdx ? { ...f, ...partial } : f);
|
||||
updateQuadro(qIdx, { fields: newFields });
|
||||
};
|
||||
|
||||
const addField = (qIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const fields = q.fields || [];
|
||||
// id basato su letter quadro + numero crescente per evitare collisioni
|
||||
const newField = {
|
||||
id: `campo_${q.id || 'X'}_${fields.length + 1}`.toLowerCase(),
|
||||
type: 'text',
|
||||
label: 'Nuovo campo',
|
||||
required: false
|
||||
};
|
||||
updateQuadro(qIdx, { fields: [...fields, newField] });
|
||||
};
|
||||
|
||||
const removeField = (qIdx, fIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const newFields = (q.fields || []).filter((_, i) => i !== fIdx);
|
||||
updateQuadro(qIdx, { fields: newFields });
|
||||
};
|
||||
|
||||
const moveField = (qIdx, fIdx, direction) => {
|
||||
const q = quadri[qIdx];
|
||||
const fields = q.fields || [];
|
||||
const newIdx = fIdx + direction;
|
||||
if (newIdx < 0 || newIdx >= fields.length) return;
|
||||
const newFields = [...fields];
|
||||
[newFields[fIdx], newFields[newIdx]] = [newFields[newIdx], newFields[fIdx]];
|
||||
updateQuadro(qIdx, { fields: newFields });
|
||||
};
|
||||
|
||||
// --- row_fields (es. Quadro B titolari) ---
|
||||
const updateRowField = (qIdx, fIdx, partial) => {
|
||||
const q = quadri[qIdx];
|
||||
const newRowFields = (q.row_fields || []).map((f, i) => i === fIdx ? { ...f, ...partial } : f);
|
||||
updateQuadro(qIdx, { row_fields: newRowFields });
|
||||
};
|
||||
|
||||
const addRowField = (qIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const rowFields = q.row_fields || [];
|
||||
updateQuadro(qIdx, {
|
||||
row_fields: [...rowFields, { id: `campo_${rowFields.length + 1}`, type: 'text', label: 'Nuovo campo', required: false }]
|
||||
});
|
||||
};
|
||||
|
||||
const removeRowField = (qIdx, fIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
updateQuadro(qIdx, { row_fields: (q.row_fields || []).filter((_, i) => i !== fIdx) });
|
||||
};
|
||||
|
||||
// --- nested fields (es. Quadro C/D) ---
|
||||
const updateNestedField = (qIdx, fIdx, partial) => {
|
||||
const q = quadri[qIdx];
|
||||
const nested = q.nested_full || {};
|
||||
const newNestedFields = (nested.fields || []).map((f, i) => i === fIdx ? { ...f, ...partial } : f);
|
||||
updateQuadro(qIdx, { nested_full: { ...nested, fields: newNestedFields } });
|
||||
};
|
||||
|
||||
const addNestedField = (qIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const nested = q.nested_full || { fields: [] };
|
||||
const fields = nested.fields || [];
|
||||
updateQuadro(qIdx, {
|
||||
nested_full: {
|
||||
...nested,
|
||||
fields: [...fields, { id: `campo_${fields.length + 1}`, type: 'text', label: 'Nuovo campo', required: false }]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeNestedField = (qIdx, fIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const nested = q.nested_full || {};
|
||||
updateQuadro(qIdx, {
|
||||
nested_full: { ...nested, fields: (nested.fields || []).filter((_, i) => i !== fIdx) }
|
||||
});
|
||||
};
|
||||
|
||||
// ----- render field editor (atom) -----
|
||||
const renderFieldRow = (field, idx, onUpdate, onRemove, onMoveUp, onMoveDown, total) => {
|
||||
const isEnum = field.type === 'enum' || field.type === 'radio';
|
||||
return (
|
||||
<div key={idx} style={{
|
||||
border: '1px solid #ddd', borderRadius: 4, padding: 10,
|
||||
marginBottom: 8, background: '#fafafa'
|
||||
}}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr 160px auto', gap: 8, alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>ID (kebab, no spazi)</label>
|
||||
<InputText value={field.id || ''}
|
||||
onChange={(e) => onUpdate({ id: e.target.value.replace(/\s+/g, '_') })}
|
||||
style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Label visualizzato</label>
|
||||
<InputText value={field.label || ''}
|
||||
onChange={(e) => onUpdate({ label: e.target.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Tipo</label>
|
||||
<Dropdown value={field.type || 'text'} options={FIELD_TYPE_OPTIONS}
|
||||
onChange={(e) => onUpdate({ type: e.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-arrow-up" rounded outlined size="small"
|
||||
disabled={idx === 0} onClick={() => onMoveUp()} tooltip="Sposta su" />
|
||||
<Button icon="pi pi-arrow-down" rounded outlined size="small"
|
||||
disabled={idx === total - 1} onClick={() => onMoveDown && onMoveDown()} tooltip="Sposta giu" />
|
||||
<Button icon="pi pi-trash" rounded outlined size="small" severity="danger"
|
||||
onClick={() => onRemove()} tooltip="Elimina campo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Checkbox inputId={`req_${idx}_${field.id}`} checked={!!field.required}
|
||||
onChange={(e) => onUpdate({ required: e.checked })} />
|
||||
<label htmlFor={`req_${idx}_${field.id}`} style={{ fontSize: 12 }}>Obbligatorio</label>
|
||||
</div>
|
||||
|
||||
{field.type === 'text' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Max lunghezza:</label>
|
||||
<InputNumber value={field.max_length || null}
|
||||
onValueChange={(e) => onUpdate({ max_length: e.value })}
|
||||
style={{ width: 90 }} inputStyle={{ fontSize: 12 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Pattern regex:</label>
|
||||
<InputText value={field.pattern || ''}
|
||||
onChange={(e) => onUpdate({ pattern: e.target.value || undefined })}
|
||||
style={{ width: 180, fontFamily: 'monospace', fontSize: 11 }}
|
||||
placeholder="es. ^[0-9]{11}$" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{field.prefill_from && (
|
||||
<Tag severity="info" value={`prefill: ${field.prefill_from}`} style={{ fontSize: 11 }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEnum && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label style={{ fontSize: 11, color: '#555', display: 'block', marginBottom: 2 }}>
|
||||
Opzioni (una per riga)
|
||||
</label>
|
||||
<InputTextarea rows={3}
|
||||
value={(field.options || []).map(o => typeof o === 'string' ? o : (o.label || o.value || '')).join('\n')}
|
||||
onChange={(e) => {
|
||||
const opts = e.target.value.split('\n').filter(Boolean);
|
||||
onUpdate({ options: opts });
|
||||
}}
|
||||
style={{ width: '100%', fontSize: 12 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ----- render -----
|
||||
return (
|
||||
<div>
|
||||
<Message severity="warn" style={{ marginBottom: 14 }}
|
||||
text="Attenzione: modifiche alla struttura dei quadri generano automaticamente una nuova versione del template. I form gia compilati continuano a usare la versione precedente grazie allo snapshot." />
|
||||
|
||||
{/* Metadati */}
|
||||
<Card title="Metadati template" style={{ marginBottom: 14 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Etichetta variante</label>
|
||||
<InputText value={qs.variant_label || ''}
|
||||
onChange={(e) => updateMeta('variant_label', e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Riferimento normativo</label>
|
||||
<InputText value={qs.legal_ref || ''}
|
||||
onChange={(e) => updateMeta('legal_ref', e.target.value)} style={{ width: '100%' }}
|
||||
placeholder="es. D.Lgs. 231/2007" />
|
||||
</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Descrizione variante</label>
|
||||
<InputTextarea rows={2} value={qs.variant_description || ''}
|
||||
onChange={(e) => updateMeta('variant_description', e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Cornice normativa (testo completo)</label>
|
||||
<InputTextarea rows={3} value={qs.normative_frame || ''}
|
||||
onChange={(e) => updateMeta('normative_frame', e.target.value)}
|
||||
style={{ width: '100%', fontSize: 12 }} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quadri */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<h3 style={{ margin: 0 }}>Quadri ({quadri.length})</h3>
|
||||
<Button label="Aggiungi quadro" icon="pi pi-plus" size="small" outlined onClick={addQuadro} />
|
||||
</div>
|
||||
|
||||
<Accordion multiple>
|
||||
{quadri.map((q, qIdx) => (
|
||||
<AccordionTab key={qIdx}
|
||||
header={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
|
||||
<Tag value={q.id || '?'} severity={q.is_legal_frame ? 'warning' : 'info'} />
|
||||
<strong style={{ flex: 1 }}>{q.title || '(senza titolo)'}</strong>
|
||||
{q.is_legal_frame && <Tag value="NORMATIVO" severity="warning" />}
|
||||
{q.row_type && <Tag value={`ripetuto (${q.row_type})`} severity="secondary" />}
|
||||
{q.nested_full && <Tag value="annidato" severity="secondary" />}
|
||||
<small style={{ color: '#888' }}>
|
||||
{(q.fields || []).length} campi
|
||||
{q.row_fields ? ` +${q.row_fields.length} riga` : ''}
|
||||
{q.nested_full?.fields ? ` +${q.nested_full.fields.length} nidif.` : ''}
|
||||
</small>
|
||||
</div>
|
||||
}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '140px 1fr 1fr auto', gap: 8, alignItems: 'end', marginBottom: 10 }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>ID quadro</label>
|
||||
<InputText value={q.id || ''}
|
||||
onChange={(e) => updateQuadro(qIdx, { id: e.target.value.toUpperCase() })}
|
||||
style={{ width: '100%', fontFamily: 'monospace' }} maxLength={4} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Titolo</label>
|
||||
<InputText value={q.title || ''}
|
||||
onChange={(e) => updateQuadro(qIdx, { title: e.target.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-arrow-up" rounded outlined size="small"
|
||||
disabled={qIdx === 0} onClick={() => moveQuadro(qIdx, -1)} tooltip="Su" />
|
||||
<Button icon="pi pi-arrow-down" rounded outlined size="small"
|
||||
disabled={qIdx === quadri.length - 1} onClick={() => moveQuadro(qIdx, 1)} tooltip="Giu" />
|
||||
<Button icon="pi pi-trash" rounded outlined size="small" severity="danger"
|
||||
onClick={() => removeQuadro(qIdx)} tooltip="Elimina quadro" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Descrizione</label>
|
||||
<InputTextarea rows={2} value={q.description || ''}
|
||||
onChange={(e) => updateQuadro(qIdx, { description: e.target.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
|
||||
{q.is_legal_frame && (
|
||||
<Message severity="error" style={{ marginBottom: 10 }}
|
||||
text="Quadro normativo (D.Lgs. 231/2007). Il testo dichiara responsabilita legali del firmatario. Modifica con massima cautela: il contenuto viene inserito nel PDF firmato dal beneficiario." />
|
||||
)}
|
||||
|
||||
{/* Fields normali — sempre mostrato, anche se q.fields non esiste (Quadri B/F/G) */}
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<strong style={{ fontSize: 13 }}>Campi ({(q.fields || []).length})</strong>
|
||||
<Button label="Aggiungi campo" icon="pi pi-plus" size="small" outlined
|
||||
onClick={() => addField(qIdx)} />
|
||||
</div>
|
||||
{(q.fields || []).map((f, fIdx) =>
|
||||
renderFieldRow(f, fIdx,
|
||||
(p) => updateField(qIdx, fIdx, p),
|
||||
() => removeField(qIdx, fIdx),
|
||||
() => moveField(qIdx, fIdx, -1),
|
||||
() => moveField(qIdx, fIdx, 1),
|
||||
(q.fields || []).length
|
||||
)
|
||||
)}
|
||||
{(!q.fields || q.fields.length === 0) && (
|
||||
<p style={{ color: '#888', fontStyle: 'italic', textAlign: 'center', fontSize: 12 }}>
|
||||
Nessun campo diretto. Aggiungine uno col bottone qui sopra
|
||||
{q.row_type ? ` (o usa i "Campi riga" qui sotto per i ${q.row_type})` : ''}
|
||||
{q.nested_full ? ' (o usa i "Campi annidati" qui sotto)' : ''}
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
{/* Row fields (Quadro B titolari) */}
|
||||
{q.row_type && (
|
||||
<>
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<strong style={{ fontSize: 13 }}>Campi riga (per ogni {q.row_type}): ({(q.row_fields || []).length})</strong>
|
||||
<Button label="Aggiungi campo riga" icon="pi pi-plus" size="small" outlined
|
||||
onClick={() => addRowField(qIdx)} />
|
||||
</div>
|
||||
<Message severity="info" style={{ marginBottom: 8, fontSize: 12 }}
|
||||
text={`Questi campi si ripetono per ogni ${q.row_type} aggiunto dal beneficiario. Non eliminare il row_type se ci sono form gia compilati.`} />
|
||||
{(q.row_fields || []).map((f, fIdx) =>
|
||||
renderFieldRow(f, fIdx,
|
||||
(p) => updateRowField(qIdx, fIdx, p),
|
||||
() => removeRowField(qIdx, fIdx),
|
||||
() => {}, () => {},
|
||||
(q.row_fields || []).length
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Nested full (Quadro C/D rappresentante/esecutore) */}
|
||||
{q.nested_full && (
|
||||
<>
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<strong style={{ fontSize: 13 }}>Campi annidati: ({(q.nested_full.fields || []).length})</strong>
|
||||
<Button label="Aggiungi campo annidato" icon="pi pi-plus" size="small" outlined
|
||||
onClick={() => addNestedField(qIdx)} />
|
||||
</div>
|
||||
{(q.nested_full.fields || []).map((f, fIdx) =>
|
||||
renderFieldRow(f, fIdx,
|
||||
(p) => updateNestedField(qIdx, fIdx, p),
|
||||
() => removeNestedField(qIdx, fIdx),
|
||||
() => {}, () => {},
|
||||
(q.nested_full.fields || []).length
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Upload slots info (Quadro F) */}
|
||||
{q.upload_slots && (
|
||||
<>
|
||||
<Divider />
|
||||
<strong style={{ fontSize: 13 }}>Slot upload:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{q.upload_slots.map((slot, i) => (
|
||||
<Tag key={i} value={slot.label || slot.id} severity="secondary" style={{ marginRight: 4 }} />
|
||||
))}
|
||||
</div>
|
||||
<small style={{ color: '#888' }}>Gli slot upload sono gestiti dal modello: modifica avanzata non ancora via UI.</small>
|
||||
</>
|
||||
)}
|
||||
</AccordionTab>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuadriStructureEditor;
|
||||
1138
src/modules/ar1/pages/Ar1AdminConfig.js
Normal file
1138
src/modules/ar1/pages/Ar1AdminConfig.js
Normal file
File diff suppressed because it is too large
Load Diff
247
src/modules/ar1/pages/Ar1Home.js
Normal file
247
src/modules/ar1/pages/Ar1Home.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
|
||||
import { useStoreValue } from '../../../store';
|
||||
|
||||
import Ar1Service from '../service/ar1Service';
|
||||
import Ar1StatusTag from '../components/Ar1StatusTag';
|
||||
|
||||
const VARIANT_OPTIONS = [
|
||||
{ label: 'A1 — Persona Giuridica (societa, ente)', value: 'A1' },
|
||||
{ label: 'A2 — Ditta Individuale (P.IVA persona fisica)', value: 'A2' },
|
||||
{ label: 'A3 — Persona Fisica (senza P.IVA)', value: 'A3' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Ar1Home: schermata principale modulo AR1 per il beneficiario.
|
||||
* - Card status con countdown
|
||||
* - CTA dinamici (Compila / Riprendi / Firma / Rinnova)
|
||||
* - Storico dichiarazioni
|
||||
*/
|
||||
const Ar1Home = () => {
|
||||
const navigate = useNavigate();
|
||||
const toast = useRef(null);
|
||||
const companyId = useStoreValue('chosenCompanyId');
|
||||
|
||||
const [status, setStatus] = useState(null);
|
||||
const [history, setHistory] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [variantDialogOpen, setVariantDialogOpen] = useState(false);
|
||||
const [selectedVariant, setSelectedVariant] = useState('A1');
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const loadAll = () => {
|
||||
if (!companyId) return;
|
||||
setLoading(true);
|
||||
Ar1Service.getStatusForCompany(companyId,
|
||||
(resp) => setStatus(resp),
|
||||
(err) => console.warn('getStatus failed', err)
|
||||
);
|
||||
Ar1Service.listFormsForCompany(companyId,
|
||||
(resp) => {
|
||||
setHistory(resp?.items || []);
|
||||
setLoading(false);
|
||||
},
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Impossibile caricare lo storico' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [companyId]);
|
||||
|
||||
const startNewDraft = () => {
|
||||
setCreating(true);
|
||||
Ar1Service.createDraft(companyId, selectedVariant,
|
||||
(resp) => {
|
||||
setCreating(false);
|
||||
setVariantDialogOpen(false);
|
||||
if (resp?.id) navigate(`/ar1/wizard/${resp.id}`);
|
||||
},
|
||||
(err) => {
|
||||
setCreating(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Impossibile creare il form' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const resumeForm = (formId) => navigate(`/ar1/wizard/${formId}`);
|
||||
const goToSignature = (formId) => navigate(`/ar1/signature/${formId}`);
|
||||
|
||||
const deleteDraft = (formId) => {
|
||||
confirmDialog({
|
||||
message: __('Sei sicuro di voler eliminare questa bozza? L\'operazione non puo essere annullata.', 'gepafin'),
|
||||
header: __('Conferma eliminazione', 'gepafin'),
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: __('Elimina', 'gepafin'),
|
||||
rejectLabel: __('Annulla', 'gepafin'),
|
||||
acceptClassName: 'p-button-danger',
|
||||
accept: () => {
|
||||
Ar1Service.deleteForm(formId,
|
||||
() => {
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Bozza eliminata' });
|
||||
loadAll();
|
||||
},
|
||||
(err) => {
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Eliminazione fallita' });
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderStatusCard = () => {
|
||||
if (!status) return null;
|
||||
const isUrgent = ['MISSING', 'EXPIRED'].includes(status.status);
|
||||
const canCompile = ['MISSING', 'EXPIRED', 'APPROACHING', 'VALID'].includes(status.status);
|
||||
const hasActive = status.form_id && ['DRAFT', 'AWAITING_SIGNATURE'].includes(status.status);
|
||||
|
||||
return (
|
||||
<Card title={__('Stato Dichiarazione AR1 — Adeguata Verifica', 'gepafin')} style={{ marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Ar1StatusTag status={status.status} />
|
||||
{status.variant && (
|
||||
<span style={{ color: '#666' }}>
|
||||
{__('Variante:', 'gepafin')} <strong>{status.variant}</strong>
|
||||
</span>
|
||||
)}
|
||||
{status.days_to_expiry !== null && status.days_to_expiry !== undefined && (
|
||||
<span style={{ color: isUrgent ? '#b71c1c' : '#444', fontWeight: 600 }}>
|
||||
{status.days_to_expiry < 0
|
||||
? __(`Scaduta da ${Math.abs(status.days_to_expiry)} giorni`, 'gepafin')
|
||||
: __(`Scade tra ${status.days_to_expiry} giorni`, 'gepafin')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status.must_recompile_reason && (
|
||||
<div style={{ marginTop: 12, padding: 10, background: '#fff3e0', borderLeft: '3px solid #e65100' }}>
|
||||
<i className="pi pi-info-circle" style={{ marginRight: 6 }} />
|
||||
{status.must_recompile_reason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{hasActive && status.status === 'DRAFT' && (
|
||||
<Button label={__('Riprendi compilazione', 'gepafin')} icon="pi pi-pencil" onClick={() => resumeForm(status.form_id)} />
|
||||
)}
|
||||
{hasActive && status.status === 'AWAITING_SIGNATURE' && (
|
||||
<Button label={__('Procedi alla firma', 'gepafin')} icon="pi pi-verified" severity="warning" onClick={() => goToSignature(status.form_id)} />
|
||||
)}
|
||||
{!hasActive && canCompile && (
|
||||
<Button
|
||||
label={status.status === 'MISSING' ? __('Compila adesso', 'gepafin') : __('Rinnova dichiarazione', 'gepafin')}
|
||||
icon="pi pi-plus"
|
||||
onClick={() => setVariantDialogOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const statusTpl = (row) => <Ar1StatusTag status={row.status} />;
|
||||
const dateTpl = (row, field) => {
|
||||
const v = row[field];
|
||||
if (!v) return '—';
|
||||
try { return new Date(v).toLocaleDateString('it-IT'); } catch (e) { return v; }
|
||||
};
|
||||
const actionsTpl = (row) => (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{row.status === 'DRAFT' && (
|
||||
<>
|
||||
<Button icon="pi pi-pencil" rounded text onClick={() => resumeForm(row.id)} tooltip={__('Riprendi', 'gepafin')} />
|
||||
<Button icon="pi pi-trash" rounded text severity="danger" onClick={() => deleteDraft(row.id)} tooltip={__('Elimina', 'gepafin')} />
|
||||
</>
|
||||
)}
|
||||
{row.status === 'AWAITING_SIGNATURE' && (
|
||||
<Button icon="pi pi-verified" rounded text severity="warning" onClick={() => goToSignature(row.id)} tooltip={__('Firma', 'gepafin')} />
|
||||
)}
|
||||
{['SIGNED', 'VERIFIED', 'EXPIRED'].includes(row.status) && (
|
||||
<Button icon="pi pi-download" rounded text onClick={async () => {
|
||||
try {
|
||||
const blob = await Ar1Service.downloadPdfSigned(row.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `AR1_${row.variant}_signed.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: e.message });
|
||||
}
|
||||
}} tooltip={__('Scarica firmato', 'gepafin')} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Toast ref={toast} />
|
||||
<ConfirmDialog />
|
||||
|
||||
<h1>{__('Dichiarazione AR1 — Adeguata Verifica', 'gepafin')}</h1>
|
||||
<p style={{ color: '#666' }}>
|
||||
{__('Modulo di aggiornamento dell\'adeguata verifica ai sensi del D.Lgs. 231/2007 (normativa antiriciclaggio).', 'gepafin')}
|
||||
</p>
|
||||
|
||||
{renderStatusCard()}
|
||||
|
||||
<Card title={__('Storico dichiarazioni', 'gepafin')}>
|
||||
<DataTable
|
||||
value={history}
|
||||
loading={loading}
|
||||
emptyMessage={__('Nessuna dichiarazione presente', 'gepafin')}
|
||||
paginator={history.length > 10}
|
||||
rows={10}
|
||||
>
|
||||
<Column field="variant" header={__('Variante', 'gepafin')} />
|
||||
<Column field="template_version" header={__('Versione modulo', 'gepafin')} />
|
||||
<Column field="status" header={__('Stato', 'gepafin')} body={statusTpl} />
|
||||
<Column field="created_at" header={__('Creato il', 'gepafin')} body={(r) => dateTpl(r, 'created_at')} />
|
||||
<Column field="signed_at" header={__('Firmato il', 'gepafin')} body={(r) => dateTpl(r, 'signed_at')} />
|
||||
<Column field="expires_at" header={__('Scade il', 'gepafin')} body={(r) => dateTpl(r, 'expires_at')} />
|
||||
<Column header={__('Azioni', 'gepafin')} body={actionsTpl} style={{ width: 160 }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
header={__('Scegli tipologia', 'gepafin')}
|
||||
visible={variantDialogOpen}
|
||||
onHide={() => setVariantDialogOpen(false)}
|
||||
style={{ width: '480px', maxWidth: '95vw' }}
|
||||
modal
|
||||
>
|
||||
<p style={{ marginBottom: 10 }}>
|
||||
{__('Seleziona la tipologia di soggetto che rappresenti:', 'gepafin')}
|
||||
</p>
|
||||
<Dropdown
|
||||
value={selectedVariant}
|
||||
options={VARIANT_OPTIONS}
|
||||
onChange={(e) => setSelectedVariant(e.value)}
|
||||
style={{ width: '100%', marginBottom: 16 }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setVariantDialogOpen(false)} />
|
||||
<Button label={__('Inizia compilazione', 'gepafin')} icon="pi pi-arrow-right" iconPos="right" loading={creating} onClick={startNewDraft} />
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ar1Home;
|
||||
222
src/modules/ar1/pages/Ar1Signature.js
Normal file
222
src/modules/ar1/pages/Ar1Signature.js
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { FileUpload } from 'primereact/fileupload';
|
||||
import { Message } from 'primereact/message';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
|
||||
import Ar1Service from '../service/ar1Service';
|
||||
import Ar1StatusTag from '../components/Ar1StatusTag';
|
||||
|
||||
/**
|
||||
* Pagina firma AR1.
|
||||
* URL: /ar1/signature/:formId
|
||||
*
|
||||
* Flusso: genera PDF → download unsigned → firma FEQ client side → upload signed → DocVerify.
|
||||
*/
|
||||
const Ar1Signature = () => {
|
||||
const { formId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const toast = useRef(null);
|
||||
|
||||
const [form, setForm] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadAttempt, setUploadAttempt] = useState(0);
|
||||
const fileUploadRef = useRef(null);
|
||||
|
||||
const resetUploadInput = () => {
|
||||
try { fileUploadRef.current?.clear?.(); } catch (_) {}
|
||||
setUploadAttempt(n => n + 1);
|
||||
};
|
||||
|
||||
const refreshForm = () => {
|
||||
Ar1Service.getForm(formId,
|
||||
(resp) => { setForm(resp); setLoading(false); },
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Form non trovato' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => { if (formId) refreshForm(); /* eslint-disable-next-line */ }, [formId]);
|
||||
|
||||
const handleGeneratePdf = () => {
|
||||
setGenerating(true);
|
||||
Ar1Service.generatePdf(formId,
|
||||
() => {
|
||||
setGenerating(false);
|
||||
refreshForm();
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'PDF generato' });
|
||||
},
|
||||
(err) => {
|
||||
setGenerating(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Generazione PDF fallita' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDownloadUnsigned = async () => {
|
||||
try {
|
||||
const blob = await Ar1Service.downloadPdfUnsigned(formId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `AR1_${form.variant}_da-firmare.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadSigned = (event) => {
|
||||
const file = event.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const ext = file.name.toLowerCase().slice(file.name.lastIndexOf('.'));
|
||||
if (ext !== '.pdf' && ext !== '.p7m') {
|
||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Formato non valido', detail: 'Accettati: .pdf (PAdES) o .p7m (CAdES)' });
|
||||
resetUploadInput();
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
if (toast.current) toast.current.show({ severity: 'info', summary: 'Verifica in corso...', detail: 'Analisi firma digitale (fino a 60 secondi)' });
|
||||
|
||||
Ar1Service.uploadSignature(formId, file,
|
||||
(resp) => {
|
||||
setUploading(false);
|
||||
resetUploadInput();
|
||||
refreshForm();
|
||||
const outcome = resp?.outcome;
|
||||
if (outcome === 'VERIFIED') {
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'Firma verificata!', detail: 'La dichiarazione e stata archiviata nei tuoi documenti aziendali.' });
|
||||
setTimeout(() => navigate('/ar1'), 1500);
|
||||
} else if (outcome === 'SIGNED_NOT_VERIFIED') {
|
||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Firma accettata', detail: 'La firma e presente ma richiede verifica manuale da parte dell\'istruttore.' });
|
||||
} else if (outcome === 'SIGNED_DOCVERIFY_UNAVAILABLE') {
|
||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Verifica rimandata', detail: 'Servizio di verifica momentaneamente non disponibile. L\'istruttore verifichera la firma manualmente.' });
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setUploading(false);
|
||||
resetUploadInput();
|
||||
if (err?.detail?.code === 'NO_SIGNATURE_DETECTED') {
|
||||
if (toast.current) toast.current.show({
|
||||
severity: 'error',
|
||||
summary: 'Firma non rilevata',
|
||||
detail: 'Il file caricato non contiene una firma digitale valida. Firmare il PDF con il proprio strumento FEQ e ricaricarlo.',
|
||||
life: 6000
|
||||
});
|
||||
} else {
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: typeof err?.detail === 'string' ? err.detail : (err?.detail?.message || 'Upload fallito') });
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ textAlign: 'center', padding: 40 }}><ProgressSpinner /></div>;
|
||||
if (!form) return <div style={{ padding: 20 }}><Message severity="error" text={__('Form non trovato', 'gepafin')} /></div>;
|
||||
|
||||
const hasUnsignedPdf = !!form.pdf_unsigned_path;
|
||||
const hasSignedPdf = !!form.pdf_signed_path;
|
||||
const canUploadSig = form.status === 'AWAITING_SIGNATURE';
|
||||
const isDone = ['VERIFIED', 'SIGNED'].includes(form.status);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16, maxWidth: 900, margin: '0 auto' }}>
|
||||
<Toast ref={toast} />
|
||||
<h1>{__('Firma AR1', 'gepafin')} — {form.variant}</h1>
|
||||
<div style={{ marginBottom: 20 }}><Ar1StatusTag status={form.status} /></div>
|
||||
|
||||
<Card title={__('1. Scarica il modulo AR1 da firmare', 'gepafin')} style={{ marginBottom: 14 }}>
|
||||
{!hasUnsignedPdf && (
|
||||
<div>
|
||||
<p>{__('Il PDF del tuo modulo AR1 non e ancora stato generato.', 'gepafin')}</p>
|
||||
<Button label={__('Genera PDF', 'gepafin')} icon="pi pi-file-pdf" onClick={handleGeneratePdf} loading={generating} />
|
||||
</div>
|
||||
)}
|
||||
{hasUnsignedPdf && (
|
||||
<div>
|
||||
<p>{__('Scarica il PDF, firmalo con il tuo strumento FEQ (CNS, Aruba, Namirial, Dike) e ricaricalo qui sotto.', 'gepafin')}</p>
|
||||
<Button label={__('Scarica PDF', 'gepafin')} icon="pi pi-download" onClick={handleDownloadUnsigned} outlined />
|
||||
{!isDone && (
|
||||
<Button label={__('Rigenera PDF', 'gepafin')} icon="pi pi-refresh" text onClick={handleGeneratePdf} loading={generating} style={{ marginLeft: 8 }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title={__('2. Carica il PDF firmato', 'gepafin')} style={{ marginBottom: 14 }}>
|
||||
{!canUploadSig && !isDone && (
|
||||
<Message severity="warn" text={__(`Il modulo non e in stato AWAITING_SIGNATURE (attuale: ${form.status})`, 'gepafin')} />
|
||||
)}
|
||||
{canUploadSig && (
|
||||
<div>
|
||||
<p>{__('Formati accettati: PDF con firma PAdES oppure file .p7m (CAdES). Dimensione massima 50 MB.', 'gepafin')}</p>
|
||||
<FileUpload
|
||||
ref={fileUploadRef}
|
||||
key={uploadAttempt}
|
||||
name="file"
|
||||
mode="basic"
|
||||
accept=".pdf,.p7m"
|
||||
maxFileSize={50 * 1024 * 1024}
|
||||
customUpload
|
||||
uploadHandler={handleUploadSigned}
|
||||
auto
|
||||
chooseLabel={uploading ? __('Verifica in corso...', 'gepafin') : __('Seleziona PDF firmato', 'gepafin')}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isDone && (
|
||||
<div>
|
||||
<Message severity="success" text={
|
||||
form.status === 'VERIFIED'
|
||||
? __('Firma verificata con successo. La dichiarazione e archiviata nei tuoi documenti aziendali.', 'gepafin')
|
||||
: __('Firma accettata. Potrebbe richiedere verifica manuale.', 'gepafin')
|
||||
} style={{ marginBottom: 12 }} />
|
||||
{hasSignedPdf && (
|
||||
<Button label={__('Scarica PDF firmato', 'gepafin')} icon="pi pi-download" onClick={async () => {
|
||||
try {
|
||||
const blob = await Ar1Service.downloadPdfSigned(formId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `AR1_${form.variant}_signed${form.pdf_signed_path?.endsWith('.p7m') ? '.p7m' : '.pdf'}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: e.message });
|
||||
}
|
||||
}} outlined />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{form.signature_verified_at && (
|
||||
<Card title={__('Dettagli verifica', 'gepafin')}>
|
||||
<p><strong>{__('Firmatario:', 'gepafin')}</strong> {form.signature_signer_name || '—'}</p>
|
||||
<p><strong>{__('Codice fiscale:', 'gepafin')}</strong> {form.signature_signer_cf || '—'}</p>
|
||||
<p><strong>{__('Metodo:', 'gepafin')}</strong> {form.signature_type || '—'}</p>
|
||||
<p><strong>{__('Verificato il:', 'gepafin')}</strong> {new Date(form.signature_verified_at).toLocaleString('it-IT')}</p>
|
||||
<p><strong>{__('Scade il:', 'gepafin')}</strong> {form.expires_at ? new Date(form.expires_at).toLocaleDateString('it-IT') : '—'}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 20, textAlign: 'center' }}>
|
||||
<Button label={__('Torna alla Home AR1', 'gepafin')} severity="secondary" outlined onClick={() => navigate('/ar1')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ar1Signature;
|
||||
392
src/modules/ar1/pages/Ar1Wizard.js
Normal file
392
src/modules/ar1/pages/Ar1Wizard.js
Normal file
@@ -0,0 +1,392 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { RadioButton } from 'primereact/radiobutton';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Steps } from 'primereact/steps';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import { Message } from 'primereact/message';
|
||||
import { FileUpload } from 'primereact/fileupload';
|
||||
|
||||
import Ar1Service from '../service/ar1Service';
|
||||
|
||||
/**
|
||||
* Wizard data-driven: legge schema_snapshot del form e genera step/field dinamicamente.
|
||||
* Uno step per quadro. Auto-save onBlur via PUT /quadri.
|
||||
*
|
||||
* URL: /ar1/wizard/:formId
|
||||
*/
|
||||
const Ar1Wizard = () => {
|
||||
const { formId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const toast = useRef(null);
|
||||
|
||||
const [form, setForm] = useState(null);
|
||||
const [quadriValues, setQuadriValues] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const quadri = useMemo(
|
||||
() => (form?.schema_snapshot?.quadri || []).filter(q => !q.is_legal_frame),
|
||||
[form]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formId) return;
|
||||
Ar1Service.getForm(formId,
|
||||
(resp) => {
|
||||
setForm(resp);
|
||||
setQuadriValues(resp.quadri || {});
|
||||
setLoading(false);
|
||||
},
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Form non trovato' });
|
||||
}
|
||||
);
|
||||
}, [formId]);
|
||||
|
||||
const isReadonly = form && form.status !== 'DRAFT';
|
||||
|
||||
const saveQuadro = (quadroId) => {
|
||||
if (isReadonly) return;
|
||||
const patch = { [quadroId]: quadriValues[quadroId] || {} };
|
||||
setSaving(true);
|
||||
Ar1Service.updateQuadri(formId, patch,
|
||||
(resp) => { setSaving(false); setForm(resp); },
|
||||
(err) => {
|
||||
setSaving(false);
|
||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Save fallito', detail: err?.detail || 'Riprovare' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleFieldChange = (quadroId, fieldId, value) => {
|
||||
setQuadriValues(prev => ({
|
||||
...prev,
|
||||
[quadroId]: { ...(prev[quadroId] || {}), [fieldId]: value }
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRowFieldChange = (quadroId, rowIndex, fieldId, value) => {
|
||||
setQuadriValues(prev => {
|
||||
const q = prev[quadroId] || { rows: [] };
|
||||
const rows = [...(q.rows || [])];
|
||||
rows[rowIndex] = { ...(rows[rowIndex] || {}), [fieldId]: value };
|
||||
return { ...prev, [quadroId]: { ...q, rows } };
|
||||
});
|
||||
};
|
||||
|
||||
const addRow = (quadroId, maxRows) => {
|
||||
setQuadriValues(prev => {
|
||||
const q = prev[quadroId] || { rows: [] };
|
||||
if ((q.rows || []).length >= maxRows) return prev;
|
||||
return { ...prev, [quadroId]: { ...q, rows: [...(q.rows || []), {}] } };
|
||||
});
|
||||
};
|
||||
|
||||
const removeRow = (quadroId, rowIndex) => {
|
||||
setQuadriValues(prev => {
|
||||
const q = prev[quadroId] || { rows: [] };
|
||||
const rows = (q.rows || []).filter((_, i) => i !== rowIndex);
|
||||
return { ...prev, [quadroId]: { ...q, rows } };
|
||||
});
|
||||
};
|
||||
|
||||
const submitFinale = () => {
|
||||
if (!activeQuadro) return;
|
||||
setSubmitting(true);
|
||||
const patch = { [activeQuadro.id]: quadriValues[activeQuadro.id] || {} };
|
||||
Ar1Service.updateQuadri(formId, patch,
|
||||
() => {
|
||||
Ar1Service.submitForSignature(formId,
|
||||
() => {
|
||||
setSubmitting(false);
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Modulo pronto per la firma' });
|
||||
setTimeout(() => navigate(`/ar1/signature/${formId}`), 600);
|
||||
},
|
||||
(err) => {
|
||||
setSubmitting(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Submit fallito' });
|
||||
}
|
||||
);
|
||||
},
|
||||
(err) => {
|
||||
setSubmitting(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const renderField = (field, value, onChange, path = '') => {
|
||||
const key = `${path}-${field.id}`;
|
||||
const disabled = isReadonly;
|
||||
const req = field.required ? ' *' : '';
|
||||
const commonLabel = <label htmlFor={key} style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{field.label}{req}</label>;
|
||||
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'email':
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14 }}>
|
||||
{commonLabel}
|
||||
<InputText
|
||||
id={key}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(field.id, field.uppercase ? e.target.value.toUpperCase() : e.target.value)}
|
||||
disabled={disabled}
|
||||
maxLength={field.max_length}
|
||||
placeholder={field.placeholder}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{field.legal_ref && <small style={{ color: '#888' }}>{field.legal_ref}</small>}
|
||||
</div>
|
||||
);
|
||||
case 'textarea':
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14 }}>
|
||||
{commonLabel}
|
||||
<InputTextarea id={key} value={value || ''} onChange={(e) => onChange(field.id, e.target.value)} disabled={disabled} rows={3} maxLength={field.max_length} style={{ width: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
case 'date':
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14 }}>
|
||||
{commonLabel}
|
||||
<Calendar id={key} value={value ? new Date(value) : null} onChange={(e) => onChange(field.id, e.value ? e.value.toISOString().slice(0, 10) : null)} disabled={disabled} dateFormat="dd/mm/yy" showIcon style={{ width: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Checkbox inputId={key} checked={!!value} onChange={(e) => onChange(field.id, e.checked)} disabled={disabled} />
|
||||
<label htmlFor={key}>{field.label}</label>
|
||||
</div>
|
||||
);
|
||||
case 'radio':
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14 }}>
|
||||
{commonLabel}
|
||||
{(field.options || []).map((opt, idx) => {
|
||||
const optVal = typeof opt === 'string' ? opt : opt.value;
|
||||
const optLabel = typeof opt === 'string' ? opt : opt.label;
|
||||
const rid = `${key}-opt-${idx}`;
|
||||
return (
|
||||
<div key={rid} style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||||
<RadioButton inputId={rid} value={optVal} checked={value === optVal} onChange={(e) => onChange(field.id, e.value)} disabled={disabled} />
|
||||
<label htmlFor={rid}>{optLabel}</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
case 'enum':
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14 }}>
|
||||
{commonLabel}
|
||||
<Dropdown id={key} value={value} options={(field.options || []).map(o => ({ label: o.replace(/_/g, ' '), value: o }))} onChange={(e) => onChange(field.id, e.value)} disabled={disabled} style={{ width: '100%' }} showClear />
|
||||
</div>
|
||||
);
|
||||
case 'yes_no_with_note': {
|
||||
const v = typeof value === 'object' && value ? value : {};
|
||||
const yes = v.value === 'si' || v.value === 'yes' || v.value === 'true';
|
||||
const no = v.value === 'no' || v.value === 'false';
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14, padding: 10, background: '#fafafa', borderLeft: '3px solid #003d7a' }}>
|
||||
{commonLabel}
|
||||
{field.legal_ref && <small style={{ color: '#888', display: 'block', marginBottom: 6 }}>{field.legal_ref}</small>}
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<RadioButton inputId={`${key}-yes`} checked={yes} onChange={() => onChange(field.id, { ...v, value: 'si' })} disabled={disabled} />
|
||||
<label htmlFor={`${key}-yes`}>{__('Si', 'gepafin')}</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<RadioButton inputId={`${key}-no`} checked={no} onChange={() => onChange(field.id, { ...v, value: 'no' })} disabled={disabled} />
|
||||
<label htmlFor={`${key}-no`}>{__('No', 'gepafin')}</label>
|
||||
</div>
|
||||
</div>
|
||||
{yes && (
|
||||
<InputTextarea
|
||||
value={v.note || ''}
|
||||
onChange={(e) => onChange(field.id, { ...v, note: e.target.value })}
|
||||
disabled={disabled}
|
||||
rows={2}
|
||||
placeholder={field.note_label || __('Specificare', 'gepafin')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<div key={key} style={{ marginBottom: 14 }}>
|
||||
{commonLabel}
|
||||
<InputText value={value || ''} onChange={(e) => onChange(field.id, e.target.value)} disabled={disabled} style={{ width: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderQuadro = (quadro) => {
|
||||
const q = quadriValues[quadro.id] || {};
|
||||
|
||||
if (quadro.upload_slots) {
|
||||
return (
|
||||
<div>
|
||||
<h3>{quadro.title}</h3>
|
||||
{quadro.description && <p style={{ color: '#666' }}>{quadro.description}</p>}
|
||||
<Message severity="info" text={__('Carica qui i documenti richiesti. Il PDF firmato dell\'AR1 verra archiviato automaticamente nei tuoi documenti aziendali.', 'gepafin')} style={{ marginBottom: 14 }} />
|
||||
{quadro.upload_slots.map(slot => (
|
||||
<div key={slot.id} style={{ marginBottom: 14, padding: 10, border: '1px solid #ddd', borderRadius: 4 }}>
|
||||
<label style={{ fontWeight: 500 }}>{slot.label}{slot.required ? ' *' : ''}</label>
|
||||
{q[slot.id]?.filename ? (
|
||||
<div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: 8, padding: 8, background: '#f0f7ff', border: '1px solid #b3d4f0', borderRadius: 4 }}>
|
||||
<i className="pi pi-file" style={{ color: '#003d7a' }} />
|
||||
<span style={{ flex: 1, wordBreak: 'break-all' }}>{q[slot.id].filename}</span>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
label={__('Rimuovi', 'gepafin')}
|
||||
severity="danger"
|
||||
outlined
|
||||
size="small"
|
||||
disabled={isReadonly}
|
||||
onClick={() => handleFieldChange(quadro.id, slot.id, null)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FileUpload
|
||||
name={slot.id}
|
||||
mode="basic"
|
||||
accept={(slot.accept || []).join(',')}
|
||||
maxFileSize={(slot.max_size_mb || 300) * 1024 * 1024}
|
||||
disabled={isReadonly}
|
||||
customUpload
|
||||
uploadHandler={(e) => {
|
||||
const file = e.files[0];
|
||||
handleFieldChange(quadro.id, slot.id, { filename: file.name, size: file.size });
|
||||
if (toast.current) toast.current.show({ severity: 'info', summary: 'File selezionato', detail: file.name });
|
||||
}}
|
||||
auto
|
||||
chooseLabel={__('Scegli file', 'gepafin')}
|
||||
style={{ marginTop: 6 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (quadro.row_type) {
|
||||
const rows = q.rows || [];
|
||||
return (
|
||||
<div>
|
||||
<h3>{quadro.title}</h3>
|
||||
{quadro.description && <p style={{ color: '#666' }}>{quadro.description}</p>}
|
||||
{rows.map((row, idx) => (
|
||||
<Card key={idx} title={`${quadro.row_type.replace(/_/g, ' ').toUpperCase()} #${idx + 1}`} style={{ marginBottom: 12 }}>
|
||||
{(quadro.row_fields || []).map(field => renderField(field, row[field.id], (fid, val) => handleRowFieldChange(quadro.id, idx, fid, val), `q-${quadro.id}-row-${idx}`))}
|
||||
{!isReadonly && (
|
||||
<Button label={__('Rimuovi', 'gepafin')} severity="danger" outlined icon="pi pi-trash" size="small" onClick={() => removeRow(quadro.id, idx)} />
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
{!isReadonly && rows.length < (quadro.max_rows || 4) && (
|
||||
<Button label={__('Aggiungi', 'gepafin')} icon="pi pi-plus" outlined onClick={() => addRow(quadro.id, quadro.max_rows || 4)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{quadro.title}</h3>
|
||||
{quadro.description && <p style={{ color: '#666' }}>{quadro.description}</p>}
|
||||
{(quadro.fields || []).map(field => renderField(field, q[field.id], (fid, val) => handleFieldChange(quadro.id, fid, val), `q-${quadro.id}`))}
|
||||
{quadro.nested_full && (
|
||||
<div style={{ marginTop: 16, padding: 12, background: '#f5f5f5', borderRadius: 4 }}>
|
||||
<h4 style={{ marginTop: 0 }}>{__('Dettaglio aggiuntivo', 'gepafin')}</h4>
|
||||
{(quadro.nested_full.fields || []).map(field => renderField(
|
||||
field,
|
||||
(q.nested || {})[field.id],
|
||||
(fid, val) => handleFieldChange(quadro.id, 'nested', { ...((q.nested) || {}), [fid]: val }),
|
||||
`q-${quadro.id}-nested`
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ textAlign: 'center', padding: 40 }}><ProgressSpinner /></div>;
|
||||
if (!form) return <div style={{ padding: 20 }}><Message severity="error" text={__('Form non trovato', 'gepafin')} /></div>;
|
||||
if (quadri.length === 0) return <div style={{ padding: 20 }}><Message severity="warn" text={__('Nessun quadro editabile nel template. Contattare il supporto.', 'gepafin')} /></div>;
|
||||
|
||||
const steps = quadri.map(q => ({ label: q.id }));
|
||||
// clamp activeIndex: difensivo se quadri cambia lunghezza o e fuori range
|
||||
const safeIndex = quadri.length === 0 ? 0 : Math.max(0, Math.min(activeIndex, quadri.length - 1));
|
||||
const activeQuadro = quadri[safeIndex];
|
||||
const isLastStep = safeIndex === quadri.length - 1;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Toast ref={toast} />
|
||||
<h1>{__('Compilazione AR1', 'gepafin')} — {form.variant}</h1>
|
||||
{isReadonly && (
|
||||
<Message severity="info" text={__(`Form in stato ${form.status} — sola lettura`, 'gepafin')} style={{ marginBottom: 14 }} />
|
||||
)}
|
||||
|
||||
<Steps
|
||||
model={steps}
|
||||
activeIndex={safeIndex}
|
||||
onSelect={(e) => {
|
||||
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
||||
const next = Math.max(0, Math.min(e.index, quadri.length - 1));
|
||||
setActiveIndex(next);
|
||||
}}
|
||||
readOnly={false}
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
|
||||
<Card style={{ marginBottom: 14 }} onBlur={() => { if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); }}>
|
||||
{activeQuadro && renderQuadro(activeQuadro)}
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button label={__('Indietro', 'gepafin')} icon="pi pi-arrow-left" severity="secondary" outlined disabled={activeIndex === 0}
|
||||
onClick={() => {
|
||||
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
||||
setActiveIndex(Math.max(0, activeIndex - 1));
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{saving && <span style={{ color: '#888' }}>{__('Salvataggio...', 'gepafin')}</span>}
|
||||
{!isLastStep && (
|
||||
<Button label={__('Avanti', 'gepafin')} icon="pi pi-arrow-right" iconPos="right"
|
||||
onClick={() => {
|
||||
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
||||
setActiveIndex(Math.min(quadri.length - 1, activeIndex + 1));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isLastStep && !isReadonly && (
|
||||
<Button label={__('Procedi alla firma', 'gepafin')} icon="pi pi-verified" severity="warning" loading={submitting} onClick={submitFinale} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ar1Wizard;
|
||||
259
src/modules/ar1/service/ar1Service.js
Normal file
259
src/modules/ar1/service/ar1Service.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Client HTTP per ar1-compiler (microservizio BFLOWS).
|
||||
* Il microservizio valida lo stesso JWT di GEPAFIN-BE (HS512 shared secret).
|
||||
*
|
||||
* Env var: REACT_APP_AR1_API_URL (es. http://78.46.41.91:18091)
|
||||
*
|
||||
* Pattern replicato 1:1 da rendicontazioneService.js.
|
||||
*/
|
||||
import { storeGet } from '../../../store';
|
||||
|
||||
const BASE_URL = process.env.REACT_APP_AR1_API_URL || '';
|
||||
|
||||
const buildHeaders = () => {
|
||||
const token = storeGet('getToken');
|
||||
const h = { 'Content-Type': 'application/json' };
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
return h;
|
||||
};
|
||||
|
||||
const buildHeadersMultipart = () => {
|
||||
const token = storeGet('getToken');
|
||||
const h = {};
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
return h;
|
||||
};
|
||||
|
||||
const handleResponse = async (response, onSuccess, onError) => {
|
||||
let body = null;
|
||||
try { body = await response.json(); } catch (e) { body = { detail: response.statusText }; }
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
if (onSuccess) onSuccess(body);
|
||||
} else {
|
||||
if (onError) onError({ status: response.status, ...body });
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (err, onError) => {
|
||||
if (onError) onError({ status: 0, detail: err.message });
|
||||
};
|
||||
|
||||
const Ar1Service = {
|
||||
// ---------- Status pubblico (per compliance modal) ----------
|
||||
getStatusForCompany(companyId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/public/ar1-status/${companyId}`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- CRUD form beneficiario ----------
|
||||
createDraft(companyId, variant, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify({ company_id: companyId, variant })
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
getForm(formId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
listFormsForCompany(companyId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/company/${companyId}`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
updateQuadri(formId, quadriPatch, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}/quadri`, {
|
||||
method: 'PUT', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify({ quadri: quadriPatch })
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
submitForSignature(formId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}/submit-for-signature`, {
|
||||
method: 'PUT', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
deleteForm(formId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}`, {
|
||||
method: 'DELETE', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => {
|
||||
if (r.status === 204) { if (onSuccess) onSuccess({}); }
|
||||
else handleResponse(r, onSuccess, onError);
|
||||
}).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- PDF ----------
|
||||
generatePdf(formId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}/generate-pdf`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
downloadPdfUnsigned(formId) {
|
||||
return fetch(`${BASE_URL}/api/ar1-forms/${formId}/pdf-unsigned`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeadersMultipart()
|
||||
}).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.blob(); });
|
||||
},
|
||||
|
||||
downloadPdfSigned(formId) {
|
||||
return fetch(`${BASE_URL}/api/ar1-forms/${formId}/pdf-signed`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeadersMultipart()
|
||||
}).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.blob(); });
|
||||
},
|
||||
|
||||
// ---------- Firma ----------
|
||||
uploadSignature(formId, fileObject, onSuccess, onError) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileObject);
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}/upload-signature`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeadersMultipart(),
|
||||
body: formData
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
reVerifySignature(formId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}/verify`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
archiveToCompanyDocument(formId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/api/ar1-forms/${formId}/archive-to-company-document`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- ADMIN: templates ----------
|
||||
listTemplates(onSuccess, onError, queryParams) {
|
||||
const qs = queryParams ? ('?' + new URLSearchParams(queryParams).toString()) : '';
|
||||
fetch(`${BASE_URL}/admin/ar1-templates${qs}`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
getTemplateDetail(templateId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-templates/${templateId}`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
getNextVersion(variant, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-templates/${variant}/next-version`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
updateTemplateLayout(templateId, layoutConfig, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-templates/${templateId}/layout-config`, {
|
||||
method: 'PUT', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify({ layout_config: layoutConfig })
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
createNewTemplateVersion(variant, payload, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-templates/${variant}/new-version`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- ADMIN: policy ----------
|
||||
getPolicy(onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-policy`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
updatePolicy(patch, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-policy`, {
|
||||
method: 'PUT', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(patch)
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- ADMIN: pec-schedule-config ----------
|
||||
listPecSchedule(onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
createPecRule(payload, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
updatePecRule(ruleId, payload, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config/${ruleId}`, {
|
||||
method: 'PUT', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
deletePecRule(ruleId, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config/${ruleId}`, {
|
||||
method: 'DELETE', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => {
|
||||
if (r.status === 204) { if (onSuccess) onSuccess({}); }
|
||||
else handleResponse(r, onSuccess, onError);
|
||||
}).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- ADMIN: bulk PEC ----------
|
||||
bulkRequestRecompilation(payload, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-forms/bulk-request-recompilation`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- ADMIN: document categories (per dropdown) ----------
|
||||
previewTemplatePdf(templateId) {
|
||||
return fetch(`${BASE_URL}/admin/ar1-templates/${templateId}/preview-pdf`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeadersMultipart()
|
||||
}).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.blob(); });
|
||||
},
|
||||
|
||||
listDocumentCategories(onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/document-categories`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
// ---------- ADMIN: email templates (Opzione 3 — tenant-agnostic, BE Gepafin pull) ----------
|
||||
listEmailTemplates(onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-email-templates`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
getEmailTemplate(kind, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-email-templates/${kind}`, {
|
||||
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
updateEmailTemplate(kind, payload, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-email-templates/${kind}`, {
|
||||
method: 'PUT', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
|
||||
previewEmailTemplate(kind, mockVars, onSuccess, onError) {
|
||||
fetch(`${BASE_URL}/admin/ar1-email-templates/${kind}/preview`, {
|
||||
method: 'POST', mode: 'cors', headers: buildHeaders(),
|
||||
body: JSON.stringify(mockVars || {})
|
||||
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
|
||||
},
|
||||
};
|
||||
|
||||
export default Ar1Service;
|
||||
@@ -21,6 +21,10 @@ import PraticaRendicontazioneEdit from './modules/rendicontazione/pages/PraticaR
|
||||
import DevSwitchUser from './modules/rendicontazione/pages/DevSwitchUser';
|
||||
import IstruttoriaQueue from './modules/rendicontazione/pages/IstruttoriaQueue';
|
||||
import IstruttoriaPratica from './modules/rendicontazione/pages/IstruttoriaPratica';
|
||||
import Ar1Home from './modules/ar1/pages/Ar1Home';
|
||||
import Ar1Wizard from './modules/ar1/pages/Ar1Wizard';
|
||||
import Ar1Signature from './modules/ar1/pages/Ar1Signature';
|
||||
import Ar1AdminConfig from './modules/ar1/pages/Ar1AdminConfig';
|
||||
import BandoFlowEdit from './pages/BandoFlowEdit';
|
||||
import Imieibandi from './pages/Imieibandi';
|
||||
import BandoApplication from './pages/BandoApplication';
|
||||
@@ -163,6 +167,34 @@ const routes = ({ role, chosenCompanyId }) => {
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/ar1" element={<DefaultLayout>
|
||||
{'ROLE_BENEFICIARY' === role ? <Ar1Home/> : null}
|
||||
{'ROLE_SUPER_ADMIN' === role ? <Ar1Home/> : null}
|
||||
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/ar1/wizard/:formId" element={<DefaultLayout>
|
||||
{'ROLE_BENEFICIARY' === role ? <Ar1Wizard/> : null}
|
||||
{'ROLE_SUPER_ADMIN' === role ? <Ar1Wizard/> : null}
|
||||
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/ar1/signature/:formId" element={<DefaultLayout>
|
||||
{'ROLE_BENEFICIARY' === role ? <Ar1Signature/> : null}
|
||||
{'ROLE_SUPER_ADMIN' === role ? <Ar1Signature/> : null}
|
||||
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/ar1-admin" element={<DefaultLayout>
|
||||
{'ROLE_SUPER_ADMIN' === role ? <Ar1AdminConfig/> : <PageNotFound/>}
|
||||
{'ROLE_BENEFICIARY' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/rendicontazioni/:id" element={<DefaultLayout>
|
||||
{'ROLE_BENEFICIARY' === role ? <PraticaRendicontazioneEdit/> : null}
|
||||
{'ROLE_SUPER_ADMIN' === role ? <PraticaRendicontazioneEdit/> : null}
|
||||
|
||||
Reference in New Issue
Block a user