diff --git a/package.json b/package.json index 21d670b..a55fc51 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "@sentry/browser": "9.11.0", "@stomp/stompjs": "7.1.1", "@tanstack/react-table": "8.21.2", + "@univerjs/preset-docs-core": "^0.18.0", + "@univerjs/preset-sheets-core": "^0.18.0", + "@univerjs/presets": "^0.18.0", "@wordpress/i18n": "5.21.0", "@wordpress/react-i18n": "4.21.0", "codice-fiscale-js": "2.3.22", @@ -42,6 +45,7 @@ "recharts": "2.15.2", "sockjs-client": "1.6.1", "validate.js": "0.13.1", + "xlsx": "^0.18.5", "zustand": "5.0.6", "zustand-x": "6.1.1" }, @@ -88,4 +92,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/src/assets/scss/components/formBuilder.scss b/src/assets/scss/components/formBuilder.scss index 23fcb37..0fc994f 100644 --- a/src/assets/scss/components/formBuilder.scss +++ b/src/assets/scss/components/formBuilder.scss @@ -176,6 +176,19 @@ width: 40rem; } +.formBuilder__elementSettings--wide { + width: 90%; +} + +// Univer renders context menus / popups with position:fixed and z-index:1070, +// but PrimeReact Sidebar (modal type) sits at z-index ~1100, hiding them. +// Raise all Univer popup/floating layers above the sidebar. +.univer-popup, +.univer-z-\[1020\], +.univer-z-\[1080\] { + z-index: 9999 !important; +} + .formElementSettings { display: flex; flex-direction: column; diff --git a/src/components/FormField/components/Spreadsheet/index.js b/src/components/FormField/components/Spreadsheet/index.js new file mode 100644 index 0000000..7b9343f --- /dev/null +++ b/src/components/FormField/components/Spreadsheet/index.js @@ -0,0 +1,84 @@ +import React, { useEffect, useRef } from 'react'; +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'; + +const parseWorkbook = (val) => { + if (!val) return null; + if (typeof val === 'object' && val.sheets) return val; + if (typeof val === 'string') { + try { + const parsed = JSON.parse(val); + return parsed && parsed.sheets ? parsed : null; + } catch { + return null; + } + } + return null; +}; + +const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template }) => { + const containerRef = useRef(null); + const univerRef = useRef(null); + const univerAPIRef = useRef(null); + const saveTimerRef = 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, + }), + ], + }); + + univerRef.current = univer; + univerAPIRef.current = univerAPI; + + const initialData = parseWorkbook(defaultValue) || parseWorkbook(template) || { name: 'Sheet' }; + univerAPI.createWorkbook(initialData); + + univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, () => { + clearTimeout(saveTimerRef.current); + saveTimerRef.current = setTimeout(() => { + const wb = univerAPIRef.current?.getActiveWorkbook(); + if (wb) { + setDataFn(fieldName, wb.save()); + } + }, 300); + }); + + return () => { + clearTimeout(saveTimerRef.current); + if (univerRef.current) { + univerRef.current.dispose(); + univerRef.current = null; + univerAPIRef.current = null; + } + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const handler = (e) => { e.stopPropagation(); e.preventDefault(); }; + el.addEventListener('wheel', handler, { passive: false }); + return () => el.removeEventListener('wheel', handler); + }, []); + + return ( +
+ ); +}; + +export default Spreadsheet; diff --git a/src/components/FormField/index.js b/src/components/FormField/index.js index 67f7ba9..5688f4f 100644 --- a/src/components/FormField/index.js +++ b/src/components/FormField/index.js @@ -19,6 +19,7 @@ import Table from './components/Table'; import PasswordField from './components/PasswordField'; import CriteriaTable from './components/CriteriaTable'; import FileSelect from './components/FileSelect'; +import Spreadsheet from './components/Spreadsheet'; const FormField = (props) => { const fields = { @@ -38,6 +39,7 @@ const FormField = (props) => { criteria_table: CriteriaTable, password: PasswordField, fileselect: FileSelect, + spreadsheet: Spreadsheet, } const Comp = !isNil(fields[props.type]) ? fields[props.type] : null; diff --git a/src/pages/BandoApplication/index.js b/src/pages/BandoApplication/index.js index b154dad..e33c453 100644 --- a/src/pages/BandoApplication/index.js +++ b/src/pages/BandoApplication/index.js @@ -730,6 +730,7 @@ const BandoApplication = () => { if (!tableColumns) { tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns')); } + const template = head(o.settings.filter(s => s.name === 'template')); const step = head(o.settings.filter(o => o.name === 'step')); const mime = head(o.settings.filter(o => o.name === 'mime')); const documentCategories = head(o.settings.filter(o => o.name === 'documentCategories')); @@ -794,6 +795,7 @@ const BandoApplication = () => { sourceId={getApplicationId()} useGrouping={false} tableColumns={tableColumns ? tableColumns.value : {}} + template={template ? template.value : null} /> }) : null} diff --git a/src/pages/BandoApplicationPreview/index.js b/src/pages/BandoApplicationPreview/index.js index ae9baac..ff459d4 100644 --- a/src/pages/BandoApplicationPreview/index.js +++ b/src/pages/BandoApplicationPreview/index.js @@ -378,6 +378,7 @@ const BandoApplicationPreview = () => { if (!tableColumns) { tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns')); } + const template = head(o.settings.filter(s => s.name === 'template')); const step = head(o.settings.filter(o => o.name === 'step')); const mime = head(o.settings.filter(o => o.name === 'mime')); const formula = head(o.settings.filter(o => o.name === 'formula')); @@ -435,6 +436,7 @@ const BandoApplicationPreview = () => { sourceId={getApplicationId()} useGrouping={false} tableColumns={tableColumns ? tableColumns.value : {}} + template={template ? template.value : null} /> })} diff --git a/src/pages/BandoEdit/components/BandoEditFormStep3/index.js b/src/pages/BandoEdit/components/BandoEditFormStep3/index.js index dbd398e..d5c40ea 100644 --- a/src/pages/BandoEdit/components/BandoEditFormStep3/index.js +++ b/src/pages/BandoEdit/components/BandoEditFormStep3/index.js @@ -92,6 +92,29 @@ const BandoEditFormStep3 = forwardRef(function () { storeSet('unsetAsyncRequest'); } + const getFormsCallback = (resp) => { + if (resp.status === 'SUCCESS') { + const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'table', 'criteria_table', 'spreadsheet']); + const raw = (resp.data ?? []).flatMap(form => + (form.content ?? []) + .filter(f => !EXCLUDED_TYPES.has(f.name)) + .map(f => ({ + id: f.id, + label: f.label, + placeholder: f.settings?.find(s => s.name === 'placeholder')?.value ?? '' + })) + ); + const byLabel = new Map(); + raw.forEach(f => { + if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [], placeholder: f.placeholder }); + byLabel.get(f.label).ids.push(f.id); + }); + storeSet('callFormFields', Array.from(byLabel.values())); + } + } + + const errGetFormsCallback = () => {} + const getFormCallback = (resp) => { if (resp.status === 'SUCCESS') { storeSet('formId', resp.data.id); @@ -117,12 +140,14 @@ const BandoEditFormStep3 = forwardRef(function () { useEffect(() => { storeSet('setAsyncRequest'); FormsService.getElementItems(getElementItemsCallback, errGetElementItemsCallbacks); + FormsService.getFormsForCall(id, getFormsCallback, errGetFormsCallback); return () => { storeSet('formId', 0); storeSet('formElements', []); storeSet('activeElement', ''); storeSet('selectedElement', ''); + storeSet('callFormFields', []); } }, []); diff --git a/src/pages/BandoEdit/components/BandoEditFormStep3Excel/index.js b/src/pages/BandoEdit/components/BandoEditFormStep3Excel/index.js new file mode 100644 index 0000000..e06f777 --- /dev/null +++ b/src/pages/BandoEdit/components/BandoEditFormStep3Excel/index.js @@ -0,0 +1,338 @@ +import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; +import { __ } from '@wordpress/i18n'; +import { useNavigate, useParams } from 'react-router-dom'; +import { klona } from 'klona'; +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'; + +// api +import EvaluationFormsService from '../../../../service/evaluation-forms-service'; +import FormsService from '../../../../service/forms-service'; + +// store +import { storeGet, storeSet } from '../../../../store'; + +// tools +import set404FromErrorResponse from '../../../../helpers/set404FromErrorResponse'; +import { xlsxToWorkbookData } from './xlsxToWorkbookData'; + +// components +import BandoEditFormActions from '../BandoEditFormActions'; +import { Toast } from 'primereact/toast'; + +const BandoEditFormStep3Excel = forwardRef(function () { + const navigate = useNavigate(); + const { id } = useParams(); + const [formName, setFormName] = useState(''); + const [bandoStatus, setBandoStatus] = useState(''); + const [tagTooltip, setTagTooltip] = useState(null); // { label } | null + const toast = useRef(null); + const univerRef = useRef(null); + const containerRef = useRef(null); + const univerAPIRef = useRef(null); + const formFieldsRef = useRef([]); + const fieldsLoadedRef = useRef(false); + const pendingWorkbookRef = useRef(undefined); // undefined = eval form not yet returned, null = returned with no workbook + const mousePos = useRef({ x: 0, y: 0 }); + const fileInputRef = useRef(null); + + const getBandoId = () => { + const parsed = parseInt(id) + return !isNaN(parsed) ? parsed : 0; + } + + const onSaveDraft = () => { + const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? null; + const formId = storeGet('formId'); + const formData = { + label: formName, + content: workbookData, + } + + storeSet('setAsyncRequest'); + EvaluationFormsService.updateForm(formId, formData, updateFormCallback, errUpdateFormCallback) + } + + const updateFormCallback = (resp) => { + if (resp.status === 'SUCCESS') { + setBandoStatus(resp.data.callStatus); + if (toast.current) { + toast.current.show({ + severity: 'success', + summary: '', + detail: __('Il bando è stato aggiornato correttamente!', 'gepafin') + }); + } + } + storeSet('unsetAsyncRequest'); + } + + const errUpdateFormCallback = (resp) => { + set404FromErrorResponse(resp); + storeSet('unsetAsyncRequest'); + } + + const openPreview = () => { + const bandoId = getBandoId(); + navigate(`/bandi/${bandoId}/preview`); + } + + const openPreviewEvaluation = () => { + const bandoId = getBandoId(); + navigate(`/bandi/${bandoId}/preview-evaluation`); + } + + const getFormCallback = (resp) => { + if (resp.status === 'SUCCESS') { + storeSet('formId', resp.data.id); + storeSet('formLabel', resp.data.label); + setFormName(resp.data.label); + setBandoStatus(resp.data.callStatus); + const content = resp.data.content ? klona(resp.data.content) : null; + storeSet('formElements', content); + const workbook = content && content.sheets ? content : null; + if (fieldsLoadedRef.current) { + initializeUniver(workbook); + } else { + pendingWorkbookRef.current = workbook; // null or workbook; signals eval form returned + } + } else { + if (!fieldsLoadedRef.current) { + pendingWorkbookRef.current = null; // eval form done, no workbook + } + } + storeSet('unsetAsyncRequest'); + } + + const getFormsCallback = (resp) => { + if (resp.status === 'SUCCESS') { + const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'table', 'criteria_table']); + const raw = (resp.data ?? []).flatMap(form => + (form.content ?? []) + .filter(f => !EXCLUDED_TYPES.has(f.name)) + .map(f => ({ id: f.id, label: f.label })) + ); + // Group by label: same label across forms → one entry with multiple ids + const byLabel = new Map(); + raw.forEach(f => { + if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [] }); + byLabel.get(f.label).ids.push(f.id); + }); + formFieldsRef.current = Array.from(byLabel.values()); + } + fieldsLoadedRef.current = true; + if (pendingWorkbookRef.current !== undefined) { + // eval form already returned; initialize with its workbook (may be null) + initializeUniver(pendingWorkbookRef.current); + pendingWorkbookRef.current = undefined; + } + // else: eval form hasn't returned yet; getFormCallback will trigger initializeUniver + } + + const errGetFormsCallback = () => { + fieldsLoadedRef.current = true; + if (pendingWorkbookRef.current !== undefined) { + initializeUniver(pendingWorkbookRef.current); + pendingWorkbookRef.current = undefined; + } + } + + const errGetFormCallback = (resp) => { + set404FromErrorResponse(resp); + if (!fieldsLoadedRef.current) { + pendingWorkbookRef.current = null; + } + storeSet('unsetAsyncRequest'); + } + + useEffect(() => { + storeSet('setAsyncRequest'); + EvaluationFormsService.getFormForCall(id, getFormCallback, errGetFormCallback) + }, [id]); + + useEffect(() => { + FormsService.getFormsForCall(id, getFormsCallback, errGetFormsCallback); + }, [id]); + + const insertFieldTag = (ids, type) => { + const api = univerAPIRef.current; + if (!api) return; + const range = api.getActiveWorkbook()?.getActiveSheet()?.getActiveRange(); + if (!range) return; + range.setValue(`{{gepafin_field:${ids.join(',')}|${type}}}`); + range.setBackground('#dbeafe'); + }; + + const initializeUniver = useCallback((workbookData) => { + if (univerRef.current) { + univerRef.current.dispose(); + univerRef.current = null; + univerAPIRef.current = null; + } + + if (!containerRef.current) return; + + const { univer, univerAPI } = createUniver({ + locale: LocaleType.EN_US, + locales: { + [LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS), + }, + presets: [ + UniverSheetsCorePreset({ + container: containerRef.current, + }), + ], + }); + + univerAPI.createWorkbook(workbookData || { name: 'Sheet' }); + univerRef.current = univer; + univerAPIRef.current = univerAPI; + + // Disable adding new sheets + univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => { + if (event.id === 'sheet.command.insert-sheet') { + event.cancel = true; + } + }); + + // Context menu: "Inserisci variabile GEPAFIN" with per-field submenus + const fields = formFieldsRef.current; + const submenu = univerAPI.createSubmenu({ + id: 'gepafin-insert-variable', + title: 'Inserisci variabile GEPAFIN', + }); + + fields.forEach((field, index) => { + if (index > 0) submenu.addSeparator(); + + 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'), + })); + + submenu.addSubmenu(univerAPI.createMenu({ + id: `gepafin-field-${menuKey}-value`, + title: `${field.label}${countLabel} (valore)`, + action: () => insertFieldTag(field.ids, 'value'), + })); + }); + + submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']); + + // Tooltip on hover over tagged cells + univerAPI.addEvent(univerAPI.Event.CellHover, (params) => { + const { row, column, worksheet } = params; + const cellValue = worksheet?.getRange(row, column)?.getValue?.(); + if (typeof cellValue === 'string' && cellValue.startsWith('{{gepafin_field:')) { + const match = cellValue.match(/\{\{gepafin_field:([^|]+)\|([^}]+)\}\}/); + if (match) { + const tagIds = match[1].split(','); + const field = formFieldsRef.current.find(f => f.ids.some(id => tagIds.includes(id))); + const countSuffix = tagIds.length > 1 ? ` — ${tagIds.length} moduli` : ''; + setTagTooltip({ label: `${field ? field.label : match[1]} (${match[2]})${countSuffix}` }); + } + } else { + setTagTooltip(null); + } + }); + }, []); // containerRef, formFieldsRef, univerRef, univerAPIRef are stable refs + + useEffect(() => { + return () => { + if (univerRef.current) { + univerRef.current.dispose(); + univerRef.current = null; + univerAPIRef.current = null; + } + }; + }, []); + + // Prevent page scroll while hovering over the spreadsheet + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const handler = (e) => { e.stopPropagation(); e.preventDefault(); }; + el.addEventListener('wheel', handler, { passive: false }); + return () => el.removeEventListener('wheel', handler); + }, []); + + 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); + initializeUniver(data); + }; + reader.readAsArrayBuffer(file); + e.target.value = ''; // reset so same file can be re-imported + }; + + return ( +
+
+
+ + +
+
{ mousePos.current = { x: e.clientX, y: e.clientY }; }} + onMouseLeave={() => setTagTooltip(null)} + /> + {tagTooltip && ( +
+ {tagTooltip.label} +
+ )} +
+ +
+ +
+ {__('Azioni', 'gepafin')} +
+ + + +
+ ) +}) + +export default BandoEditFormStep3Excel; diff --git a/src/pages/BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData.js b/src/pages/BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData.js new file mode 100644 index 0000000..a3311bb --- /dev/null +++ b/src/pages/BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData.js @@ -0,0 +1,58 @@ +import * as XLSX from 'xlsx'; + +export function xlsxToWorkbookData(arrayBuffer) { + const wb = XLSX.read(arrayBuffer, { type: 'array', cellStyles: true, sheetStubs: true }); + + const sheets = {}; + const sheetOrder = []; + + wb.SheetNames.forEach((sheetName, sheetIndex) => { + const ws = wb.Sheets[sheetName]; + const sheetId = `sheet_${sheetIndex}`; + sheetOrder.push(sheetId); + + const cellData = {}; + const ref = ws['!ref']; + if (!ref) { + sheets[sheetId] = { id: sheetId, name: sheetName, cellData }; + return; + } + + const range = XLSX.utils.decode_range(ref); + for (let r = range.s.r; r <= range.e.r; r++) { + for (let c = range.s.c; c <= range.e.c; c++) { + const cellAddress = XLSX.utils.encode_cell({ r, c }); + const cell = ws[cellAddress]; + if (!cell) continue; + + // CellValueType: 1=STRING, 2=NUMBER, 3=BOOLEAN + let t; + if (cell.t === 'n') t = 2; + else if (cell.t === 'b') t = 3; + else t = 1; + + if (!cellData[r]) cellData[r] = {}; + cellData[r][c] = { v: cell.v !== undefined ? cell.v : (cell.w || ''), t }; + } + } + + const mergeData = (ws['!merges'] || []).map(m => ({ + startRow: m.s.r, + startColumn: m.s.c, + endRow: m.e.r, + endColumn: m.e.c, + })); + + sheets[sheetId] = { id: sheetId, name: sheetName, cellData, mergeData }; + }); + + return { + id: 'imported-workbook', + name: wb.SheetNames[0] || 'Sheet', + appVersion: '0.18.0', + locale: 'enUS', + styles: {}, + sheetOrder, + sheets, + }; +} diff --git a/src/pages/BandoEdit/index.js b/src/pages/BandoEdit/index.js index ea9165c..3204ee2 100644 --- a/src/pages/BandoEdit/index.js +++ b/src/pages/BandoEdit/index.js @@ -23,8 +23,8 @@ import BandoEditFormStep2 from './components/BandoEditFormStep2'; import { Messages } from 'primereact/messages'; import BlockingOverlay from '../../components/BlockingOverlay'; import { Toast } from 'primereact/toast'; -import BandoEditFormStep3 from './components/BandoEditFormStep3'; import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup'; +import BandoEditFormStep3 from './components/BandoEditFormStep3'; const BandoEdit = () => { const isAsyncRequest = useStoreValue('isAsyncRequest'); diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js index 0b7eb31..d167688 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js @@ -18,6 +18,7 @@ import { InputSwitch } from 'primereact/inputswitch'; import ElementSettingChips from '../ElementSettingChips'; import ElementSettingCriteriaTableColumns from '../ElementSettingCriteriaTableColumns'; import ElementSettingTableColumnsForCsv from '../ElementSettingTableColumnsForCsv'; +import ElementSettingSpreadsheet from '../ElementSettingSpreadsheet'; import { mimeTypes } from '../../../../../../configData'; import ElementSettingReportHeader from '../ElementSettingReportHeader'; @@ -47,7 +48,8 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { isChecklistItem: __('Fa parte di "checklist"?', 'gepafin'), reportEnable: __('Aggiungere nel report CSV?', 'gepafin'), reportHeader: __('Nome della colonna nel CSV', 'gepafin'), - reportColumns: __('', 'gepafin') + reportColumns: __('', 'gepafin'), + template: __('Template foglio', 'gepafin') } const settingDescription = { @@ -156,6 +158,11 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { name={setting.name} bandoStatus={bandoStatus} setDataFn={updateDataFn}/> + } else if (setting.name === 'template') { + return ; } else if (setting.name === 'formula') { const isInvalid = invalidFormula(setting.value); return <> diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js new file mode 100644 index 0000000..010b3c9 --- /dev/null +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingSpreadsheet/index.js @@ -0,0 +1,215 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { __ } from '@wordpress/i18n'; +import { isEmpty } from 'ramda'; +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'; + +// store +import { useStoreValue } from '../../../../../../store'; + +// tools +import { xlsxToWorkbookData } from '../../../../../BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData'; + +const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => { + const callFormFields = useStoreValue('callFormFields'); + const [tagTooltip, setTagTooltip] = useState(null); + const containerRef = useRef(null); + const univerRef = useRef(null); + const univerAPIRef = useRef(null); + const formFieldsRef = useRef([]); + const mousePos = useRef({ x: 0, y: 0 }); + const fileInputRef = useRef(null); + + const insertFieldTag = (ids, type, field) => { + const api = univerAPIRef.current; + if (!api) return; + const range = api.getActiveWorkbook()?.getActiveSheet()?.getActiveRange(); + if (!range) return; + const displayValue = type === 'value' ? (field.placeholder || '') : field.label; + range.setValues([[{ + v: displayValue, + f: `{{gepafin_field:${ids.join(',')}|${type}}}` + }]]); + range.setBackground('#dbeafe'); + }; + + const initializeUniver = useCallback((workbookData) => { + if (univerRef.current) { + univerRef.current.dispose(); + univerRef.current = null; + univerAPIRef.current = null; + } + + if (!containerRef.current) return; + + const { univer, univerAPI } = createUniver({ + locale: LocaleType.EN_US, + locales: { + [LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS), + }, + presets: [ + UniverSheetsCorePreset({ + container: containerRef.current, + }), + ], + }); + + univerAPI.createWorkbook(workbookData || { name: 'Sheet' }); + univerRef.current = univer; + univerAPIRef.current = univerAPI; + + // Disable adding new sheets + univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => { + if (event.id === 'sheet.command.insert-sheet') { + event.cancel = true; + } + }); + + // 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', + }); + + fields.forEach((field, index) => { + if (index > 0) submenu.addSeparator(); + + 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}-value`, + title: `${field.label}${countLabel} (valore)`, + action: () => insertFieldTag(field.ids, 'value', field), + })); + }); + + submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']); + } + + // Tooltip on hover over tagged cells — read formula field to detect tag + univerAPI.addEvent(univerAPI.Event.CellHover, (params) => { + const { row, column, worksheet } = params; + const formula = worksheet?.getRange(row, column)?.getFormulas()?.[0]?.[0]; + if (typeof formula === 'string' && formula.startsWith('{{gepafin_field:')) { + const match = formula.match(/\{\{gepafin_field:([^|]+)\|([^}]+)\}\}/); + if (match) { + const tagIds = match[1].split(','); + const field = formFieldsRef.current.find(f => f.ids.some(fid => tagIds.includes(fid))); + const countSuffix = tagIds.length > 1 ? ` — ${tagIds.length} moduli` : ''; + setTagTooltip({ label: `${field ? field.label : match[1]} (${match[2]})${countSuffix}` }); + } + } else { + setTagTooltip(null); + } + }); + }, []); // refs are stable + + // Keep formFieldsRef in sync with store value + useEffect(() => { + formFieldsRef.current = callFormFields; + }, [callFormFields]); + + // Initialize Univer on mount + useEffect(() => { + const workbook = value && !isEmpty(value) && value.sheets ? value : null; + initializeUniver(workbook); + + return () => { + if (univerRef.current) { + univerRef.current.dispose(); + univerRef.current = null; + univerAPIRef.current = null; + } + }; + }, []); // run once on mount + + // Prevent page scroll while hovering over the spreadsheet + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const handler = (e) => { e.stopPropagation(); e.preventDefault(); }; + el.addEventListener('wheel', handler, { passive: false }); + return () => el.removeEventListener('wheel', handler); + }, []); + + 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); + initializeUniver(data); + }; + reader.readAsArrayBuffer(file); + e.target.value = ''; + }; + + const handleSave = () => { + const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? {}; + setDataFn(name, workbookData); + }; + + return ( +
+
+ + + +
+
{ mousePos.current = { x: e.clientX, y: e.clientY }; }} + onMouseLeave={() => setTagTooltip(null)} + /> + {tagTooltip && ( +
+ {tagTooltip.label} +
+ )} +
+ ); +}; + +export default ElementSettingSpreadsheet; diff --git a/src/pages/BandoFormsEdit/components/FormBuilder/index.js b/src/pages/BandoFormsEdit/components/FormBuilder/index.js index 9bca372..ee7e058 100644 --- a/src/pages/BandoFormsEdit/components/FormBuilder/index.js +++ b/src/pages/BandoFormsEdit/components/FormBuilder/index.js @@ -18,6 +18,7 @@ const FormBuilder = ({ callStatus, context }) => { const elementItems = useStoreValue('elementItems'); const activeElement = useStoreValue('activeElement'); const isAsyncRequest = useStoreValue('isAsyncRequest'); + const isSpreadsheetActive = elements.find(el => el.id === activeElement)?.name === 'spreadsheet'; const renderField = useCallback((field, index) => { return ( @@ -56,7 +57,7 @@ const FormBuilder = ({ callStatus, context }) => { return ( <> - +

{__('Impostazioni del campo modulo', 'gepafin')}

{!isEmpty(activeElement) ? diff --git a/src/pages/DomandaEditInstructorManager/index.js b/src/pages/DomandaEditInstructorManager/index.js index 3cd2e85..94522ed 100644 --- a/src/pages/DomandaEditInstructorManager/index.js +++ b/src/pages/DomandaEditInstructorManager/index.js @@ -1586,6 +1586,7 @@ const DomandaEditInstructorManager = () => { if (!tableColumns) { tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns')); } + const template = head(o.settings.filter(s => s.name === 'template')); const step = head(o.settings.filter(o => o.name === 'step')); const mime = head(o.settings.filter(o => o.name === 'mime')); const formula = head(o.settings.filter(o => o.name === 'formula')); @@ -1645,6 +1646,7 @@ const DomandaEditInstructorManager = () => { sourceId={id} useGrouping={false} tableColumns={tableColumns ? tableColumns.value : {}} + template={template ? template.value : null} /> })} diff --git a/src/pages/DomandaEditPreInstructor/index.js b/src/pages/DomandaEditPreInstructor/index.js index fcad5c2..ec62089 100644 --- a/src/pages/DomandaEditPreInstructor/index.js +++ b/src/pages/DomandaEditPreInstructor/index.js @@ -1493,6 +1493,7 @@ const DomandaEditPreInstructor = () => { if (!tableColumns) { tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns')); } + const template = head(o.settings.filter(s => s.name === 'template')); const step = head(o.settings.filter(o => o.name === 'step')); const mime = head(o.settings.filter(o => o.name === 'mime')); const formula = head(o.settings.filter(o => o.name === 'formula')); @@ -1552,6 +1553,7 @@ const DomandaEditPreInstructor = () => { sourceId={id} useGrouping={false} tableColumns={tableColumns ? tableColumns.value : {}} + template={template ? template.value : null} /> })} diff --git a/src/pages/LoginConfidi/index.js b/src/pages/LoginConfidi/index.js index 3f92c11..1357bde 100644 --- a/src/pages/LoginConfidi/index.js +++ b/src/pages/LoginConfidi/index.js @@ -73,7 +73,7 @@ const LoginConfidi = () => { } useEffect(() => { - if (!isEmpty(token)) { + if (!isEmpty(token) && !AuthenticationService.isExpired()) { setLoading(true); window.location.replace('/') } diff --git a/src/service/network-service.js b/src/service/network-service.js index 09228c9..a7d4e3f 100644 --- a/src/service/network-service.js +++ b/src/service/network-service.js @@ -1,4 +1,4 @@ -import { storeGet, storeSet } from '../store'; +import { storeGet } from '../store'; import logMsgWithSentry from '../helpers/logMsgWithSentry'; import { isEmpty } from 'ramda'; diff --git a/src/store/initial.js b/src/store/initial.js index dda4363..e72b3f8 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -17,6 +17,7 @@ const initialStore = { formLabel: '', formElements: [], elementItems: [], + callFormFields: [], activeElement: '', selectedElement: '', draggingElementId: 0, diff --git a/src/tempData.js b/src/tempData.js index bf12269..3a38f3b 100644 --- a/src/tempData.js +++ b/src/tempData.js @@ -675,5 +675,23 @@ export const elementItems = [ validators: { isRequired: false } + }, + { + id: 24, + sortOrder: 24, + name: 'spreadsheet', + label: 'Foglio di Calcolo', + description: 'Modello di foglio di calcolo con variabili dinamiche', + settings: [ + { + name: "label", + value: "Foglio di Calcolo" + }, + { + name: "template", + value: {} + } + ], + validators: {} } ]