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/index.js b/src/pages/BandoEdit/index.js index 4b52abd..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 BandoEditFormStep3Excel from './components/BandoEditFormStep3Excel'; import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup'; +import BandoEditFormStep3 from './components/BandoEditFormStep3'; const BandoEdit = () => { const isAsyncRequest = useStoreValue('isAsyncRequest'); @@ -375,11 +375,8 @@ const BandoEdit = () => { ? : null} - {/*{activeStep === 2 && data.evaluationVersion === 'V2' - ? - : null}*/} {activeStep === 2 && data.evaluationVersion === 'V2' - ? + ? : null}
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 7e742fa..e389807 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 7b44651..ec62089 100644 --- a/src/pages/DomandaEditPreInstructor/index.js +++ b/src/pages/DomandaEditPreInstructor/index.js @@ -822,7 +822,7 @@ const DomandaEditPreInstructor = () => { storeSet('unsetAsyncRequest'); } - const doCreateAppointment = () => { + /*const doCreateAppointment = () => { setAppointmentData({ title: '', text: '', @@ -830,7 +830,7 @@ const DomandaEditPreInstructor = () => { amount: 0 }); setIsVisibleAppointmentDialog(true); - } + }*/ const setAppointmentFieldValue = (name, value) => { const newData = wrap(appointmentData).set(name, value).value(); @@ -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: {} } ]