feat(ar1): superadmin Ar1AdminConfig TabView 4 sezioni (templates+policy+pec+bulk)
Nuova voce sidebar 'Configurazione AR1' (icona pi pi-id-card, href /ar1-admin,
permesso MANAGE_TENDERS) accanto a 'Rendicontazione'. Pagina dedicata
Ar1AdminConfig.js (490 LOC) con TabView PrimeReact a 4 sezioni:
1. TEMPLATE — DataTable con 3 varianti (A1/A2/A3), status+version+quadri_count.
Bottone 'Edit layout L2' (Dialog con InputTextarea JSON layout_config,
chiama PUT /admin/ar1-templates/:id/layout-config).
Bottone 'Nuova versione' (Dialog con version semver + layout + toggle
activate_now, chiama POST /admin/ar1-templates/:variant/new-version).
2. POLICY — grid 2 colonne con editor singleton:
- validity_days (InputNumber 30-1825, default 365)
- popup_dismiss_hours (InputNumber 1-168, default 24)
- company_document_category_id (InputNumber, default 4 ANTIRICICLAGGIO)
- popup_force_on_expired (InputSwitch)
- auto_archive_on_company_document (InputSwitch)
- allow_bulk_recompilation_request (InputSwitch)
Save via PUT /admin/ar1-policy.
3. REGOLE REMINDER PEC — DataTable CRUD con Dialog edit:
kind (disabled se editing), offset_days, is_recurring+recurring_interval_days,
enabled, description. Chiamate POST/PUT/DELETE /admin/ar1-pec-schedule-config.
4. INVIO MASSIVO PEC — InputText company_ids virgola-separati, Checkbox
only_expired/only_missing. Bottoni:
- Dry-run (eye icon, severity info) → chiama con dry_run=true
- Invia PEC live (send icon, severity warning) → ConfirmDialog prima di
chiamare con dry_run=false
Result box con matched/marked counts.
SERVICE — src/modules/ar1/service/ar1Service.js esteso da 164 a 213 LOC:
+ listTemplates (con query params opzionali)
+ getTemplateDetail
+ updateTemplateLayout
+ createNewTemplateVersion
+ getPolicy / updatePolicy
+ listPecSchedule / createPecRule / updatePecRule / deletePecRule
+ bulkRequestRecompilation
INTEGRAZIONE:
src/layouts/DefaultLayout/components/AppSidebar/index.js
+ voce 'Configurazione AR1' id=23 (MANAGE_TENDERS) dopo 'Rendicontazione'
src/routes.js
+ import Ar1AdminConfig
+ route /ar1-admin (solo ROLE_SUPER_ADMIN, altri PageNotFound)
VALIDAZIONE: parse-check 9 file con @babel/parser + plugin JSX: 9 OK / 0 FAIL.
This commit is contained in:
@@ -1,532 +1,490 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
// primereact
|
||||
// prime
|
||||
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 { TabView, TabPanel } from 'primereact/tabview';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { InputSwitch } from 'primereact/inputswitch';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Message } from 'primereact/message';
|
||||
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
||||
import { Chips } from 'primereact/chips';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
|
||||
// service
|
||||
import Ar1Service from '../service/ar1Service';
|
||||
|
||||
/**
|
||||
* Configurazione AR1 (superadmin). 4 sottosezioni in TabView:
|
||||
* 1. Template — lista versioni con status (ACTIVE/ARCHIVED/DRAFT), edit L2 layout_config, nuova versione
|
||||
* 2. Policy — singleton (validity, popup, auto-archive, bulk flag)
|
||||
* 3. Regole Reminder PEC — CRUD ar1_pec_schedule_config
|
||||
* 4. Invio Massivo PEC — filtri azienda + dry-run + submit live
|
||||
* Ar1AdminConfig — configurazione AR1 per superadmin.
|
||||
* URL: /ar1-admin
|
||||
*
|
||||
* Percorso: /ar1-admin (permessi MANAGE_TENDERS)
|
||||
* 4 sezioni (TabView):
|
||||
* 1. Template — lista + layout editor L2 + nuova versione
|
||||
* 2. Policy — singleton (validity, popup, auto-archive, category)
|
||||
* 3. Regole reminder PEC — CRUD pec-schedule-config
|
||||
* 4. Invio massivo PEC — bulk-request-recompilation (dry-run + live)
|
||||
*/
|
||||
const Ar1AdminConfig = () => {
|
||||
const toast = useRef(null);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
|
||||
// ========= TEMPLATES =========
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [loadingTpl, setLoadingTpl] = useState(false);
|
||||
const [editLayoutOpen, setEditLayoutOpen] = useState(false);
|
||||
const [editLayoutTpl, setEditLayoutTpl] = useState(null);
|
||||
const [layoutJsonText, setLayoutJsonText] = useState('');
|
||||
const [newVersionOpen, setNewVersionOpen] = useState(false);
|
||||
const [newVersionVariant, setNewVersionVariant] = useState('A1');
|
||||
const [newVersionData, setNewVersionData] = useState({ version: '', layout_config: '{}', activate_now: true });
|
||||
|
||||
// ========= POLICY =========
|
||||
const [policy, setPolicy] = useState(null);
|
||||
const [policyDraft, setPolicyDraft] = useState(null);
|
||||
const [savingPolicy, setSavingPolicy] = useState(false);
|
||||
|
||||
// ========= PEC RULES =========
|
||||
const [pecRules, setPecRules] = useState([]);
|
||||
const [pecDialogOpen, setPecDialogOpen] = useState(false);
|
||||
const [pecEditing, setPecEditing] = useState(null);
|
||||
const [pecDraft, setPecDraft] = useState({ kind: '', offset_days: 0, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' });
|
||||
|
||||
// ========= BULK =========
|
||||
const [bulkCompanyIds, setBulkCompanyIds] = useState('');
|
||||
const [bulkOnlyExpired, setBulkOnlyExpired] = useState(false);
|
||||
const [bulkOnlyMissing, setBulkOnlyMissing] = useState(false);
|
||||
const [bulkResult, setBulkResult] = useState(null);
|
||||
const [bulkRunning, setBulkRunning] = useState(false);
|
||||
|
||||
// ---- load all ----
|
||||
const loadTemplates = () => {
|
||||
setLoadingTpl(true);
|
||||
Ar1Service.listTemplates(
|
||||
(resp) => { setTemplates(resp?.items || resp || []); setLoadingTpl(false); },
|
||||
(err) => { setLoadingTpl(false); if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Load templates fallito' }); }
|
||||
);
|
||||
};
|
||||
const loadPolicy = () => {
|
||||
Ar1Service.getPolicy(
|
||||
(resp) => { setPolicy(resp); setPolicyDraft(resp); },
|
||||
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Load policy fallito' }); }
|
||||
);
|
||||
};
|
||||
const loadPecRules = () => {
|
||||
Ar1Service.listPecSchedule(
|
||||
(resp) => setPecRules(resp?.items || resp || []),
|
||||
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Load pec rules fallito' }); }
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
loadPolicy();
|
||||
loadPecRules();
|
||||
}, []);
|
||||
|
||||
// ========= TEMPLATE HANDLERS =========
|
||||
const openEditLayout = (tpl) => {
|
||||
setEditLayoutTpl(tpl);
|
||||
setLayoutJsonText(JSON.stringify(tpl.layout_config || {}, null, 2));
|
||||
setEditLayoutOpen(true);
|
||||
};
|
||||
|
||||
const saveLayout = () => {
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(layoutJsonText); }
|
||||
catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'JSON invalido', detail: e.message }); return; }
|
||||
Ar1Service.updateTemplateLayout(editLayoutTpl.id, parsed,
|
||||
() => {
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Layout aggiornato' });
|
||||
setEditLayoutOpen(false);
|
||||
loadTemplates();
|
||||
},
|
||||
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' }); }
|
||||
);
|
||||
};
|
||||
|
||||
const openNewVersion = (variant) => {
|
||||
setNewVersionVariant(variant);
|
||||
setNewVersionData({ version: '', layout_config: '{}', activate_now: true });
|
||||
setNewVersionOpen(true);
|
||||
};
|
||||
|
||||
const saveNewVersion = () => {
|
||||
let layoutParsed;
|
||||
try { layoutParsed = JSON.parse(newVersionData.layout_config); }
|
||||
catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'Layout JSON invalido', detail: e.message }); return; }
|
||||
Ar1Service.createNewTemplateVersion(newVersionVariant, {
|
||||
version: newVersionData.version,
|
||||
layout_config: layoutParsed,
|
||||
activate_now: newVersionData.activate_now
|
||||
},
|
||||
() => {
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: `Nuova versione ${newVersionVariant} v${newVersionData.version} creata` });
|
||||
setNewVersionOpen(false);
|
||||
loadTemplates();
|
||||
},
|
||||
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Create fallito' }); }
|
||||
);
|
||||
};
|
||||
|
||||
// ========= POLICY HANDLERS =========
|
||||
const savePolicy = () => {
|
||||
setSavingPolicy(true);
|
||||
const patch = {
|
||||
validity_days: policyDraft.validity_days,
|
||||
popup_dismiss_hours: policyDraft.popup_dismiss_hours,
|
||||
popup_force_on_expired: policyDraft.popup_force_on_expired,
|
||||
auto_archive_on_company_document: policyDraft.auto_archive_on_company_document,
|
||||
company_document_category_id: policyDraft.company_document_category_id,
|
||||
allow_bulk_recompilation_request: policyDraft.allow_bulk_recompilation_request,
|
||||
};
|
||||
Ar1Service.updatePolicy(patch,
|
||||
(resp) => {
|
||||
setSavingPolicy(false);
|
||||
setPolicy(resp);
|
||||
setPolicyDraft(resp);
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Policy aggiornata' });
|
||||
},
|
||||
(err) => {
|
||||
setSavingPolicy(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save policy fallito' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// ========= PEC RULE HANDLERS =========
|
||||
const openPecDialog = (rule) => {
|
||||
if (rule) {
|
||||
setPecEditing(rule);
|
||||
setPecDraft({
|
||||
kind: rule.kind,
|
||||
offset_days: rule.offset_days,
|
||||
is_recurring: rule.is_recurring,
|
||||
recurring_interval_days: rule.recurring_interval_days,
|
||||
enabled: rule.enabled,
|
||||
description: rule.description || ''
|
||||
});
|
||||
} else {
|
||||
setPecEditing(null);
|
||||
setPecDraft({ kind: '', offset_days: 0, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' });
|
||||
}
|
||||
setPecDialogOpen(true);
|
||||
};
|
||||
|
||||
const savePecRule = () => {
|
||||
const payload = { ...pecDraft };
|
||||
const onOk = () => {
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: pecEditing ? 'Regola aggiornata' : 'Regola creata' });
|
||||
setPecDialogOpen(false);
|
||||
loadPecRules();
|
||||
};
|
||||
const onKo = (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' }); };
|
||||
if (pecEditing) Ar1Service.updatePecRule(pecEditing.id, payload, onOk, onKo);
|
||||
else Ar1Service.createPecRule(payload, onOk, onKo);
|
||||
};
|
||||
|
||||
const deletePecRule = (rule) => {
|
||||
confirmDialog({
|
||||
message: `Eliminare la regola "${rule.kind}"?`,
|
||||
header: 'Conferma eliminazione',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptClassName: 'p-button-danger',
|
||||
accept: () => {
|
||||
Ar1Service.deletePecRule(rule.id,
|
||||
() => {
|
||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Regola eliminata' });
|
||||
loadPecRules();
|
||||
},
|
||||
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Delete fallito' }); }
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ========= BULK HANDLERS =========
|
||||
const runBulk = (dryRun) => {
|
||||
setBulkRunning(true);
|
||||
const companyIds = bulkCompanyIds.trim()
|
||||
? bulkCompanyIds.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n))
|
||||
: null;
|
||||
const payload = {
|
||||
dry_run: dryRun,
|
||||
only_expired: bulkOnlyExpired,
|
||||
only_missing: bulkOnlyMissing,
|
||||
company_ids: companyIds,
|
||||
};
|
||||
Ar1Service.bulkRequestRecompilation(payload,
|
||||
(resp) => {
|
||||
setBulkRunning(false);
|
||||
setBulkResult({ ...resp, was_dry_run: dryRun });
|
||||
if (toast.current) toast.current.show({
|
||||
severity: 'success',
|
||||
summary: dryRun ? 'Dry-run completato' : 'Bulk eseguito',
|
||||
detail: `${resp.matched || 0} aziende matchate`
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
setBulkRunning(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Bulk fallito' });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// ---------- RENDER ----------
|
||||
const tplStatusTpl = (row) => {
|
||||
const severity = row.status === 'ACTIVE' ? 'success' : row.status === 'ARCHIVED' ? 'secondary' : 'warning';
|
||||
return <Tag severity={severity} value={row.status} />;
|
||||
};
|
||||
|
||||
const tplActionsTpl = (row) => (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-pencil" rounded text onClick={() => openEditLayout(row)} tooltip="Modifica layout L2" />
|
||||
<Button icon="pi pi-plus-circle" rounded text severity="warning" onClick={() => openNewVersion(row.variant)} tooltip="Nuova versione" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const pecActionsTpl = (row) => (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-pencil" rounded text onClick={() => openPecDialog(row)} />
|
||||
<Button icon="pi pi-trash" rounded text severity="danger" onClick={() => deletePecRule(row)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const pecBoolTpl = (row, field) => <i className={row[field] ? 'pi pi-check' : 'pi pi-times'} style={{ color: row[field] ? '#2e7d32' : '#c62828' }} />;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Toast ref={toast} />
|
||||
<ConfirmDialog />
|
||||
|
||||
<h1>{__('Configurazione AR1 — Adeguata Verifica', 'gepafin')}</h1>
|
||||
<h1>{__('Configurazione AR1', 'gepafin')}</h1>
|
||||
<p style={{ color: '#666' }}>
|
||||
{__('Gestione template, policy, regole reminder PEC e invio massivo solleciti per la dichiarazione AR1 (D.Lgs. 231/2007).', 'gepafin')}
|
||||
{__('Gestione template, policy, regole di scadenza e invio massivo PEC per il modulo di adeguata verifica.', 'gepafin')}
|
||||
</p>
|
||||
|
||||
<TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
|
||||
<TabPanel header={__('Template AR1', 'gepafin')} leftIcon="pi pi-file mr-2">
|
||||
<TemplatesTab toast={toast} />
|
||||
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
||||
|
||||
{/* === TAB 1: TEMPLATES === */}
|
||||
<TabPanel header={__('Template', 'gepafin')} leftIcon="pi pi-file-edit">
|
||||
<Card>
|
||||
<Message severity="info" text={__('I template AR1 hanno 3 varianti (A1 Persona Giuridica, A2 Ditta Individuale, A3 Persona Fisica). Puoi modificare il layout grafico (L2) della versione ACTIVE oppure creare una nuova versione semver.', 'gepafin')} style={{ marginBottom: 14 }} />
|
||||
<DataTable value={templates} loading={loadingTpl} emptyMessage={__('Nessun template', 'gepafin')}>
|
||||
<Column field="variant" header="Variante" style={{ width: 100 }} />
|
||||
<Column field="version" header="Versione" style={{ width: 100 }} />
|
||||
<Column field="status" header="Stato" body={tplStatusTpl} style={{ width: 120 }} />
|
||||
<Column field="quadri_count" header="Quadri" style={{ width: 80 }} body={(r) => r.quadri_count ?? (r.questions_snapshot?.quadri?.length ?? '—')} />
|
||||
<Column header="Azioni" body={tplActionsTpl} style={{ width: 120 }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel header={__('Policy', 'gepafin')} leftIcon="pi pi-cog mr-2">
|
||||
<PolicyTab toast={toast} />
|
||||
|
||||
{/* === TAB 2: POLICY === */}
|
||||
<TabPanel header={__('Policy', 'gepafin')} leftIcon="pi pi-cog">
|
||||
<Card>
|
||||
{!policyDraft && <p>{__('Caricamento...', 'gepafin')}</p>}
|
||||
{policyDraft && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Validita (giorni)', 'gepafin')}</label>
|
||||
<InputNumber value={policyDraft.validity_days} onValueChange={(e) => setPolicyDraft({ ...policyDraft, validity_days: e.value })} min={30} max={1825} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>30-1825 giorni (default 365)</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Dismiss pop-up (ore)', 'gepafin')}</label>
|
||||
<InputNumber value={policyDraft.popup_dismiss_hours} onValueChange={(e) => setPolicyDraft({ ...policyDraft, popup_dismiss_hours: e.value })} min={1} max={168} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>1-168 ore (default 24)</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Categoria documento (company_document)', 'gepafin')}</label>
|
||||
<InputNumber value={policyDraft.company_document_category_id} onValueChange={(e) => setPolicyDraft({ ...policyDraft, company_document_category_id: e.value })} min={1} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>default 4 = ANTIRICICLAGGIO</small>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 22 }}>
|
||||
<InputSwitch checked={!!policyDraft.popup_force_on_expired} onChange={(e) => setPolicyDraft({ ...policyDraft, popup_force_on_expired: e.value })} />
|
||||
<label>{__('Pop-up forzato se EXPIRED', 'gepafin')}</label>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<InputSwitch checked={!!policyDraft.auto_archive_on_company_document} onChange={(e) => setPolicyDraft({ ...policyDraft, auto_archive_on_company_document: e.value })} />
|
||||
<label>{__('Auto-archivio su company_document', 'gepafin')}</label>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<InputSwitch checked={!!policyDraft.allow_bulk_recompilation_request} onChange={(e) => setPolicyDraft({ ...policyDraft, allow_bulk_recompilation_request: e.value })} />
|
||||
<label>{__('Invio massivo PEC abilitato', 'gepafin')}</label>
|
||||
</div>
|
||||
|
||||
<div style={{ gridColumn: '1 / -1', display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setPolicyDraft(policy)} disabled={savingPolicy} />
|
||||
<Button label={__('Salva policy', 'gepafin')} icon="pi pi-save" onClick={savePolicy} loading={savingPolicy} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel header={__('Regole Reminder PEC', 'gepafin')} leftIcon="pi pi-clock mr-2">
|
||||
<PecScheduleTab toast={toast} />
|
||||
|
||||
{/* === TAB 3: PEC RULES === */}
|
||||
<TabPanel header={__('Regole reminder PEC', 'gepafin')} leftIcon="pi pi-bell">
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 10 }}>
|
||||
<Button label={__('Nuova regola', 'gepafin')} icon="pi pi-plus" onClick={() => openPecDialog(null)} />
|
||||
</div>
|
||||
<DataTable value={pecRules} emptyMessage={__('Nessuna regola', 'gepafin')}>
|
||||
<Column field="kind" header="Kind" />
|
||||
<Column field="offset_days" header="Offset (gg)" style={{ width: 110 }} />
|
||||
<Column field="is_recurring" header="Ricorrente" body={(r) => pecBoolTpl(r, 'is_recurring')} style={{ width: 110 }} />
|
||||
<Column field="recurring_interval_days" header="Intervallo" style={{ width: 110 }} />
|
||||
<Column field="enabled" header="Attiva" body={(r) => pecBoolTpl(r, 'enabled')} style={{ width: 90 }} />
|
||||
<Column field="description" header="Descrizione" />
|
||||
<Column header="Azioni" body={pecActionsTpl} style={{ width: 110 }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel header={__('Invio Massivo PEC', 'gepafin')} leftIcon="pi pi-send mr-2">
|
||||
<BulkPecTab toast={toast} />
|
||||
|
||||
{/* === TAB 4: BULK PEC === */}
|
||||
<TabPanel header={__('Invio massivo PEC', 'gepafin')} leftIcon="pi pi-send">
|
||||
<Card>
|
||||
<Message severity="warn" text={__('Questa azione invia PEC di richiesta aggiornamento AR1 a tutte le aziende matchate. Esegui sempre prima un dry-run per verificare il numero di destinatari.', 'gepafin')} style={{ marginBottom: 14 }} />
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('ID aziende (virgola-separati, vuoto = tutte)', 'gepafin')}</label>
|
||||
<InputText value={bulkCompanyIds} onChange={(e) => setBulkCompanyIds(e.target.value)} placeholder="es. 1, 7, 42" style={{ width: '100%' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Checkbox inputId="bulk-expired" checked={bulkOnlyExpired} onChange={(e) => setBulkOnlyExpired(e.checked)} />
|
||||
<label htmlFor="bulk-expired">{__('Solo EXPIRED', 'gepafin')}</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Checkbox inputId="bulk-missing" checked={bulkOnlyMissing} onChange={(e) => setBulkOnlyMissing(e.checked)} />
|
||||
<label htmlFor="bulk-missing">{__('Solo MISSING', 'gepafin')}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button label={__('Dry-run (anteprima)', 'gepafin')} icon="pi pi-eye" severity="info" outlined onClick={() => runBulk(true)} loading={bulkRunning} />
|
||||
<Button label={__('Invia PEC (live)', 'gepafin')} icon="pi pi-send" severity="warning" onClick={() => {
|
||||
confirmDialog({
|
||||
message: `Confermi invio PEC live? Questa azione notifichera i BE poller per dispatch PEC reale.`,
|
||||
header: 'Conferma invio PEC',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
accept: () => runBulk(false)
|
||||
});
|
||||
}} loading={bulkRunning} />
|
||||
</div>
|
||||
|
||||
{bulkResult && (
|
||||
<Card title={bulkResult.was_dry_run ? __('Anteprima (dry-run)', 'gepafin') : __('Esito invio', 'gepafin')} style={{ marginTop: 14 }}>
|
||||
<p><strong>{__('Aziende matchate:', 'gepafin')}</strong> {bulkResult.matched ?? 0}</p>
|
||||
{!bulkResult.was_dry_run && (
|
||||
<p><strong>{__('Form segnati per PEC:', 'gepafin')}</strong> {bulkResult.marked_for_pec ?? bulkResult.marked ?? 0}</p>
|
||||
)}
|
||||
{bulkResult.company_ids && bulkResult.company_ids.length > 0 && (
|
||||
<p><strong>{__('ID:', 'gepafin')}</strong> {bulkResult.company_ids.join(', ')}</p>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
</TabView>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// ========== Tab 1: Template ==========
|
||||
|
||||
const TemplatesTab = ({ toast }) => {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [selectedTpl, setSelectedTpl] = useState(null);
|
||||
const [layoutJson, setLayoutJson] = useState('');
|
||||
const [newVersionDialog, setNewVersionDialog] = useState(false);
|
||||
const [newVersionPayload, setNewVersionPayload] = useState({ version: '', layout_config: {}, activate_now: true });
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
Ar1Service.adminListTemplates(
|
||||
(resp) => {
|
||||
setTemplates(resp?.items || []);
|
||||
setLoading(false);
|
||||
},
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Impossibile caricare template' });
|
||||
}
|
||||
);
|
||||
};
|
||||
useEffect(() => { load(); /* eslint-disable-next-line */ }, []);
|
||||
|
||||
const openDetail = (row) => {
|
||||
Ar1Service.adminGetTemplate(row.id,
|
||||
(resp) => {
|
||||
setSelectedTpl(resp);
|
||||
setLayoutJson(JSON.stringify(resp.layout_config || {}, null, 2));
|
||||
setDetailOpen(true);
|
||||
},
|
||||
(err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail })
|
||||
);
|
||||
};
|
||||
|
||||
const saveLayout = () => {
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(layoutJson); }
|
||||
catch (e) {
|
||||
toast.current?.show({ severity: 'error', summary: 'JSON non valido', detail: e.message });
|
||||
return;
|
||||
}
|
||||
Ar1Service.adminUpdateLayoutConfig(selectedTpl.id, parsed,
|
||||
() => {
|
||||
toast.current?.show({ severity: 'success', summary: 'OK', detail: 'Layout aggiornato' });
|
||||
setDetailOpen(false);
|
||||
load();
|
||||
},
|
||||
(err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail })
|
||||
);
|
||||
};
|
||||
|
||||
const openNewVersion = (variant) => {
|
||||
const current = templates.find(t => t.variant === variant && t.status === 'ACTIVE');
|
||||
setNewVersionPayload({
|
||||
version: '',
|
||||
layout_config: current?.layout_config || {},
|
||||
activate_now: true,
|
||||
_variant: variant,
|
||||
});
|
||||
setNewVersionDialog(true);
|
||||
};
|
||||
|
||||
const submitNewVersion = () => {
|
||||
Ar1Service.adminNewVersion(newVersionPayload._variant, {
|
||||
version: newVersionPayload.version,
|
||||
layout_config: newVersionPayload.layout_config,
|
||||
activate_now: newVersionPayload.activate_now,
|
||||
},
|
||||
() => {
|
||||
toast.current?.show({ severity: 'success', summary: 'OK', detail: `Nuova versione ${newVersionPayload.version} creata` });
|
||||
setNewVersionDialog(false);
|
||||
load();
|
||||
},
|
||||
(err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail })
|
||||
);
|
||||
};
|
||||
|
||||
const statusTpl = (row) => {
|
||||
const map = {
|
||||
ACTIVE: { severity: 'success', icon: 'pi pi-check-circle' },
|
||||
DRAFT: { severity: 'warning', icon: 'pi pi-pencil' },
|
||||
ARCHIVED: { severity: 'secondary', icon: 'pi pi-history' },
|
||||
};
|
||||
const cfg = map[row.status] || { severity: 'info', icon: 'pi pi-circle' };
|
||||
return <Tag severity={cfg.severity} icon={cfg.icon} value={row.status} />;
|
||||
};
|
||||
|
||||
const actionsTpl = (row) => (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-eye" rounded text tooltip={__('Dettaglio + edit layout', 'gepafin')} onClick={() => openDetail(row)} />
|
||||
{row.status === 'ACTIVE' && (
|
||||
<Button icon="pi pi-plus-circle" rounded text tooltip={__('Nuova versione', 'gepafin')} onClick={() => openNewVersion(row.variant)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<DataTable value={templates} loading={loading} emptyMessage={__('Nessun template', 'gepafin')}>
|
||||
<Column field="variant" header={__('Variante', 'gepafin')} style={{ width: 100 }} />
|
||||
<Column field="variant_label" header={__('Descrizione', 'gepafin')} />
|
||||
<Column field="version" header={__('Versione', 'gepafin')} style={{ width: 100 }} />
|
||||
<Column field="status" header={__('Status', 'gepafin')} body={statusTpl} style={{ width: 130 }} />
|
||||
<Column field="quadri_count" header={__('N. Quadri', 'gepafin')} style={{ width: 90 }} />
|
||||
<Column header={__('Azioni', 'gepafin')} body={actionsTpl} style={{ width: 120 }} />
|
||||
</DataTable>
|
||||
|
||||
{/* Dialog detail */}
|
||||
<Dialog header={selectedTpl ? `${selectedTpl.variant} v${selectedTpl.version}` : ''} visible={detailOpen} onHide={() => setDetailOpen(false)} style={{ width: '780px', maxWidth: '95vw' }} modal>
|
||||
{selectedTpl && (
|
||||
<div>
|
||||
<p><strong>{__('Status:', 'gepafin')}</strong> {selectedTpl.status}</p>
|
||||
<p><strong>{__('N. Quadri:', 'gepafin')}</strong> {(selectedTpl.questions_snapshot?.quadri || []).length}</p>
|
||||
|
||||
<h4>{__('Layout config (L2 — editabile)', 'gepafin')}</h4>
|
||||
<p style={{ color: '#888', fontSize: 12 }}>
|
||||
{__('Modifica il JSON del layout (brand, header, intro, privacy, field_labels_override). Le domande normative (L1) non sono editabili da qui.', 'gepafin')}
|
||||
</p>
|
||||
<textarea value={layoutJson} onChange={(e) => setLayoutJson(e.target.value)} rows={16} style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 12 }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setDetailOpen(false)} />
|
||||
<Button label={__('Salva layout', 'gepafin')} icon="pi pi-save" onClick={saveLayout} disabled={selectedTpl.status === 'ARCHIVED'} />
|
||||
</div>
|
||||
{selectedTpl.status === 'ARCHIVED' && (
|
||||
<p style={{ color: '#b71c1c', fontSize: 12, marginTop: 8 }}>
|
||||
{__('Template ARCHIVED — non modificabile. Usa "Nuova versione" per creare una variante.', 'gepafin')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog new version */}
|
||||
<Dialog header={__('Nuova versione template', 'gepafin')} visible={newVersionDialog} onHide={() => setNewVersionDialog(false)} style={{ width: '560px', maxWidth: '95vw' }} modal>
|
||||
<p>{__('Variante:', 'gepafin')} <strong>{newVersionPayload._variant}</strong></p>
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Versione (semver xx.yy.zz)', 'gepafin')}</label>
|
||||
<InputText value={newVersionPayload.version} onChange={(e) => setNewVersionPayload({ ...newVersionPayload, version: e.target.value })} placeholder="1.1.0" style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 14, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Checkbox inputId="activate_now" checked={newVersionPayload.activate_now} onChange={(e) => setNewVersionPayload({ ...newVersionPayload, activate_now: e.checked })} />
|
||||
<label htmlFor="activate_now">{__('Attiva subito (ARCHIVE precedente ACTIVE)', 'gepafin')}</label>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: '#888' }}>
|
||||
{__('Il layout_config viene pre-popolato da quello ACTIVE corrente. Puoi modificarlo dopo la creazione.', 'gepafin')}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setNewVersionDialog(false)} />
|
||||
<Button label={__('Crea versione', 'gepafin')} icon="pi pi-plus" onClick={submitNewVersion} disabled={!newVersionPayload.version.match(/^\d+\.\d+\.\d+$/)} />
|
||||
{/* DIALOG: edit layout L2 */}
|
||||
<Dialog
|
||||
header={editLayoutTpl ? `${__('Modifica layout', 'gepafin')} ${editLayoutTpl.variant} v${editLayoutTpl.version}` : ''}
|
||||
visible={editLayoutOpen}
|
||||
onHide={() => setEditLayoutOpen(false)}
|
||||
style={{ width: '720px', maxWidth: '95vw' }}
|
||||
modal
|
||||
>
|
||||
<Message severity="info" text={__('Layout L2 in JSON. Modifiche applicate solo a template ACTIVE. Per cambi strutturali usare "Nuova versione".', 'gepafin')} style={{ marginBottom: 10 }} />
|
||||
<InputTextarea value={layoutJsonText} onChange={(e) => setLayoutJsonText(e.target.value)} rows={18} style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setEditLayoutOpen(false)} />
|
||||
<Button label={__('Salva layout', 'gepafin')} icon="pi pi-save" onClick={saveLayout} />
|
||||
</div>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// ========== Tab 2: Policy ==========
|
||||
|
||||
const PolicyTab = ({ toast }) => {
|
||||
const [policy, setPolicy] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
Ar1Service.adminGetPolicy(
|
||||
(resp) => { setPolicy(resp); setLoading(false); },
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail });
|
||||
}
|
||||
);
|
||||
};
|
||||
useEffect(() => { load(); /* eslint-disable-next-line */ }, []);
|
||||
|
||||
const save = () => {
|
||||
if (!policy) return;
|
||||
const payload = {
|
||||
validity_days: policy.validity_days,
|
||||
popup_dismiss_hours: policy.popup_dismiss_hours,
|
||||
popup_force_on_expired: policy.popup_force_on_expired,
|
||||
auto_archive_on_company_document: policy.auto_archive_on_company_document,
|
||||
company_document_category_id: policy.company_document_category_id,
|
||||
allow_bulk_recompilation_request: policy.allow_bulk_recompilation_request,
|
||||
};
|
||||
Ar1Service.adminUpdatePolicy(payload,
|
||||
() => { toast.current?.show({ severity: 'success', summary: 'OK', detail: 'Policy aggiornata' }); load(); },
|
||||
(err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail })
|
||||
);
|
||||
};
|
||||
|
||||
if (loading || !policy) return <Card><p>{__('Caricamento...', 'gepafin')}</p></Card>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 500 }}>
|
||||
{__('Validità dichiarazione (giorni)', 'gepafin')}
|
||||
</label>
|
||||
<InputNumber value={policy.validity_days} onValueChange={(e) => setPolicy({ ...policy, validity_days: e.value })} min={30} max={1825} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>{__('Range: 30-1825 giorni (5 anni max)', 'gepafin')}</small>
|
||||
{/* DIALOG: nuova versione */}
|
||||
<Dialog
|
||||
header={`${__('Nuova versione variante', 'gepafin')} ${newVersionVariant}`}
|
||||
visible={newVersionOpen}
|
||||
onHide={() => setNewVersionOpen(false)}
|
||||
style={{ width: '720px', maxWidth: '95vw' }}
|
||||
modal
|
||||
>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Versione (semver, maggiore dell\'ACTIVE)', 'gepafin')}</label>
|
||||
<InputText value={newVersionData.version} onChange={(e) => setNewVersionData({ ...newVersionData, version: e.target.value })} placeholder="es. 1.1.0" style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 500 }}>
|
||||
{__('Ore di dismiss popup', 'gepafin')}
|
||||
</label>
|
||||
<InputNumber value={policy.popup_dismiss_hours} onValueChange={(e) => setPolicy({ ...policy, popup_dismiss_hours: e.value })} min={1} max={168} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>{__('Range: 1-168 ore (1 settimana max)', 'gepafin')}</small>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Layout config JSON', 'gepafin')}</label>
|
||||
<InputTextarea value={newVersionData.layout_config} onChange={(e) => setNewVersionData({ ...newVersionData, layout_config: e.target.value })} rows={12} style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 500 }}>
|
||||
{__('Categoria documento (auto-archive)', 'gepafin')}
|
||||
</label>
|
||||
<InputNumber value={policy.company_document_category_id} onValueChange={(e) => setPolicy({ ...policy, company_document_category_id: e.value })} min={1} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>{__('4 = ANTIRICICLAGGIO (default Gepafin)', 'gepafin')}</small>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<InputSwitch checked={newVersionData.activate_now} onChange={(e) => setNewVersionData({ ...newVersionData, activate_now: e.value })} />
|
||||
<label>{__('Attiva subito (archivia versione precedente)', 'gepafin')}</label>
|
||||
</div>
|
||||
<div></div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<InputSwitch checked={policy.popup_force_on_expired} onChange={(e) => setPolicy({ ...policy, popup_force_on_expired: e.value })} />
|
||||
<label>{__('Popup bloccante se EXPIRED', 'gepafin')}</label>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setNewVersionOpen(false)} />
|
||||
<Button label={__('Crea versione', 'gepafin')} icon="pi pi-plus" onClick={saveNewVersion} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<InputSwitch checked={policy.auto_archive_on_company_document} onChange={(e) => setPolicy({ ...policy, auto_archive_on_company_document: e.value })} />
|
||||
<label>{__('Auto-archive in company_document', 'gepafin')}</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<InputSwitch checked={policy.allow_bulk_recompilation_request} onChange={(e) => setPolicy({ ...policy, allow_bulk_recompilation_request: e.value })} />
|
||||
<label>{__('Abilita invio massivo PEC', 'gepafin')}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Button label={__('Salva policy', 'gepafin')} icon="pi pi-save" onClick={save} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// ========== Tab 3: Regole Reminder PEC ==========
|
||||
|
||||
const PecScheduleTab = ({ toast }) => {
|
||||
const [rules, setRules] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editDialog, setEditDialog] = useState(false);
|
||||
const [editingRule, setEditingRule] = useState(null);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
Ar1Service.adminListPecSchedule(
|
||||
(resp) => { setRules(resp?.items || []); setLoading(false); },
|
||||
(err) => {
|
||||
setLoading(false);
|
||||
toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail });
|
||||
}
|
||||
);
|
||||
};
|
||||
useEffect(() => { load(); /* eslint-disable-next-line */ }, []);
|
||||
|
||||
const openNew = () => {
|
||||
setEditingRule({ kind: '', offset_days: 30, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' });
|
||||
setEditDialog(true);
|
||||
};
|
||||
const openEdit = (r) => {
|
||||
setEditingRule({ ...r });
|
||||
setEditDialog(true);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!editingRule) return;
|
||||
const cb = {
|
||||
onOk: () => { toast.current?.show({ severity: 'success', summary: 'OK', detail: 'Regola salvata' }); setEditDialog(false); load(); },
|
||||
onErr: (err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail }),
|
||||
};
|
||||
if (editingRule.id) {
|
||||
Ar1Service.adminUpdatePecRule(editingRule.id, {
|
||||
offset_days: editingRule.offset_days, is_recurring: editingRule.is_recurring,
|
||||
recurring_interval_days: editingRule.recurring_interval_days, enabled: editingRule.enabled,
|
||||
description: editingRule.description,
|
||||
}, cb.onOk, cb.onErr);
|
||||
} else {
|
||||
Ar1Service.adminCreatePecRule(editingRule, cb.onOk, cb.onErr);
|
||||
}
|
||||
};
|
||||
|
||||
const del = (rule) => {
|
||||
confirmDialog({
|
||||
message: __(`Eliminare la regola "${rule.kind}"?`, 'gepafin'),
|
||||
header: __('Conferma', 'gepafin'), icon: 'pi pi-exclamation-triangle',
|
||||
acceptClassName: 'p-button-danger',
|
||||
accept: () => {
|
||||
Ar1Service.adminDeletePecRule(rule.id,
|
||||
() => { toast.current?.show({ severity: 'success', summary: 'OK', detail: 'Regola eliminata' }); load(); },
|
||||
(err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail })
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const enabledTpl = (r) => <Tag severity={r.enabled ? 'success' : 'secondary'} value={r.enabled ? 'ON' : 'OFF'} />;
|
||||
const recurringTpl = (r) => r.is_recurring ? `ogni ${r.recurring_interval_days}gg` : '—';
|
||||
const actionsTpl = (r) => (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-pencil" rounded text onClick={() => openEdit(r)} />
|
||||
<Button icon="pi pi-trash" rounded text severity="danger" onClick={() => del(r)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Button label={__('Nuova regola', 'gepafin')} icon="pi pi-plus" onClick={openNew} />
|
||||
</div>
|
||||
<DataTable value={rules} loading={loading} emptyMessage={__('Nessuna regola', 'gepafin')}>
|
||||
<Column field="kind" header={__('Kind', 'gepafin')} />
|
||||
<Column field="offset_days" header={__('Offset (gg)', 'gepafin')} style={{ width: 110 }} />
|
||||
<Column header={__('Ricorrenza', 'gepafin')} body={recurringTpl} style={{ width: 130 }} />
|
||||
<Column field="description" header={__('Descrizione', 'gepafin')} />
|
||||
<Column header={__('Abilitata', 'gepafin')} body={enabledTpl} style={{ width: 100 }} />
|
||||
<Column header={__('Azioni', 'gepafin')} body={actionsTpl} style={{ width: 110 }} />
|
||||
</DataTable>
|
||||
|
||||
<Dialog header={editingRule?.id ? __('Modifica regola', 'gepafin') : __('Nuova regola', 'gepafin')} visible={editDialog} onHide={() => setEditDialog(false)} style={{ width: '520px', maxWidth: '95vw' }} modal>
|
||||
{editingRule && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Kind (identificatore, unique, es: AR1_REMINDER_30D)', 'gepafin')}</label>
|
||||
<InputText value={editingRule.kind || ''} onChange={(e) => setEditingRule({ ...editingRule, kind: e.target.value })} disabled={!!editingRule.id} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Offset giorni (positivo = prima scadenza, negativo = dopo)', 'gepafin')}</label>
|
||||
<InputNumber value={editingRule.offset_days} onValueChange={(e) => setEditingRule({ ...editingRule, offset_days: e.value })} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Checkbox inputId="rec" checked={!!editingRule.is_recurring} onChange={(e) => setEditingRule({ ...editingRule, is_recurring: e.checked })} />
|
||||
<label htmlFor="rec">{__('Ricorrente', 'gepafin')}</label>
|
||||
</div>
|
||||
{editingRule.is_recurring && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Intervallo ripetizione (giorni)', 'gepafin')}</label>
|
||||
<InputNumber value={editingRule.recurring_interval_days} onValueChange={(e) => setEditingRule({ ...editingRule, recurring_interval_days: e.value })} min={1} style={{ width: '100%' }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Descrizione', 'gepafin')}</label>
|
||||
<InputText value={editingRule.description || ''} onChange={(e) => setEditingRule({ ...editingRule, description: e.target.value })} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<InputSwitch checked={editingRule.enabled} onChange={(e) => setEditingRule({ ...editingRule, enabled: e.value })} />
|
||||
<label>{__('Abilitata', 'gepafin')}</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setEditDialog(false)} />
|
||||
<Button label={__('Salva', 'gepafin')} icon="pi pi-save" onClick={save} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// ========== Tab 4: Invio Massivo PEC ==========
|
||||
|
||||
const BulkPecTab = ({ toast }) => {
|
||||
const [filters, setFilters] = useState({
|
||||
only_expired: false,
|
||||
only_missing: false,
|
||||
company_ids: [],
|
||||
expired_before: null,
|
||||
});
|
||||
const [dryRunResult, setDryRunResult] = useState(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const runDryRun = () => {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
only_expired: filters.only_expired,
|
||||
only_missing: filters.only_missing,
|
||||
company_ids: filters.company_ids.length ? filters.company_ids.map(Number) : null,
|
||||
expired_before: filters.expired_before ? filters.expired_before.toISOString() : null,
|
||||
dry_run: true,
|
||||
};
|
||||
Ar1Service.adminBulkRecompilation(payload,
|
||||
(resp) => { setSubmitting(false); setDryRunResult(resp); },
|
||||
(err) => { setSubmitting(false); toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail }); }
|
||||
);
|
||||
};
|
||||
|
||||
const runLive = () => {
|
||||
confirmDialog({
|
||||
message: __(`Confermi l'invio di ${dryRunResult?.matched_count || 0} PEC? L'operazione non può essere annullata.`, 'gepafin'),
|
||||
header: __('Conferma invio massivo', 'gepafin'), icon: 'pi pi-exclamation-triangle',
|
||||
accept: () => {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
only_expired: filters.only_expired,
|
||||
only_missing: filters.only_missing,
|
||||
company_ids: filters.company_ids.length ? filters.company_ids.map(Number) : null,
|
||||
expired_before: filters.expired_before ? filters.expired_before.toISOString() : null,
|
||||
dry_run: false,
|
||||
};
|
||||
Ar1Service.adminBulkRecompilation(payload,
|
||||
(resp) => {
|
||||
setSubmitting(false);
|
||||
toast.current?.show({ severity: 'success', summary: 'Inviato', detail: `Marcati ${resp.marked_count} form per PEC` });
|
||||
setDryRunResult(null);
|
||||
},
|
||||
(err) => { setSubmitting(false); toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail }); }
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p style={{ color: '#666' }}>
|
||||
{__('Invia una PEC di sollecito ricompilazione a un insieme filtrato di aziende. Il micro-servizio marca i form; il BE poller invia la PEC tenant-aware (PEC Massiva o Mailgun) entro 5 minuti.', 'gepafin')}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginTop: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Checkbox inputId="exp" checked={filters.only_expired} onChange={(e) => setFilters({ ...filters, only_expired: e.checked })} />
|
||||
<label htmlFor="exp">{__('Solo aziende con AR1 EXPIRED', 'gepafin')}</label>
|
||||
{/* DIALOG: edit pec rule */}
|
||||
<Dialog
|
||||
header={pecEditing ? __('Modifica regola PEC', 'gepafin') : __('Nuova regola PEC', 'gepafin')}
|
||||
visible={pecDialogOpen}
|
||||
onHide={() => setPecDialogOpen(false)}
|
||||
style={{ width: '560px', maxWidth: '95vw' }}
|
||||
modal
|
||||
>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Kind', 'gepafin')}</label>
|
||||
<InputText value={pecDraft.kind} onChange={(e) => setPecDraft({ ...pecDraft, kind: e.target.value })} placeholder="es. AR1_REMINDER_30D" style={{ width: '100%' }} disabled={!!pecEditing} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Checkbox inputId="mis" checked={filters.only_missing} onChange={(e) => setFilters({ ...filters, only_missing: e.checked })} />
|
||||
<label htmlFor="mis">{__('Solo aziende senza AR1 (richiede company_ids)', 'gepafin')}</label>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Offset giorni (rispetto a expires_at)', 'gepafin')}</label>
|
||||
<InputNumber value={pecDraft.offset_days} onValueChange={(e) => setPecDraft({ ...pecDraft, offset_days: e.value })} style={{ width: '100%' }} />
|
||||
<small style={{ color: '#888' }}>{__('Negativo = prima della scadenza, positivo = dopo', 'gepafin')}</small>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Company IDs (opzionale, lista)', 'gepafin')}</label>
|
||||
<Chips value={filters.company_ids} onChange={(e) => setFilters({ ...filters, company_ids: e.value })} placeholder={__('es. 1, 2, 15', 'gepafin')} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<InputSwitch checked={pecDraft.is_recurring} onChange={(e) => setPecDraft({ ...pecDraft, is_recurring: e.value })} />
|
||||
<label>{__('Ricorrente', 'gepafin')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 4 }}>{__('Scaduto prima di (solo con EXPIRED)', 'gepafin')}</label>
|
||||
<Calendar value={filters.expired_before} onChange={(e) => setFilters({ ...filters, expired_before: e.value })} dateFormat="dd/mm/yy" showIcon style={{ width: '100%' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20, display: 'flex', gap: 8 }}>
|
||||
<Button label={__('Dry-run (conta aziende)', 'gepafin')} icon="pi pi-search" onClick={runDryRun} loading={submitting} />
|
||||
{dryRunResult && dryRunResult.matched_count > 0 && (
|
||||
<Button label={__(`Invia PEC a ${dryRunResult.matched_count} aziende`, 'gepafin')} icon="pi pi-send" severity="warning" onClick={runLive} loading={submitting} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dryRunResult && (
|
||||
<div style={{ marginTop: 20, padding: 14, background: '#f5f5f5', borderRadius: 4 }}>
|
||||
<p><strong>{__('Risultato dry-run', 'gepafin')}</strong></p>
|
||||
<p>{__('Aziende matchate:', 'gepafin')} <strong>{dryRunResult.matched_count}</strong></p>
|
||||
<p>{__('Già in coda PEC:', 'gepafin')} <strong>{dryRunResult.already_pending_count || 0}</strong></p>
|
||||
{dryRunResult.sample_company_ids && dryRunResult.sample_company_ids.length > 0 && (
|
||||
<p>{__('Sample company IDs (max 20):', 'gepafin')} {dryRunResult.sample_company_ids.join(', ')}</p>
|
||||
)}
|
||||
{dryRunResult.note && <p style={{ color: '#888', fontSize: 12 }}>{dryRunResult.note}</p>}
|
||||
{pecDraft.is_recurring && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Intervallo ricorrenza (giorni)', 'gepafin')}</label>
|
||||
<InputNumber value={pecDraft.recurring_interval_days} onValueChange={(e) => setPecDraft({ ...pecDraft, recurring_interval_days: e.value })} min={1} style={{ width: '100%' }} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<InputSwitch checked={pecDraft.enabled} onChange={(e) => setPecDraft({ ...pecDraft, enabled: e.value })} />
|
||||
<label>{__('Attiva', 'gepafin')}</label>
|
||||
</div>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{__('Descrizione', 'gepafin')}</label>
|
||||
<InputTextarea value={pecDraft.description} onChange={(e) => setPecDraft({ ...pecDraft, description: e.target.value })} rows={2} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
|
||||
<Button label={__('Annulla', 'gepafin')} severity="secondary" outlined onClick={() => setPecDialogOpen(false)} />
|
||||
<Button label={__('Salva', 'gepafin')} icon="pi pi-save" onClick={savePecRule} />
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Ar1AdminConfig;
|
||||
|
||||
Reference in New Issue
Block a user