From 8988bed952dbd0e40d04d12cfffdf9356e412182 Mon Sep 17 00:00:00 2001 From: BFLOWS Date: Sat, 18 Apr 2026 18:51:57 +0200 Subject: [PATCH] feat(schemas): picker 3-card blank/template/clone per nuovo schema rendicontazione Sostituisce il vecchio flusso che proponeva solo 'Inizializza con template RE-START'. Quando il bando non ha ancora uno schema, mostra un picker a 3 card: 1. 'Nuovo schema' -> inizializzazione blank (scheletro vuoto da popolare) 2. 'Da template' -> dropdown con template predefiniti + descrizione 3. 'Clona da bando' -> dropdown bandi con schema esistente Nuovo componente: components/SchemaTemplatePicker.js (200 righe). Gestisce: - loading parallelo di templates + clonable-calls - selezione card con border highlight primary-color - dropdown espanso solo sulla card attiva (stopPropagation su click) - bottone Inizia disabilitato finche la selezione non e completa - spinner durante init, callback onInitialized con schema_json per aggiornare il form dell'editor senza reload pagina Nuovo service esportato: schemaPickerService { listTemplates, listClonableCalls, initializeSchema(callId, payload) }. BandoRendicontazioneSchemaEdit.js: rimosso il box 'Inizializza con template RE-START', sostituito con quando !hasSchema. onInitialized popola setSchemaRecord + setForm + mostra toast di conferma. Funzione handleInitializeRestart resta nel file (non ancora chiamata, per sicurezza rollback). --- .../components/SchemaTemplatePicker.js | 198 ++++++++++++++++++ .../pages/BandoRendicontazioneSchemaEdit.js | 29 ++- .../service/rendicontazioneService.js | 33 +++ 3 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 src/modules/rendicontazione/components/SchemaTemplatePicker.js diff --git a/src/modules/rendicontazione/components/SchemaTemplatePicker.js b/src/modules/rendicontazione/components/SchemaTemplatePicker.js new file mode 100644 index 0000000..189a4fb --- /dev/null +++ b/src/modules/rendicontazione/components/SchemaTemplatePicker.js @@ -0,0 +1,198 @@ +import React, { useEffect, useState } from 'react'; +import { __ } from '@wordpress/i18n'; + +import { Button } from 'primereact/button'; +import { Card } from 'primereact/card'; +import { Dropdown } from 'primereact/dropdown'; +import { Message } from 'primereact/message'; +import { Skeleton } from 'primereact/skeleton'; + +import { schemaPickerService } from '../service/rendicontazioneService'; + +/** + * SchemaTemplatePicker + * Mostrato quando un bando non ha ancora uno schema di rendicontazione. + * Offre 3 modalita: schema nuovo vuoto, da template predefinito, clone da altro bando. + * + * Props: + * callId number — bando target + * onInitialized fn(schemaData) — chiamato dopo initialize con successo + * onError fn(err) + */ +const SchemaTemplatePicker = ({ callId, onInitialized, onError }) => { + const [templates, setTemplates] = useState([]); + const [clonableCalls, setClonableCalls] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // Selezione utente + const [mode, setMode] = useState(null); // null | 'blank' | 'template' | 'clone' + const [pickedTemplateId, setPickedTemplateId] = useState(null); + const [pickedSourceCallId, setPickedSourceCallId] = useState(null); + + useEffect(() => { + let active = true; + setLoading(true); + let tpls = null, clones = null; + const finish = () => { + if (tpls !== null && clones !== null && active) setLoading(false); + }; + + schemaPickerService.listTemplates( + (resp) => { if (active) { tpls = resp?.data?.templates || []; setTemplates(tpls); } finish(); }, + (err) => { tpls = []; finish(); if (onError) onError(err); } + ); + schemaPickerService.listClonableCalls( + (resp) => { if (active) { clones = resp?.data?.calls || []; setClonableCalls(clones); } finish(); }, + (err) => { clones = []; finish(); } + ); + return () => { active = false; }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const canSubmit = () => { + if (!mode) return false; + if (mode === 'blank') return true; + if (mode === 'template') return !!pickedTemplateId; + if (mode === 'clone') return !!pickedSourceCallId; + return false; + }; + + const handleSubmit = () => { + const payload = { source: mode }; + if (mode === 'template') payload.template_id = pickedTemplateId; + if (mode === 'clone') payload.source_call_id = pickedSourceCallId; + + setSaving(true); + schemaPickerService.initializeSchema(callId, payload, + (resp) => { setSaving(false); if (onInitialized) onInitialized(resp?.data); }, + (err) => { setSaving(false); if (onError) onError(err); } + ); + }; + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + const templateOptions = templates + .filter(t => t.template_id !== 'blank') // blank ha card dedicata + .map(t => ({ label: t.label, value: t.template_id, description: t.description })); + + const cloneOptions = clonableCalls.map(c => ({ + label: `${c.name} · ${c.schema_status === 'PUBLISHED' ? __('Pubblicato','gepafin') : __('Bozza','gepafin')}`, + value: c.call_id, + })); + + const cardStyle = (selected) => ({ + cursor: 'pointer', + height: '100%', + border: selected ? '2px solid var(--primary-color)' : '2px solid transparent', + transition: 'border-color 0.15s' + }); + + return ( +
+
+

