From c52d0c8fd93f1ad485f4c4179fd32a176425f406 Mon Sep 17 00:00:00 2001 From: Vitalii Kiiko Date: Wed, 8 Apr 2026 10:57:16 +0200 Subject: [PATCH] - added import individual sheets; - fixed bugs reated to init of univerjs; --- src/assets/scss/components/formBuilder.scss | 6 +- .../FormField/components/Spreadsheet/index.js | 282 ++++++++++++++---- src/helpers/validators.js | 22 ++ src/pages/BandoApplication/index.js | 10 +- .../components/BandoEditFormStep3/index.js | 2 - .../components/ElementSetting/index.js | 3 +- .../ElementSettingSpreadsheet/index.js | 248 ++++++++++++--- .../BuilderElementSettings/index.js | 3 + src/pages/BandoFormsPreview/index.js | 2 + 9 files changed, 476 insertions(+), 102 deletions(-) diff --git a/src/assets/scss/components/formBuilder.scss b/src/assets/scss/components/formBuilder.scss index 0fc994f..91c873f 100644 --- a/src/assets/scss/components/formBuilder.scss +++ b/src/assets/scss/components/formBuilder.scss @@ -204,14 +204,14 @@ .formElementSettings__fieldDescription, .formElementSettings__fieldVarsList { padding: 15px; - background-color: #ffe0c5; + background-color: #fdf1e5; border: 1px solid #e6a973; - p { + p, li { margin: 0; color: #c68e5e; font-size: 15px; - line-height: 1.5; + line-height: 1.8; } code { diff --git a/src/components/FormField/components/Spreadsheet/index.js b/src/components/FormField/components/Spreadsheet/index.js index ccc04b9..72ec039 100644 --- a/src/components/FormField/components/Spreadsheet/index.js +++ b/src/components/FormField/components/Spreadsheet/index.js @@ -1,11 +1,28 @@ import React, { useEffect, useRef } from 'react'; +import { classNames } from 'primereact/utils'; import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets'; import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'; import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US'; import '@univerjs/preset-sheets-core/lib/index.css'; +import { __ } from '@wordpress/i18n'; + +// tools +import { xlsxToWorkbookData } from '../../../../pages/BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData'; const TAG_RE = /^\{\{gepafin_field:[^|]+\|[^}]+}}$/; +const BLOCKED_COMMANDS = new Set([ + 'sheet.command.set-range-values', + 'sheet.command.clear-selection-content', + 'sheet.command.clear-selection-all', + 'sheet.command.delete-range', + 'sheet.command.insert-range', + 'sheet.command.set-range-format', + 'sheet.command.insert-sheet', + 'sheet.command.delete-sheet', + 'sheet.command.rename-sheet', +]); + const buildTagMap = (workbookData) => { const map = {}; if (!workbookData?.sheets) return map; @@ -36,82 +53,131 @@ const parseWorkbook = (val) => { return null; }; -const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, config = {} }) => { +const Spreadsheet = ({ fieldName, label, errors = {}, defaultValue, setDataFn, template, register, config = {} }) => { const containerRef = useRef(null); const univerRef = useRef(null); const univerAPIRef = useRef(null); const saveTimerRef = useRef(null); const isRestoringRef = useRef(false); const tagCellMapRef = useRef({}); + const reinitializeRef = useRef(null); + const fileInputRef = useRef(null); + const addSheetsFileInputRef = useRef(null); useEffect(() => { if (!containerRef.current) return; - const { univer, univerAPI } = createUniver({ - locale: LocaleType.EN_US, - locales: { - [LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS), - }, - presets: [ - UniverSheetsCorePreset({ - container: containerRef.current, - }), - ], - }); + const templateData = parseWorkbook(template); + const gepafin = templateData?.gepafin ?? {}; + const editableSet = new Set(gepafin.editableCells ?? []); + const inputOnly = editableSet.size > 0; + const validationCellKeys = new Set((gepafin.validationCells ?? []).map(vc => vc.key ?? vc)); - univerRef.current = univer; - univerAPIRef.current = univerAPI; + const doInit = (workbookData) => { + if (univerRef.current) { + univerRef.current.dispose(); + univerRef.current = null; + univerAPIRef.current = null; + } - const initialData = parseWorkbook(defaultValue) || parseWorkbook(template) || { name: 'Sheet' }; - tagCellMapRef.current = buildTagMap(initialData); - univerAPI.createWorkbook(initialData); + const { univer, univerAPI } = createUniver({ + locale: LocaleType.EN_US, + locales: { + [LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS), + }, + presets: [ + UniverSheetsCorePreset({ + container: containerRef.current, + }), + ], + }); - const restoreTagCells = () => { - if (isRestoringRef.current) return; - const wb = univerAPIRef.current?.getActiveWorkbook(); - if (!wb) return; - const sheet = wb.getActiveSheet(); - if (!sheet) return; - const saved = wb.save(); - if (!saved?.sheets) return; - const sheetId = saved.sheetOrder?.[0]; - const sheetData = sheetId ? saved.sheets[sheetId] : Object.values(saved.sheets)[0]; - if (!sheetData?.cellData) return; + univerRef.current = univer; + univerAPIRef.current = univerAPI; - const restorations = []; - Object.entries(sheetData.cellData).forEach(([r, row]) => { - Object.entries(row).forEach(([c, cell]) => { - if (typeof cell.v === 'string' && TAG_RE.test(cell.v)) { - const orig = tagCellMapRef.current[`${r}:${c}`]; - if (orig) restorations.push({ r: +r, c: +c, v: orig.v, f: orig.f }); - } + tagCellMapRef.current = buildTagMap(workbookData); + univerAPI.createWorkbook(workbookData); + + // Apply visual markers for editable and validation cells in input-only mode + if (inputOnly && (editableSet.size > 0 || validationCellKeys.size > 0)) { + setTimeout(() => { + const wb = univerAPIRef.current?.getActiveWorkbook(); + const sheet = wb?.getActiveSheet(); + if (!sheet) return; + editableSet.forEach((key) => { + const [r, c] = key.split(':').map(Number); + sheet.getRange(r, c, 1, 1).setBackground('#dcfce7'); + }); + validationCellKeys.forEach((key) => { + const [r, c] = key.split(':').map(Number); + sheet.getRange(r, c, 1, 1).setBackground('#fef3c7'); + }); + }, 300); + } + + const restoreTagCells = () => { + if (isRestoringRef.current) return; + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (!wb) return; + const sheet = wb.getActiveSheet(); + if (!sheet) return; + const saved = wb.save(); + if (!saved?.sheets) return; + const sheetId = saved.sheetOrder?.[0]; + const sheetData = sheetId ? saved.sheets[sheetId] : Object.values(saved.sheets)[0]; + if (!sheetData?.cellData) return; + + const restorations = []; + Object.entries(sheetData.cellData).forEach(([r, row]) => { + Object.entries(row).forEach(([c, cell]) => { + if (typeof cell.v === 'string' && TAG_RE.test(cell.v)) { + const orig = tagCellMapRef.current[`${r}:${c}`]; + if (orig) restorations.push({ r: +r, c: +c, v: orig.v, f: orig.f }); + } + }); }); - }); - if (restorations.length === 0) return; - isRestoringRef.current = true; - restorations.forEach(({ r, c, v, f }) => { - sheet.getRange(r, c, 1, 1).setValues([[{ v, f }]]); + if (restorations.length === 0) return; + isRestoringRef.current = true; + restorations.forEach(({ r, c, v, f }) => { + sheet.getRange(r, c, 1, 1).setValues([[{ v, f }]]); + }); + isRestoringRef.current = false; + }; + + let restoreTimer; + univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => { + // Input-only mode: block write commands on non-editable cells + if (inputOnly && BLOCKED_COMMANDS.has(event.id)) { + const sheet = univerAPIRef.current?.getActiveWorkbook()?.getActiveSheet(); + const range = sheet?.getActiveRange(); + if (range) { + const key = `${range.getRow()}:${range.getColumn()}`; + if (!editableSet.has(key)) { + event.cancel = true; + return; + } + } + } + + clearTimeout(saveTimerRef.current); + saveTimerRef.current = setTimeout(() => { + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (wb) { + setDataFn(fieldName, wb.save(), { shouldValidate: true }); + } + }, 300); + clearTimeout(restoreTimer); + restoreTimer = setTimeout(restoreTagCells, 80); }); - isRestoringRef.current = false; }; - let restoreTimer; - univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, () => { - clearTimeout(saveTimerRef.current); - saveTimerRef.current = setTimeout(() => { - const wb = univerAPIRef.current?.getActiveWorkbook(); - if (wb) { - setDataFn(fieldName, wb.save(), { shouldValidate: true }); - } - }, 300); - clearTimeout(restoreTimer); - restoreTimer = setTimeout(restoreTagCells, 80); - }); + const initialData = parseWorkbook(defaultValue) || templateData || { name: 'Sheet' }; + doInit(initialData); + reinitializeRef.current = doInit; return () => { clearTimeout(saveTimerRef.current); - clearTimeout(restoreTimer); if (univerRef.current) { univerRef.current.dispose(); univerRef.current = null; @@ -132,11 +198,111 @@ const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, c register(fieldName, config); }, []); // eslint-disable-line react-hooks/exhaustive-deps + const handleImport = (e) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const data = xlsxToWorkbookData(ev.target.result); + reinitializeRef.current?.(data); + setTimeout(() => { + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (wb) setDataFn(fieldName, wb.save(), { shouldValidate: true }); + }, 400); + }; + reader.readAsArrayBuffer(file); + e.target.value = ''; + }; + + const handleImportAsSheets = (e) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const imported = xlsxToWorkbookData(ev.target.result); + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (!wb) return; + const current = wb.save(); + const existingIds = new Set(current.sheetOrder || []); + const mergedSheets = { ...(current.sheets || {}) }; + const mergedOrder = [...(current.sheetOrder || [])]; + (imported.sheetOrder || []).forEach((importedId) => { + let newId = importedId; + let counter = 0; + while (existingIds.has(newId)) { + counter++; + newId = `${importedId}_${counter}`; + } + existingIds.add(newId); + mergedSheets[newId] = { ...imported.sheets[importedId], id: newId }; + mergedOrder.push(newId); + }); + const merged = { ...current, sheets: mergedSheets, sheetOrder: mergedOrder }; + reinitializeRef.current?.(merged); + setTimeout(() => { + const wbAfter = univerAPIRef.current?.getActiveWorkbook(); + if (wbAfter) setDataFn(fieldName, wbAfter.save(), { shouldValidate: true }); + }, 400); + }; + reader.readAsArrayBuffer(file); + e.target.value = ''; + }; + + const templateData = parseWorkbook(template); + const inputOnly = (templateData?.gepafin?.editableCells?.length ?? 0) > 0; + return ( -
+ <> + {label + ? + : null} + {inputOnly && ( +
+ + {__('Modalità inserimento dati — modifica solo le celle evidenziate in verde', 'gepafin')} + +
+ )} + {!inputOnly && ( +
+ + + + +
+ )} +
+ ); }; diff --git a/src/helpers/validators.js b/src/helpers/validators.js index dcf06c7..f891ee4 100644 --- a/src/helpers/validators.js +++ b/src/helpers/validators.js @@ -66,6 +66,28 @@ export const maxChecks = (v, num) => { return is(Array, v) ? v.length <= parseInt(num) : false; } +export const createSpreadsheetValidator = (gepafin) => (workbookValue) => { + if (!gepafin || !workbookValue?.sheets) return true; + const sheetId = workbookValue.sheetOrder?.[0]; + const sheet = sheetId + ? workbookValue.sheets[sheetId] + : Object.values(workbookValue.sheets)[0]; + const cellData = sheet?.cellData ?? {}; + + const getCellValue = (key) => { + const [r, c] = key.split(':'); + return cellData[r]?.[c]?.v; + }; + + for (const { key, errorMessage } of (gepafin.validationCells ?? [])) { + const v = getCellValue(key); + if (v === undefined || v === null || v === 0 || v === false || v === 'FALSE' || v === '') { + return errorMessage || 'Validazione foglio di calcolo fallita'; + } + } + return true; +}; + export const nonEmptyTables = (v = [], tableCfg = []) => { const colsCfg = pathOr([], ['stateFieldData'], tableCfg); const nonPredefinedCells = colsCfg diff --git a/src/pages/BandoApplication/index.js b/src/pages/BandoApplication/index.js index e33c453..b1f0518 100644 --- a/src/pages/BandoApplication/index.js +++ b/src/pages/BandoApplication/index.js @@ -25,7 +25,7 @@ import { isEmail, isEmailPEC, isUrl, - isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables + isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables, createSpreadsheetValidator } from '../../helpers/validators'; import renderHtmlContent from '../../helpers/renderHtmlContent'; import set404FromErrorResponse from '../../helpers/set404FromErrorResponse'; @@ -761,6 +761,14 @@ const BandoApplication = () => { return acc; }, {}); + if (o.name === 'spreadsheet') { + const gepafin = template?.value?.gepafin; + if (gepafin?.validationCells?.length) { + if (!validations.validate) validations.validate = {}; + validations.validate.spreadsheetInputValid = createSpreadsheetValidator(gepafin); + } + } + /*if (o.name === 'fileselect') { console.log('options::', options) }*/ diff --git a/src/pages/BandoEdit/components/BandoEditFormStep3/index.js b/src/pages/BandoEdit/components/BandoEditFormStep3/index.js index 4c9412e..afc9dc9 100644 --- a/src/pages/BandoEdit/components/BandoEditFormStep3/index.js +++ b/src/pages/BandoEdit/components/BandoEditFormStep3/index.js @@ -95,7 +95,6 @@ const BandoEditFormStep3 = forwardRef(function () { const getFormsCallback = (resp) => { if (resp.status === 'SUCCESS') { const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'spreadsheet']); - console.log('resp.data', resp.data) const raw = (resp.data ?? []).flatMap(form => (form.content ?? []) .filter(f => !EXCLUDED_TYPES.has(f.name)) @@ -110,7 +109,6 @@ const BandoEditFormStep3 = forwardRef(function () { if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [], placeholder: f.placeholder }); byLabel.get(f.label).ids.push(f.id); }); - console.log('byLabel', Array.from(byLabel.values())) storeSet('callFormFields', Array.from(byLabel.values())); } } diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js index 02ccccf..5b817c8 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js @@ -26,7 +26,7 @@ import getTokens from '../../../../../../helpers/getTokens'; const ElementSettingSpreadsheet = React.lazy(() => import('../ElementSettingSpreadsheet')); -const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { +const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus, context }) => { const [existingVars, setExistingVars] = useState([]); const documentCategories = useStoreValue('documentCategories'); @@ -163,6 +163,7 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { ; } else if (setting.name === 'formula') { diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js index 52ebc16..aa7625d 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js @@ -14,7 +14,7 @@ import { xlsxToWorkbookData } from '../../../../../BandoEdit/components/BandoEdi const TAG_RE = /^\{\{gepafin_field:([^|]+)\|([^}]+)}}$/; -const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { +const ElementSettingSpreadsheet = ({ value, name, setDataFn, context }) => { const callFormFields = useStoreValue('callFormFields'); const [tagTooltip, setTagTooltip] = useState(null); const containerRef = useRef(null); @@ -27,6 +27,10 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { const taggedCellsRef = useRef(new Set()); const saveTimerRef = useRef(null); const setDataFnRef = useRef(setDataFn); + const gepafinMetaRef = useRef({ inputOnlyMode: false, editableCells: [], validationCells: [] }); + const contextRef = useRef(context); + const addSheetsFileInputRef = useRef(null); + const [inputOnlyActive, setInputOnlyActive] = useState(false); useEffect(() => { setDataFnRef.current = setDataFn; }, [setDataFn]); @@ -43,6 +47,70 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { range.setBackground('#dbeafe'); }; + const getActiveCellKey = () => { + const api = univerAPIRef.current; + if (!api) return null; + const range = api.getActiveWorkbook()?.getActiveSheet()?.getActiveRange(); + if (!range) return null; + return `${range.getRow()}:${range.getColumn()}`; + }; + + const saveWithMeta = useCallback(() => { + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (wb) { + const raw = wb.save(); + const meta = gepafinMetaRef.current; + // inputOnlyMode is derived: active whenever any editable cells are defined + setDataFnRef.current(name, { + ...raw, + gepafin: { ...meta, inputOnlyMode: meta.editableCells.length > 0 } + }); + } + }, [name]); + + const toggleEditableCell = useCallback(() => { + const key = getActiveCellKey(); + if (!key) return; + const meta = gepafinMetaRef.current; + const api = univerAPIRef.current; + const sheet = api?.getActiveWorkbook()?.getActiveSheet(); + const [r, c] = key.split(':').map(Number); + + if (meta.editableCells.includes(key)) { + meta.editableCells = meta.editableCells.filter(k => k !== key); + sheet?.getRange(r, c, 1, 1).setBackground(''); + } else { + meta.editableCells = [...meta.editableCells, key]; + sheet?.getRange(r, c, 1, 1).setBackground('#dcfce7'); + } + setInputOnlyActive(meta.editableCells.length > 0); + saveWithMeta(); + }, [saveWithMeta]); + + const toggleValidationCell = useCallback(() => { + const key = getActiveCellKey(); + if (!key) return; + const meta = gepafinMetaRef.current; + const api = univerAPIRef.current; + const sheet = api?.getActiveWorkbook()?.getActiveSheet(); + const [r, c] = key.split(':').map(Number); + + const existing = meta.validationCells.find(vc => (vc.key ?? vc) === key); + if (existing) { + meta.validationCells = meta.validationCells.filter(vc => (vc.key ?? vc) !== key); + sheet?.getRange(r, c, 1, 1).setBackground(''); + } else { + // eslint-disable-next-line no-alert + const errorMessage = window.prompt( + __('Messaggio di errore di validazione (lascia vuoto per il default):', 'gepafin'), + '' + ) ?? ''; + meta.validationCells = [...meta.validationCells, { key, errorMessage: errorMessage || 'Validazione foglio di calcolo fallita' }]; + sheet?.getRange(r, c, 1, 1).setBackground('#fef3c7'); + } + saveWithMeta(); + }, [saveWithMeta]); + const initializeUniver = useCallback((workbookData) => { if (univerRef.current) { univerRef.current.dispose(); @@ -52,6 +120,12 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { if (!containerRef.current) return; + // Initialise gepafin metadata from workbook + gepafinMetaRef.current = workbookData?.gepafin + ? { inputOnlyMode: false, editableCells: [], validationCells: [], ...workbookData.gepafin } + : { inputOnlyMode: false, editableCells: [], validationCells: [] }; + setInputOnlyActive(gepafinMetaRef.current.editableCells.length > 0); + const { univer, univerAPI } = createUniver({ locale: LocaleType.EN_US, locales: { @@ -84,6 +158,23 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { } taggedCellsRef.current = initialTagged; + // Re-apply gepafin cell background markers after a short delay (Univer needs to settle) + setTimeout(() => { + const wb = univerAPIRef.current?.getActiveWorkbook(); + const sheet = wb?.getActiveSheet(); + if (!sheet) return; + const meta = gepafinMetaRef.current; + meta.editableCells.forEach((key) => { + const [r, c] = key.split(':').map(Number); + sheet.getRange(r, c, 1, 1).setBackground('#dcfce7'); + }); + meta.validationCells.forEach((vc) => { + const key = vc.key ?? vc; + const [r, c] = key.split(':').map(Number); + sheet.getRange(r, c, 1, 1).setBackground('#fef3c7'); + }); + }, 300); + // Restore tag cells whose display value was overwritten by raw tag on blur; // also clear background for cells whose tag formula was removed. const restoreTagCells = () => { @@ -152,48 +243,66 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { let restoreTimer; - // Disable adding new sheets; auto-save on every command; schedule tag restoration + // Auto-save on every command; schedule tag restoration univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => { - if (event.id === 'sheet.command.insert-sheet') { - event.cancel = true; - } clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(() => { const wb = univerAPIRef.current?.getActiveWorkbook(); - if (wb) setDataFnRef.current(name, wb.save()); + if (wb) { + const raw = wb.save(); + const m = gepafinMetaRef.current; + setDataFnRef.current(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } }); + } }, 300); clearTimeout(restoreTimer); restoreTimer = setTimeout(restoreTagCells, 80); }); - // Context menu: "Inserisci variabile GEPAFIN" - const fields = formFieldsRef.current; - if (fields.length > 0) { - const submenu = univerAPI.createSubmenu({ - id: 'gepafin-insert-variable', - title: 'Inserisci variabile GEPAFIN', - }); + // Context menu: "Inserisci variabile GEPAFIN" — only for evaluation form builder (context="call") + if (contextRef.current === 'call') { + const fields = formFieldsRef.current; + if (fields.length > 0) { + const submenu = univerAPI.createSubmenu({ + id: 'gepafin-insert-variable', + title: 'Inserisci variabile GEPAFIN', + }); - fields.forEach((field, index) => { - if (index > 0) submenu.addSeparator(); + fields.forEach((field, index) => { + if (index > 0) submenu.addSeparator(); - const menuKey = field.ids[0]; - const countLabel = field.ids.length > 1 ? ` [${field.ids.length}]` : ''; + const menuKey = field.ids[0]; + const countLabel = field.ids.length > 1 ? ` [${field.ids.length}]` : ''; - submenu.addSubmenu(univerAPI.createMenu({ - id: `gepafin-field-${menuKey}-label`, - title: `${field.label}${countLabel} (etichetta)`, - action: () => insertFieldTag(field.ids, 'label', field), - })); + submenu.addSubmenu(univerAPI.createMenu({ + id: `gepafin-field-${menuKey}-label`, + title: `${field.label}${countLabel} (etichetta)`, + action: () => insertFieldTag(field.ids, 'label', field), + })); - submenu.addSubmenu(univerAPI.createMenu({ - id: `gepafin-field-${menuKey}-value`, - title: `${field.label}${countLabel} (valore)`, - action: () => insertFieldTag(field.ids, 'value', field), - })); - }); + submenu.addSubmenu(univerAPI.createMenu({ + id: `gepafin-field-${menuKey}-value`, + title: `${field.label}${countLabel} (valore)`, + action: () => insertFieldTag(field.ids, 'value', field), + })); + }); - submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']); + submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']); + } + } + + // Context menu: input-only mode management — only for regular form builder (context="application") + if (contextRef.current === 'application') { + univerAPI.createMenu({ + id: 'gepafin-toggle-editable-cell', + title: 'Modalità inserimento: segna/rimuovi cella modificabile', + action: toggleEditableCell, + }).appendTo(['contextMenu.mainArea', 'contextMenu.others']); + + univerAPI.createMenu({ + id: 'gepafin-toggle-validation-cell', + title: 'Modalità inserimento: segna/rimuovi cella di validazione', + action: toggleValidationCell, + }).appendTo(['contextMenu.mainArea', 'contextMenu.others']); } // Tooltip on hover over tagged cells — read formula field to detect tag @@ -213,7 +322,7 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { } }); - }, []); // refs are stable + }, [toggleEditableCell, toggleValidationCell]); // refs are stable // Keep formFieldsRef in sync with store value; rebuild Univer (and its context menu) // once fields arrive, because the menu is constructed at init time from formFieldsRef.current. @@ -222,7 +331,11 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { formFieldsRef.current = callFormFields; if (callFormFields.length > 0 && univerAPIRef.current) { const currentWorkbook = univerAPIRef.current.getActiveWorkbook()?.save() ?? null; - initializeUniver(currentWorkbook); + // Preserve gepafin metadata across reinit + const withMeta = currentWorkbook + ? { ...currentWorkbook, gepafin: gepafinMetaRef.current } + : null; + initializeUniver(withMeta); } }, [callFormFields, initializeUniver]); @@ -242,8 +355,6 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { }, []); // run once on mount // Flush workbook data to parent immediately when the user clicks outside the spreadsheet. - // This runs in the capture phase — before the clicked element's own handler — so the - // parent's store is up to date by the time any outer "Salva" button handler reads it. useEffect(() => { const container = containerRef.current; if (!container) return; @@ -251,11 +362,15 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { if (container.contains(e.target)) return; clearTimeout(saveTimerRef.current); const wb = univerAPIRef.current?.getActiveWorkbook(); - if (wb) setDataFnRef.current(name, wb.save()); + if (wb) { + const raw = wb.save(); + const m = gepafinMetaRef.current; + setDataFnRef.current(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } }); + } }; document.addEventListener('click', flushOnExternalClick, true); return () => document.removeEventListener('click', flushOnExternalClick, true); - }, [name]); // name is stable ('template') + }, [name]); // Prevent page scroll while hovering over the spreadsheet useEffect(() => { @@ -278,13 +393,56 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { e.target.value = ''; }; + const handleImportAsSheets = (e) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const imported = xlsxToWorkbookData(ev.target.result); + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (!wb) return; + const current = wb.save(); + const meta = gepafinMetaRef.current; + const existingIds = new Set(current.sheetOrder || []); + const mergedSheets = { ...(current.sheets || {}) }; + const mergedOrder = [...(current.sheetOrder || [])]; + (imported.sheetOrder || []).forEach((importedId) => { + let newId = importedId; + let counter = 0; + while (existingIds.has(newId)) { + counter++; + newId = `${importedId}_${counter}`; + } + existingIds.add(newId); + mergedSheets[newId] = { ...imported.sheets[importedId], id: newId }; + mergedOrder.push(newId); + }); + initializeUniver({ ...current, sheets: mergedSheets, sheetOrder: mergedOrder, gepafin: meta }); + }; + reader.readAsArrayBuffer(file); + e.target.value = ''; + }; + const handleSave = () => { - const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? {}; - setDataFn(name, workbookData); + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (wb) { + const raw = wb.save(); + const m = gepafinMetaRef.current; + setDataFn(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } }); + } }; return (
+ {context === 'application' &&
+

{__('Celle modificabili (verde)', 'gepafin')} — {__('Clicca destro su una cella e scegli "Segna/rimuovi cella modificabile" per definire quali celle il beneficiario potrà compilare. La modalità inserimento dati si attiva automaticamente non appena viene definita almeno una cella modificabile.', 'gepafin')}

+

{__('Celle di validazione (ambra)', 'gepafin')} — {__('Segna una cella come "cella di validazione" e inserisci una formula che restituisce TRUE/FALSE. Se il risultato non è vero al momento dell\'invio, il campo viene considerato non valido. Esempi di formule utili:', 'gepafin')}

+
    +
  • =NOT(ISBLANK(A1)) — {__('cella A1 obbligatoria', 'gepafin')}
  • +
  • =AND(A1>0, A1<100) — {__('valore numerico tra 0 e 100', 'gepafin')}
  • +
  • =LEN(TRIM(B2))>0 — {__('testo non vuoto in B2', 'gepafin')}
  • +
+
}
{ style={{ display: 'none' }} onChange={handleImport} /> + +