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: {}
}
]