{__('Scegli come iniziare lo schema di rendicontazione','gepafin')}

+

+ {__("Puoi partire da un modello vuoto, usare un template predefinito oppure clonare lo schema di un altro bando. Lo schema creato sarà in bozza e modificabile liberamente.", 'gepafin')} +

+
+ +
+ {/* Card 1: NUOVO SCHEMA VUOTO */} +
+ {__('Nuovo schema','gepafin')}} + style={cardStyle(mode === 'blank')} + onClick={() => setMode('blank')} + > +

+ {__('Parti da zero con sezioni vuote. Configurerai categorie di spesa, documenti richiesti e controlli aggiuntivi secondo le esigenze del bando.','gepafin')} +

+
+ {__('Consigliato se il bando è nuovo e non somiglia a bandi precedenti.','gepafin')} +
+
+
+ + {/* Card 2: DA TEMPLATE */} +
+ {__('Da template','gepafin')}} + style={cardStyle(mode === 'template')} + onClick={() => setMode('template')} + > +

+ {__('Parti da un template predefinito che replica schemi di bandi noti (es. RE-START). Potrai comunque modificare tutto.','gepafin')} +

+ {mode === 'template' && ( +
e.stopPropagation()}> + setPickedTemplateId(e.value)} + options={templateOptions} + placeholder={__('Scegli un template...','gepafin')} + style={{ width: '100%' }} + /> + {pickedTemplateId && ( + + {templateOptions.find(t => t.value === pickedTemplateId)?.description} + + )} +
+ )} +
+
+ + {/* Card 3: CLONE */} +
+ {__('Clona da bando','gepafin')}} + style={cardStyle(mode === 'clone')} + onClick={() => setMode('clone')} + > +

+ {__('Copia lo schema di un altro bando (in bozza o pubblicato). Utile se il nuovo bando è molto simile a uno esistente.','gepafin')} +

+ {mode === 'clone' && ( +
e.stopPropagation()}> + {cloneOptions.length === 0 ? ( + + ) : ( + setPickedSourceCallId(e.value)} + options={cloneOptions} + placeholder={__('Scegli un bando sorgente...','gepafin')} + style={{ width: '100%' }} + /> + )} +
+ )} +
+
+
+ +
+
+
+ ); +}; + +export default SchemaTemplatePicker; diff --git a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js index 98e1589..1b9ed43 100644 --- a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js +++ b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js @@ -22,6 +22,7 @@ import BlockingOverlay from '../../../components/BlockingOverlay'; // api import RendicontazioneService from '../service/rendicontazioneService'; +import SchemaTemplatePicker from '../components/SchemaTemplatePicker'; import BandoService from '../../../service/bando-service'; // ---------- costanti ---------- @@ -343,15 +344,25 @@ const BandoRendicontazioneSchemaEdit = () => { )} {!schemaLoading && !hasSchema && ( -
- -

{__('Nessuno schema di rendicontazione per questo bando','gepafin')}

-

- {__('Puoi inizializzarlo con un template predefinito. Per ora è disponibile il template RE-START (fondo prestiti con remissione del debito).','gepafin')} -

-