- updated;
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
338
src/pages/BandoEdit/components/BandoEditFormStep3Excel/index.js
Normal file
338
src/pages/BandoEdit/components/BandoEditFormStep3Excel/index.js
Normal file
@@ -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 (
|
||||
<div className="appForm">
|
||||
<div className="appPageSection">
|
||||
<div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv,.ods"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleImport}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="p-button p-button-outlined p-button-sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{__('Importa foglio', 'gepafin')}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ width: '100%', height: '700px' }}
|
||||
onMouseMove={(e) => { mousePos.current = { x: e.clientX, y: e.clientY }; }}
|
||||
onMouseLeave={() => setTagTooltip(null)}
|
||||
/>
|
||||
{tagTooltip && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: mousePos.current.y - 36,
|
||||
left: mousePos.current.x + 12,
|
||||
background: '#1e293b',
|
||||
color: '#fff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{tagTooltip.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
<div className="appPageSection__hr">
|
||||
<span>{__('Azioni', 'gepafin')}</span>
|
||||
</div>
|
||||
|
||||
<Toast ref={toast} />
|
||||
<BandoEditFormActions
|
||||
id={id}
|
||||
status={bandoStatus}
|
||||
submitFn={onSaveDraft}
|
||||
openPreview={openPreview}
|
||||
openPreviewEvaluation={openPreviewEvaluation}/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default BandoEditFormStep3Excel;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -23,7 +23,7 @@ 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 BandoEditFormStep3Excel from './components/BandoEditFormStep3Excel';
|
||||
import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
|
||||
|
||||
const BandoEdit = () => {
|
||||
@@ -375,8 +375,11 @@ const BandoEdit = () => {
|
||||
? <BandoEditFormStep2 initialData={data} setInitialData={setData} ref={formRef}
|
||||
status={data.status}/>
|
||||
: null}
|
||||
{activeStep === 2 && data.evaluationVersion === 'V2'
|
||||
{/*{activeStep === 2 && data.evaluationVersion === 'V2'
|
||||
? <BandoEditFormStep3/>
|
||||
: null}*/}
|
||||
{activeStep === 2 && data.evaluationVersion === 'V2'
|
||||
? <BandoEditFormStep3Excel/>
|
||||
: null}
|
||||
|
||||
<div className="appPageSection">
|
||||
|
||||
Reference in New Issue
Block a user