diff --git a/package.json b/package.json index 0783782..ccd2660 100644 --- a/package.json +++ b/package.json @@ -4,53 +4,54 @@ "private": true, "dependencies": { "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/preset-react": "7.24.7", - "@date-fns/tz": "1.1.2", - "@emailjs/browser": "^4.4.1", - "@emotion/styled": "11.13.0", + "@babel/preset-react": "7.25.9", + "@date-fns/tz": "1.2.0", + "@emailjs/browser": "4.4.1", + "@emotion/styled": "11.13.5", "@number-flow/react": "0.4.2", - "@sentry/browser": "^8.42.0", - "@stomp/stompjs": "^7.0.0", - "@tanstack/react-table": "^8.20.5", - "@wordpress/i18n": "5.8.0", - "@wordpress/react-i18n": "4.8.0", - "@xyflow/react": "12.3.1", + "@sentry/browser": "8.42.0", + "@stomp/stompjs": "7.0.0", + "@tanstack/react-table": "8.20.5", + "@wordpress/i18n": "5.13.0", + "@wordpress/react-i18n": "4.13.0", "codice-fiscale-js": "2.3.22", - "copy-to-clipboard": "^3.3.3", - "deep-object-diff": "^1.1.9", - "dompurify": "3.1.7", + "copy-to-clipboard": "3.3.3", + "deep-object-diff": "1.1.9", + "dompurify": "3.2.2", "fast-deep-equal": "3.1.3", - "hotkeys-js": "^3.13.7", - "html-react-parser": "5.1.16", + "hotkeys-js": "3.13.7", + "html-react-parser": "5.1.18", "jwt-decode": "4.0.0", "klona": "2.0.6", + "leader-line-new": "1.1.9", "luxon": "3.5.0", "object-path-immutable": "4.1.2", "primeicons": "7.0.0", - "primereact": "10.8.4", - "quill": "2.0.2", + "primereact": "10.8.5", + "quill": "2.0.3", "ramda": "0.30.1", "react": "18.3.1", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "18.3.1", - "react-hook-form": "7.53.0", - "react-router-dom": "6.26.2", + "react-hook-form": "7.53.2", + "react-router-dom": "7.0.1", "react-scripts": "5.0.1", + "recharts": "2.15.0", "sockjs-client": "^1.6.1", "validate.js": "0.13.1", "zustand": "4.5.4", "zustand-x": "3.0.4" }, "devDependencies": { - "@babel/cli": "7.25.6", - "@babel/core": "7.25.2", - "@babel/plugin-syntax-jsx": "7.24.7", - "@wordpress/babel-plugin-makepot": "6.8.0", + "@babel/cli": "7.25.9", + "@babel/core": "7.26.0", + "@babel/plugin-syntax-jsx": "7.25.9", + "@wordpress/babel-plugin-makepot": "6.13.0", "babel-plugin-macros": "3.1.0", - "node-wp-i18n": "^1.2.7", - "sass": "1.79.3", - "sass-loader": "16.0.2" + "node-wp-i18n": "1.2.7", + "sass": "1.81.0", + "sass-loader": "16.0.3" }, "scripts": { "start": "GENERATE_SOURCEMAP=false react-scripts start", diff --git a/src/assets/scss/components/charts.scss b/src/assets/scss/components/charts.scss new file mode 100644 index 0000000..15cff0e --- /dev/null +++ b/src/assets/scss/components/charts.scss @@ -0,0 +1,39 @@ +.chartCard { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + min-height: 220px; + padding: 20px 5px; + border-radius: 6px; + border: 1px solid #EAB308; + background: #FFF; +} + +.chartCard__title { + color: var(--global-textColor); + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: normal; + margin: 0 0 16px; +} + +.chartCard__chart { + width: 100%; + height: 24rem; +} + +.chartCard__tooltip { + padding: 5px 10px; + background-color: white; + border: 1px solid var(--global-textColor); +} + +.chartCard__tooltipTitle { + font-weight: bold; +} + +.chartCard__tooltipText { + +} \ No newline at end of file diff --git a/src/assets/scss/components/flowBuilder.scss b/src/assets/scss/components/flowBuilder.scss index 04e3e83..8916a7b 100644 --- a/src/assets/scss/components/flowBuilder.scss +++ b/src/assets/scss/components/flowBuilder.scss @@ -32,4 +32,136 @@ font-size: 13px; text-align: center; } +} +.flowContainer { + width: 100%; + overflow-x: auto; + margin-top: 30px; +} + +.flowContainerInner { + position: relative; + display: flex; + flex-direction: column; + width: max-content; + margin: 0 auto; +} + +.flowContainer__level { + display: flex; + justify-content: center; + gap: 20px; + /*min-height: 240px;*/ + /*margin: 0 auto;*/ + + &.initialLevel { + padding: 0 0 30px; + } + + &.intermediateLevel, &.finalLevel { + padding: 30px 0 30px; + } + + &.intermediateLevel { + border-bottom: 1px solid var(--table-border-color); + border-top: 1px solid var(--table-border-color); + } +} + +.flowContainer__flowItem { + position: relative; + display: flex; + flex-direction: column; + width: 280px; + min-width: 280px; + padding: 15px; + border: 1px solid var(--table-border-color); + z-index: 9; + + .flowContainer__flowItemInner > label { + border-color: #757575; + background-color: #757575; + color: white; + } + + &.initialForm, &.finalForm { + .flowContainer__flowItemInner > label { + border-color: var(--card-full-background-color-3); + background-color: var(--card-full-background-color-3); + color: white; + } + } + + &.levelForms, &.initialForm { + &:after { + position: absolute; + bottom: -31px; + left: 50%; + content: ''; + width: 1px; + height: 31px; + background-color: var(--table-border-color); + } + } + + &.levelForms, &.finalForm { + &:before { + position: absolute; + top: -31px; + left: 50%; + content: ''; + width: 1px; + height: 31px; + background-color: var(--table-border-color); + } + } +} + +.flowContainer__levelMaskStart, .flowContainer__levelMaskEnd { + position: absolute; + width: 140px; + height: 100% ; + top: -1px; + background-color: white; +} +.flowContainer__levelMaskStart { + left: 0; +} +.flowContainer__levelMaskEnd { + right: 0; +} + +.flowContainer__flowItemInner { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + max-width: 250px; + border: 1px solid var(--panel-content-borderColor); + height: 100%; + + > label { + display: flex; + justify-content: center; + padding: 10px; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; + text-align: center; + } +} + +.flowContainer__flowItemContent { + padding: 5px; + display: flex; + flex-direction: column; + + .appForm__field { + margin-top: 10px; + + label { + text-align: center; + } + } } \ No newline at end of file diff --git a/src/assets/scss/components/misc.scss b/src/assets/scss/components/misc.scss index b809ae8..162068e 100644 --- a/src/assets/scss/components/misc.scss +++ b/src/assets/scss/components/misc.scss @@ -147,6 +147,14 @@ max-width: 100%; } +.p-password.p-inputwrapper { + width: 100%; + + > div, input { + width: 100%; + } +} + .p-inputgroup.flex-1 { align-items: center; } diff --git a/src/assets/scss/components/topBar.scss b/src/assets/scss/components/topBar.scss index fc73015..bb9c86d 100644 --- a/src/assets/scss/components/topBar.scss +++ b/src/assets/scss/components/topBar.scss @@ -72,4 +72,10 @@ i { margin-right: 7px; } +} + +@media (max-width: 500px) { + .topBar__endContent { + flex-wrap: wrap; + } } \ No newline at end of file diff --git a/src/assets/scss/theme.scss b/src/assets/scss/theme.scss index 943b18a..53499eb 100644 --- a/src/assets/scss/theme.scss +++ b/src/assets/scss/theme.scss @@ -21,6 +21,7 @@ --message-warning-color: #cc8925; --message-info-background: rgba(183, 183, 183, 0.7); --message-info-color: #3B82F6; + --panel-content-borderColor: #E5E7EB; --card-full-background-color-2: #EEC137; --card-full-background-color-3: #FA8E42; @@ -49,3 +50,4 @@ @import "./components/evaluation.scss"; @import "./components/fieldsRepeater.scss"; @import "./components/notificationsSidebar.scss"; +@import "./components/charts.scss"; diff --git a/src/components/ChartDomandePerBando/index.js b/src/components/ChartDomandePerBando/index.js new file mode 100644 index 0000000..700e6a7 --- /dev/null +++ b/src/components/ChartDomandePerBando/index.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { isEmpty } from 'ramda'; + +// components + + +const ChartDomandePerBando = ({ title, data = [] }) => { + const truncateText = (text) => { + const maxLength = 12; + if (typeof text === 'string' && text.length > maxLength) { + return `${text.slice(0, maxLength)}...`; + } + return text; + }; + + // Custom tooltip + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ {__('Domande', 'gepafin')}: {payload[0].value} +

+
+ ); + } + return null; + }; + + return (
+ {title ? {title} : null} + {data && !isEmpty(data) + ?
+ + + + + + }/> + + + + +
: null} +
) +} + +export default ChartDomandePerBando; \ No newline at end of file diff --git a/src/components/ChartStatoDomande/index.js b/src/components/ChartStatoDomande/index.js new file mode 100644 index 0000000..856fd6c --- /dev/null +++ b/src/components/ChartStatoDomande/index.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { Tooltip, ResponsiveContainer, Cell, Pie, PieChart } from 'recharts'; +import { isEmpty } from 'ramda'; +import getBandoLabel from '../../helpers/getBandoLabel'; + +// components + + +const ChartStatoDomande = ({ title, data = [] }) => { + const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d']; + + const CustomTooltip = ({ active, payload }) => { + if (active && payload && payload.length) { + return ( +
+

{getBandoLabel(payload[0].name)}

+

+ {__('Domande', 'gepafin')}: {payload[0].value} +

+
+ ); + } + return null; + }; + + return (
+ {title ? {title} : null} + {data && !isEmpty(data) + ?
+ + + `${(percent * 100).toFixed(0)}%`} + outerRadius={120} + fill="#8884d8" + dataKey="numberOfApplications" + nameKey="status" + > + {data.map((entry, index) => ( + + ))} + + } /> + + +
: null} +
) +} + +export default ChartStatoDomande; \ No newline at end of file diff --git a/src/components/FlowBuilder/components/NodeInitialForm/index.js b/src/components/FlowBuilder/components/NodeInitialForm/index.js deleted file mode 100644 index 79fe833..0000000 --- a/src/components/FlowBuilder/components/NodeInitialForm/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Handle, Position } from '@xyflow/react'; -import { isEmpty, head } from 'ramda'; - -// store -import { storeGet, storeSet } from '../../../../store'; -import { __ } from '@wordpress/i18n'; - -const NodeInitialForm = ({ data: { id, label = '' } }) => { - const flowData = storeGet.main.flowData(); - const [value, setValue] = useState(''); - - useEffect(() => { - const flowForms = storeGet.main.flowForms(); - const form = head(flowForms.filter(o => String(o.id) === String(id))); - const flowDataItem = head(flowData.filter(o => String(o.formId) === String(id))); - - if (form && flowDataItem) { - const field = head(form.content.filter(o => o.id === flowDataItem.chosenField)); - - if (field) { - const label = head(field.settings.filter(o => o.name === 'label')); - setValue(label ? label.value : field.label); - } - } - }, [flowData]); - - return ( -
- - {value} - {/*{options && !isEmpty(options) - ? : null}*/} - -
- ); -} - -export default NodeInitialForm; \ No newline at end of file diff --git a/src/components/FlowBuilder/components/NodeIntermediateForm/index.js b/src/components/FlowBuilder/components/NodeIntermediateForm/index.js deleted file mode 100644 index 8cc3a0e..0000000 --- a/src/components/FlowBuilder/components/NodeIntermediateForm/index.js +++ /dev/null @@ -1,78 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Handle, Position } from '@xyflow/react'; -import { head, isEmpty } from 'ramda'; -import { __ } from '@wordpress/i18n'; - -import { useStore, storeSet, storeGet } from '../../../../store'; - -const NodeIntermediateForm = ({ data: { id, label = '' } }) => { - const flowEdges = useStore().main.flowEdges(); - const flowData = useStore().main.flowData(); - const [options, setOptions] = useState([]); - const [value, setValue] = useState(''); - - const onChangeFn = (e) => { - const { value } = e.target; - const data = { - formId: String(id), - chosenField: '', - chosenValue: value - } - setValue(value); - storeSet.main.addFlowData(data); - } - - useEffect(() => { - const edge = head(flowEdges.filter(o => o.target === String(id))); - if (edge) { - const sourceForm = edge.source; - const sourceFormData = head(flowData.filter(o => String(o.formId) === sourceForm)); - const flowForms = storeGet.main.flowForms(); - const form = head(flowForms.filter(o => String(o.id) === String(sourceForm))); - - if (form && sourceFormData) { - const { chosenField } = sourceFormData; - const field = head(form.content.filter(o => o.id === chosenField)); - if (field) { - const options = head(field.settings.filter(o => o.name === 'options')); - if (options) { - setOptions(options.value); - } - } - } - } - - const flowDataForm = head(flowData.filter(o => String(o.formId) === String(id))); - - if (flowDataForm) { - setValue(flowDataForm.chosenValue); - } - }, [flowEdges, flowData]); - - return ( -
- - - {options && !isEmpty(options) - ? : null} - -
- ); -} - -export default NodeIntermediateForm; \ No newline at end of file diff --git a/src/components/FlowBuilder/index.js b/src/components/FlowBuilder/index.js deleted file mode 100644 index 3bf66de..0000000 --- a/src/components/FlowBuilder/index.js +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - ReactFlow, - Background -} from '@xyflow/react'; -import { isEmpty } from 'ramda'; - -import '@xyflow/react/dist/style.css'; - -// store -import { useStore, storeSet } from '../../store'; - -// nodes -import NodeInitialForm from './components/NodeInitialForm'; -import NodeIntermediateForm from './components/NodeIntermediateForm'; - -const nodeTypes = { - initialForm: NodeInitialForm, - intermediateForm: NodeIntermediateForm -}; - -const FlowBuilder = ({ initialForm = 0, finalForm = 0, mainField = '' }) => { - const flowForms = useStore().main.flowForms(); - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); - - const range = (start, stop, step) => { - return Array.from( - { length: (stop - start) / step + 1 }, - (_, i) => start + i * step - ); - } - - useEffect(() => { - if ( - (flowForms.length === 2 && initialForm) || - (flowForms.length > 2 && initialForm && finalForm) - ) { - const total = (flowForms.length - 2) * (200 - 90); - let coordinates = range(total * -1, total, 200); - - const initialNodes = flowForms.map(o => { - const formId = String(o.id); - let obj; - - if (formId === String(initialForm)) { - obj = { - id: formId, - type: 'initialForm', - data: { label: o.label, id: formId }, - position: { x: 0, y: 0 }, - } - } else if (formId === String(finalForm)) { - obj = { - id: formId, - type: 'output', - data: { label: o.label, id: formId }, - position: { x: 0, y: flowForms.length === 2 ? 150 : 300 }, - } - } else { - const x = coordinates.splice(0, 1); - obj = { - id: formId, - type: 'intermediateForm', - data: { label: o.label, id: formId }, - position: { x, y: 150 }, - } - } - return obj - }); - - let edges = []; - // eslint-disable-next-line - flowForms.map(o => { - const formId = String(o.id); - - if (formId !== String(initialForm) && formId !== String(finalForm)) { - edges.push({ - id: `${initialForm}->${formId}`, - source: String(initialForm), - target: formId, - type: 'smoothstep' - }); - } - if (formId !== String(initialForm) && formId !== String(finalForm) && String(finalForm) !== '0') { - edges.push({ - id: `${formId}->${finalForm}`, - source: formId, - target: String(finalForm), - type: 'smoothstep' - }); - } - }); - - if (flowForms.length === 2 && initialForm && finalForm) { - edges.push({ - id: `${initialForm}->${finalForm}`, - source: String(initialForm), - target: String(finalForm), - type: 'smoothstep' - }); - } - - setNodes(initialNodes); - setEdges(edges); - storeSet.main.flowEdges(edges); - } else { - setNodes([]); - setEdges([]); - } - }, [initialForm, finalForm, flowForms, mainField]); - - return ( - !isEmpty(nodes) && !isEmpty(edges) - ?
- - - -
- : null - ); -} - -export default FlowBuilder; \ No newline at end of file diff --git a/src/components/FormField/components/PasswordField/index.js b/src/components/FormField/components/PasswordField/index.js new file mode 100644 index 0000000..f214f9c --- /dev/null +++ b/src/components/FormField/components/PasswordField/index.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { Password } from 'primereact/password'; + +const PasswordField = ({ + fieldName, + label, + control, + errors, + defaultValue, + config = {}, + infoText = null, + inputgroup = false, + icon = null, + placeholder = '', + disabled = false, + onBlurFn = () => { + } + }) => { + const input = ( + + )}/> + return ( + <> + + {inputgroup + ?
+ + {icon} + + {input} +
+ : input} + {infoText ? {infoText} : null} + ) +} + +export default PasswordField; \ No newline at end of file diff --git a/src/components/FormField/index.js b/src/components/FormField/index.js index bde12fb..c1559a7 100644 --- a/src/components/FormField/index.js +++ b/src/components/FormField/index.js @@ -16,6 +16,7 @@ import Wysiwyg from './components/Wysiwyg'; import Checkboxes from './components/Checkboxes'; import Fileupload from './components/Fileupload'; import Table from './components/Table'; +import PasswordField from './components/PasswordField'; const FormField = (props) => { const fields = { @@ -31,7 +32,8 @@ const FormField = (props) => { radio: Radio, wysiwyg: Wysiwyg, checkboxes: Checkboxes, - table: Table + table: Table, + password: PasswordField } const Comp = !isNil(fields[props.type]) ? fields[props.type] : null; diff --git a/src/configData.js b/src/configData.js index ba5d649..5d3e7b8 100644 --- a/src/configData.js +++ b/src/configData.js @@ -19,6 +19,7 @@ const dynamicDataForTextinput = [ { label: 'ragione sociale', value: 'company.companyName' }, { label: 'partita IVA', value: 'company.vatNumber' }, { label: 'codice fiscale azienda', value: 'company.codiceFiscale' }, + { label: 'codice ateco', value: 'company.codiceAteco' }, { label: 'indirizzo', value: 'company.address' }, { label: 'numero di telefono azienda', value: 'company.phoneNumber' }, { label: 'città', value: 'company.city' }, diff --git a/src/layouts/DefaultLayout/components/AppSidebar/index.js b/src/layouts/DefaultLayout/components/AppSidebar/index.js index 37aa0eb..5c81b8f 100644 --- a/src/layouts/DefaultLayout/components/AppSidebar/index.js +++ b/src/layouts/DefaultLayout/components/AppSidebar/index.js @@ -64,11 +64,25 @@ const AppSidebar = () => { }, { label: __('Archivio domande', 'gepafin'), - icon: 'pi pi-file', + icon: 'pi pi-briefcase', href: '/domande', id: 7, enable: intersection(permissions, ['APPLY_CALLS']).length }, + { + label: __('Archivio domande', 'gepafin'), + icon: 'pi pi-briefcase', + href: '/domande-archivio', + id: 5, + enable: intersection(permissions, ['VIEW_USERS', 'MANAGE_USERS']).length + }, + { + label: __('Archivio domande', 'gepafin'), + icon: 'pi pi-briefcase', + href: '/domande-archivio', + id: 6, + enable: intersection(permissions, ['EVALUATE_APPLICATIONS']).length + }, { label: __('Soccorso istruttorio', 'gepafin'), icon: , diff --git a/src/pages/BandoApplication/index.js b/src/pages/BandoApplication/index.js index 6a33db6..d6d324b 100644 --- a/src/pages/BandoApplication/index.js +++ b/src/pages/BandoApplication/index.js @@ -312,7 +312,8 @@ const BandoApplication = () => { dynamicData = Object.keys(company).reduce((acc, cur) => { if ([ 'companyName', 'vatNumber', 'codiceFiscale', 'address', 'phoneNumber', - 'city', 'province', 'cap', 'country', 'pec', 'email', 'contactName', 'contactEmail' + 'city', 'province', 'cap', 'country', 'pec', 'email', 'contactName', 'contactEmail', + 'codiceAteco' ].includes(cur)) { acc.company[cur] = company[cur]; } diff --git a/src/pages/BandoFlowEdit/index.js b/src/pages/BandoFlowEdit/index.js index e6b210d..af64d22 100644 --- a/src/pages/BandoFlowEdit/index.js +++ b/src/pages/BandoFlowEdit/index.js @@ -1,13 +1,14 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { useNavigate, useParams } from 'react-router-dom'; -import { isEmpty, head } from 'ramda'; +import { isEmpty, head, pathOr } from 'ramda'; // store -import { storeGet, storeSet, useStore } from '../../store'; +import { storeSet } from '../../store'; // api import FormsService from '../../service/forms-service'; +import FlowService from '../../service/flow-service'; // tools import set404FromErrorResponse from '../../helpers/set404FromErrorResponse'; @@ -15,27 +16,33 @@ import set404FromErrorResponse from '../../helpers/set404FromErrorResponse'; // components import { Button } from 'primereact/button'; import { Dropdown } from 'primereact/dropdown'; -import FlowBuilder from '../../components/FlowBuilder'; import { Messages } from 'primereact/messages'; -import FlowService from '../../service/flow-service'; import { confirmPopup, ConfirmPopup } from 'primereact/confirmpopup'; import { Toast } from 'primereact/toast'; const BandoFlowEdit = () => { const { id } = useParams(); const navigate = useNavigate(); - const forms = useStore().main.flowForms(); - const flowData = useStore().main.flowData(); - const flowEdges = useStore().main.flowEdges(); + + const [flowStructure, setFlowStructure] = useState({ + initialForm: 0, + finalForm: 0, + flowData: [], + flowEdges: [], + chosenField: '' + }); + + const [forms, setForms] = useState([]); const [formOptions, setFormOptions] = useState([]); - const [initialForm, setInitialForm] = useState(0); - const [mainFieldOptions, setMainFieldOptions] = useState([]); - const [mainField, setMainField] = useState(''); + const [chosenMainFieldOptions, setChosenMainFieldOptions] = useState([]); + //const [chosenMainField, setChosenMainField] = useState(''); + const [mainFieldSuboptions, setMainFieldSubOptions] = useState([]); const [bandoStatus, setBandoStatus] = useState(''); - const [isFlowAllowed, setIsFlowAllowed] = useState(false); - const [finalForm, setFinalForm] = useState(0); + const [isFlowAllowed, setIsFlowAllowed] = useState(true); const flowMsgs = useRef(null); const toast = useRef(null); + const itemRefs = useRef({}); + const itemContainerRef = useRef(null); const getBandoId = () => { const parsed = parseInt(id) @@ -56,7 +63,8 @@ const BandoFlowEdit = () => { defaultFocus: 'reject', acceptClassName: 'p-button-danger', accept: doDelete, - reject: () => {} + reject: () => { + } }); }; @@ -64,46 +72,120 @@ const BandoFlowEdit = () => { if (flowMsgs.current) { flowMsgs.current.clear(); } - storeSet.main.flowData([]); - storeSet.main.flowEdges([]); - setInitialForm(0); - setMainFieldOptions([]); - setMainField(''); + + setFlowStructure({ + initialForm: 0, + finalForm: 0, + flowData: [], + flowEdges: [], + chosenField: '' + }) + setIsFlowAllowed(false); - setFinalForm(0); + setChosenMainFieldOptions([]); } - const updateInitialForm = (value) => { - setInitialForm(value); - if (forms.length === 2) { - const finalForm = head(forms.filter(o => o.id !== value)); - if (finalForm) { - setFinalForm(finalForm.id); + const updateInitialForm = useCallback((value) => { + const finalFormObj = head(forms.filter(o => o.id !== value)); + + if (forms.length === 2 && finalFormObj) { + setFlowStructure({ + ...flowStructure, + initialForm: value, + finalForm: finalFormObj.id + }); + } else { + setFlowStructure({ + ...flowStructure, + initialForm: value + }); + } + }, [flowStructure]) + + const updateFinalForm = useCallback((value) => { + const filtered = flowStructure.flowData.filter(o => o.formId === flowStructure.initialForm); + const flowEdges = buildFlowEdges(flowStructure.initialForm, value); + + setFlowStructure({ + ...flowStructure, + flowEdges, + flowData: filtered, + finalForm: value + }); + }, [flowStructure]); + + const updateChosenField = useCallback((value) => { + setFlowStructure({ + ...flowStructure, + chosenField: value + }); + }, [flowStructure]); + + const addFlowData = useCallback((data) => { + const initial = flowStructure.flowData; + const exists = initial ? initial.filter(o => parseInt(o.formId) === parseInt(data.formId)) : []; + let final = []; + + if (exists.length) { + final = initial.map(o => parseInt(o.formId) === parseInt(data.formId) ? data : o); + } else { + final = [...initial, data]; + } + + setFlowStructure({ + ...flowStructure, + flowData: final + }); + }, [flowStructure]); + + const updateItermediateForm = (value, formId) => { + const isUsed = flowStructure.flowData.map(o => o.chosenValue).filter(v => !isEmpty(v)).includes(value); + if (!isUsed) { + const data = { + formId: parseInt(formId), + chosenField: '', + chosenValue: value } + addFlowData(data); } } + const displayChosenOptionValue = (id) => { + const suboptionId = pathOr('', ['chosenValue'], head(flowStructure.flowData.filter(f => parseInt(f.formId) === parseInt(id)))); + return pathOr('', ['label'], head(mainFieldSuboptions.filter(o => o.name === suboptionId))); + } + + const disabledOptionForIntermediateForm = (opt) => { + return flowStructure.flowData.map(o => o.chosenValue).filter(v => !isEmpty(v)).includes(opt.name); + } + const shoudDisableSaving = useCallback(() => { + const nonEmptyFlowItems = flowStructure.flowData.filter(o => isEmpty(o.chosenField)).filter(o => !isEmpty(o.chosenValue)); + + /*if (flowForms.length > 2) { + console.log('disable BTN:', nonEmptyFlowItems.length !== flowForms.length - 2, isEmpty(flowEdges), 'PUBLISH' === bandoStatus, + isEmpty(initialForm), isEmpty(finalForm)); + } else { + console.log('disable BTN:', nonEmptyFlowItems.length !== 1, isEmpty(flowEdges), 'PUBLISH' === bandoStatus, + isEmpty(initialForm), isEmpty(finalForm)); + }*/ + return forms.length > 2 - ? isEmpty(flowData) || isEmpty(flowEdges) || isEmpty(initialForm) || isEmpty(finalForm) - || flowData.length < forms.length - 1 || 'PUBLISH' === bandoStatus - : isEmpty(flowEdges) || isEmpty(initialForm) || 'PUBLISH' === bandoStatus; - }, [flowData, flowEdges]); + ? nonEmptyFlowItems.length !== forms.length - 2 || isEmpty(flowStructure.flowEdges) || 'PUBLISH' === bandoStatus + || isEmpty(flowStructure.initialForm) || isEmpty(flowStructure.finalForm) + : nonEmptyFlowItems.length !== 1 || isEmpty(flowStructure.flowEdges) || 'PUBLISH' === bandoStatus + || isEmpty(flowStructure.initialForm) || isEmpty(flowStructure.finalForm); + }, [flowStructure, forms]); const doSave = () => { storeSet.main.setAsyncRequest(); const bandoId = getBandoId(); - const body = { - initialForm, - finalForm, - flowData, - flowEdges - }; + if (flowMsgs.current) { flowMsgs.current.clear(); } - FlowService.createFlow(bandoId, body, getFlowCreateCallback, errGetFlowCreateCallback); + FlowService.createFlow(bandoId, flowStructure, getFlowCreateCallback, errGetFlowCreateCallback); } const getFlowCreateCallback = (data) => { @@ -126,12 +208,13 @@ const BandoFlowEdit = () => { const getFormsCallback = (data) => { if (data.status === 'SUCCESS') { + setForms(data.data); const formOptions = data.data.map(o => ({ label: o.label, value: o.id })) - storeSet.main.flowForms(data.data); - setFormOptions([{label: '', value: ''}, ...formOptions]); + setFormOptions([{ label: '', value: '' }, ...formOptions]); const bandoId = getBandoId(); + storeSet.main.setAsyncRequest(); - FlowService.getFlow(bandoId, getFlowCallback, errGetFlowCallback); + FlowService.getFlow(bandoId, (resp) => getFlowCallback(resp, data.data), errGetFlowCallback); } storeSet.main.unsetAsyncRequest(); } @@ -141,21 +224,42 @@ const BandoFlowEdit = () => { storeSet.main.unsetAsyncRequest(); } - const getFlowCallback = (data) => { + const getFlowCallback = (data, forms) => { if (data.status === 'SUCCESS' && data.data) { - storeSet.main.flowData(data.data.flowData); - storeSet.main.flowEdges(data.data.flowEdges); - setInitialForm(data.data.initialForm); - setFinalForm(data.data.finalForm); - setBandoStatus(data.data.callStatus); const chosenFieldItem = head(data.data.flowData.filter(o => !isEmpty(o.chosenField))); - if (chosenFieldItem) { - setMainField(chosenFieldItem.chosenField); - } - const flowDataItem = head(data.data.flowData.filter(o => !isEmpty(o.chosenField))); + setBandoStatus(data.data.callStatus); - if (flowDataItem) { - setMainField(flowDataItem.chosenField); + if (chosenFieldItem) { + setFlowStructure({ + initialForm: data.data.initialForm, + finalForm: data.data.finalForm, + flowData: data.data.flowData, + flowEdges: data.data.flowEdges, + chosenField: chosenFieldItem.chosenField + }); + const form = head(forms.filter(o => o.id === data.data.initialForm)); + const relevantFields = form + ? form.content + .filter(o => ['radio', 'select'].includes(o.name)) + .map(o => { + const label = head(o.settings.filter(o => o.name === 'label')); + return { value: o.id, label: label ? label.value : o.label }; + }) + : []; + setChosenMainFieldOptions(relevantFields); + const field = form ? head(form.content.filter(o => o.id === chosenFieldItem.chosenField)) : null; + if (field) { + const options = head(field.settings.filter(o => o.name === 'options')); + setMainFieldSubOptions(options.value); + } + } else { + setFlowStructure({ + initialForm: data.data.initialForm, + finalForm: data.data.finalForm, + flowData: data.data.flowData, + flowEdges: data.data.flowEdges, + chosenField: '' + }); } } storeSet.main.unsetAsyncRequest(); @@ -166,71 +270,48 @@ const BandoFlowEdit = () => { storeSet.main.unsetAsyncRequest(); } - useEffect(() => { - const flowForms = storeGet.main.flowForms(); - const form = head(flowForms.filter(o => String(o.id) === String(initialForm))) - const field = form ? head(form.content.filter(o => o.id === mainField)) : null; - let options = []; + const setItemRef = (id, element) => { + itemRefs.current[id] = element; + }; - if (field) { - options = head(field.settings.filter(o => o.name === 'options')); - } + const buildFlowEdges = (initialForm, finalForm) => { + let edges = []; - if (field && options.value && options.value.length === flowForms.length - 2) { - setIsFlowAllowed(true); - const data = { - formId: String(initialForm), - chosenField: mainField, - chosenValue: '' - } - storeSet.main.addFlowData(data); - } else { - setIsFlowAllowed(false); - let msg = 'Non è possibile creare il flusso. Il campo principale deve avere esattamente %s opzioni.'; + if (!isEmpty(initialForm) && !isEmpty(finalForm)) { + // eslint-disable-next-line + forms.map(o => { + const formId = String(o.id); - if (flowForms.length - 2 === 1) { - msg = 'Non è possibile creare il flusso. Il campo principale deve avere esattamente %s opzione.'; - } + if (formId !== String(initialForm) && formId !== String(finalForm)) { + edges.push({ + id: `${initialForm}->${formId}`, + source: String(initialForm), + target: formId, + type: 'smoothstep' + }); + } + if (formId !== String(initialForm) && formId !== String(finalForm) && String(finalForm) !== '0') { + edges.push({ + id: `${formId}->${finalForm}`, + source: formId, + target: String(finalForm), + type: 'smoothstep' + }); + } + }); - if (flowMsgs.current && !isEmpty(mainField)) { - flowMsgs.current.clear(); - flowMsgs.current.show([ - { - id: '1', - sticky: true, severity: 'error', summary: '', - detail: sprintf( - __(msg, 'gepafin'), - flowForms.length - 2 - ), - closable: false - } - ]); + if (forms.length === 2 && initialForm && finalForm) { + edges.push({ + id: `${initialForm}->${finalForm}`, + source: String(initialForm), + target: String(finalForm), + type: 'smoothstep' + }); } } - }, [mainField]); - useEffect(() => { - setMainField(''); - setMainFieldOptions([]); - const flowForms = storeGet.main.flowForms(); - const form = head(flowForms.filter(o => String(o.id) === String(initialForm))) - const relevantFields = form - ? form.content - .filter(o => ['radio', 'select'].includes(o.name)) - .map(o => { - const label = head(o.settings.filter(o => o.name === 'label')); - return { value: o.id, label: label ? label.value : o.label }; - }) - : []; - setMainFieldOptions([ - {label: isEmpty(relevantFields) ? __('Nessun scelta', 'gepafin') : '', value: ''}, - ...relevantFields] - ); - - if (flowForms.length === 2) { - setIsFlowAllowed(true) - } - }, [initialForm]); + return edges; + }; useEffect(() => { const bandoId = getBandoId(); @@ -251,23 +332,98 @@ const BandoFlowEdit = () => { ]); } else { flowMsgs.current.clear(); + if (itemContainerRef.current) { + itemContainerRef.current.dispatchEvent(new Event('scroll')); + } } }, [forms]); useEffect(() => { - const chosenFieldItem = head(flowData.filter(o => !isEmpty(o.chosenField))); - if (chosenFieldItem) { - setMainField(chosenFieldItem.chosenField); - } - }, [flowData]) + const initialForm = flowStructure.initialForm; + const finalForm = flowStructure.finalForm; + const chosenField = flowStructure.chosenField; - useEffect(() => { - return () => { - storeSet.main.flowForms([]); - storeSet.main.flowData([]); - storeSet.main.flowEdges([]); + if (!isEmpty(initialForm) && !isEmpty(finalForm)) { + const form = head(forms.filter(o => String(o.id) === String(initialForm))) + const relevantFields = form + ? form.content + .filter(o => ['radio', 'select'].includes(o.name)) + .map(o => { + const label = head(o.settings.filter(o => o.name === 'label')); + return { value: o.id, label: label ? label.value : o.label }; + }) + : []; + setChosenMainFieldOptions([ + { label: isEmpty(relevantFields) ? __('Nessun scelta', 'gepafin') : '', value: '' }, + ...relevantFields] + ); + + if (forms.length === 2) { + setIsFlowAllowed(true); + } + + //const flowEdges = buildFlowEdges(initialForm, finalForm); + + if (!isEmpty(chosenField)) { + const field = form ? head(form.content.filter(o => o.id === chosenField)) : null; + let options = []; + + if (field) { + options = head(field.settings.filter(o => o.name === 'options')); + } + + if (field && options.value && options.value.length === forms.length - 2) { + setIsFlowAllowed(true); + const suboptions = [ + { label: __('Nessun scelta', 'gepafin'), name: '' }, + ...options.value + ] + + setMainFieldSubOptions(suboptions); + + const data = { + formId: parseInt(initialForm), + chosenField: chosenField, + chosenValue: '' + } + + addFlowData(data); + + if (flowMsgs.current && !isEmpty(chosenField)) { + flowMsgs.current.clear(); + } + } else { + setIsFlowAllowed(false); + + let msg = 'Non è possibile creare il flusso. Il campo principale deve avere esattamente %s opzioni.'; + + if (forms.length - 2 === 1) { + msg = 'Non è possibile creare il flusso. Il campo principale deve avere esattamente %s opzioni.'; + } + + if (flowMsgs.current && !isEmpty(chosenField)) { + flowMsgs.current.clear(); + flowMsgs.current.show([ + { + id: '1', + sticky: true, severity: 'error', summary: '', + detail: sprintf( + __(msg, 'gepafin'), + forms.length - 2 + ), + closable: false + } + ]); + } + } + } } - }, []); + }, [flowStructure.initialForm, flowStructure.finalForm, flowStructure.chosenField]); + + const { initialForm = 0, finalForm = 0, flowData = [], chosenField = '' } = flowStructure; + const initialFormData = head(forms.filter(o => o.id === initialForm)); + const finalFormData = head(forms.filter(o => o.id === finalForm)); + const levelForms = forms.filter(o => o.id !== initialForm && o.id !== finalForm); return (
@@ -296,29 +452,29 @@ const BandoFlowEdit = () => { placeholder={__('Scegli il form', 'gepafin')}/>
- {forms.length > 2 && initialForm && mainFieldOptions + {forms.length > 2 && initialForm && chosenMainFieldOptions ?
- + setMainField(e.value)} + value={chosenField} + onChange={(e) => updateChosenField(e.value)} optionDisabled={(opt) => isEmpty(opt.value)} - options={mainFieldOptions} + options={chosenMainFieldOptions} optionLabel="label" optionValue="value" placeholder={__('Scegli il campo', 'gepafin')}/>
: null} - {(forms.length > 2 && mainField && isFlowAllowed) || (forms.length === 2 && isFlowAllowed) + {(forms.length > 2 && chosenField && isFlowAllowed) || (forms.length === 2 && isFlowAllowed) ?
setFinalForm(e.value)} + onChange={(e) => updateFinalForm(e.value)} optionDisabled={(opt) => initialForm === opt.value || isEmpty(opt.value)} options={formOptions} optionLabel="label" @@ -343,11 +499,67 @@ const BandoFlowEdit = () => {
- {forms.length >= 2 && isFlowAllowed - ? : null} + + {forms.length >= 2 && initialForm && finalForm && isFlowAllowed + ?
+
+
+
initialForm ? setItemRef(initialForm, el) : null}> +
+ +
+
+
+ + {levelForms.length && initialForm && finalForm + ?
+ {levelForms.map((o, i) =>
setItemRef(o.id, el)} + className="flowContainer__flowItem levelForms"> +
+ +
+ {mainFieldSuboptions && !isEmpty(mainFieldSuboptions) + ? 'PUBLISH' !== bandoStatus + ? f.formId === parseInt(o.id))))} + onChange={(e) => updateItermediateForm(e.value, o.id)} + options={mainFieldSuboptions} + optionDisabled={disabledOptionForIntermediateForm} + optionLabel="label" + optionValue="name" + placeholder={__('Scegli il valore', 'gepafin')}/> + : + + : null} +
+
+
)} + {levelForms.length > 1 + ? <> +
+
+ : null} +
: null} + + {forms.length >= 2 && initialForm && finalForm + ?
+
finalForm ? setItemRef(finalForm, el) : null}> +
+ +
+
+
+ : null} +
+
: null}
diff --git a/src/pages/Dashboard/index.js b/src/pages/Dashboard/index.js index 71334b0..4bb003e 100644 --- a/src/pages/Dashboard/index.js +++ b/src/pages/Dashboard/index.js @@ -4,33 +4,31 @@ import { useNavigate } from 'react-router-dom'; import { pathOr } from 'ramda'; import NumberFlow from '@number-flow/react'; -// store -//import { storeSet } from '../../store'; - // api import DashboardService from '../../service/dashboard-service'; // components import LatestBandiTable from './components/LatestBandiTable'; -//import LatestUsersActivityTable from './components/LatestUsersActivityTable'; import { Button } from 'primereact/button'; -//import MyEvaluationsTable from '../DashboardPreInstructor/components/PreInstructorDomandeTable'; import AllDomandeTable from '../Domande/components/AllDomandeTable'; import DraftApplicationsTable from './components/DraftApplicationsTable'; +import ChartDomandePerBando from '../../components/ChartDomandePerBando'; +import ChartStatoDomande from '../../components/ChartStatoDomande'; const Dashboard = () => { const navigate = useNavigate(); const [mainStats, setMainStats] = useState({}); + const [chartStats, setChartStats] = useState({}); const onGoToCreateNewBando = () => { navigate('/bandi/new'); } - /*const onGoToUsers = () => { - console.log('onGoToUsers') + const onGoToUsers = () => { + navigate('/utenti'); } - const onGoToStats = () => { + /*const onGoToStats = () => { console.log('onGoToStats') } @@ -45,16 +43,18 @@ const Dashboard = () => { const getStats = (data) => { if (data.status === 'SUCCESS') { setMainStats(data.data.widget1); + setChartStats(data.data.widgetBars); } } - const errGetStats = () => {} + const errGetStats = () => { + } useEffect(() => { DashboardService.getAdminStats(getStats, errGetStats); }, []); - return( + return (

{__('Dashboard', 'gepafin')}

@@ -147,20 +147,36 @@ const Dashboard = () => {
+ {chartStats.applicationPerCall + ?
+

{__('Statistiche di sistema', 'gepafin')}

+
+ + +
+
: null} + +
+
{__('Azioni rapide', 'gepafin')}
+
+
+ ); + }; + + const dateAppliedBodyTemplate = (rowData) => { + return formatDate(rowData.submissionDate); + }; + + const statusFilterTemplate = (options) => { + return options.filterCallback(e.value, options.index)} + itemTemplate={statusItemTemplate} placeholder={translationStrings.selectOneLabel} className="p-column-filter" + showClear/>; + }; + + const dateFilterTemplate = (options) => { + return options.filterCallback(e.value, options.index)} + dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999"/>; + }; + + const statusBodyTemplate = (rowData) => { + return ; + }; + + const statusItemTemplate = (option) => { + return ; + }; + + const actionsBodyTemplate = (rowData) => { + return
+ +
+ } + + const header = renderHeader(); + + return ( +
+ setFilters(e.filters)}> + + + + + + + + + +
+ ) +} + +export default AllDomandeArchiveTable; diff --git a/src/pages/DomandeArchive/index.js b/src/pages/DomandeArchive/index.js new file mode 100644 index 0000000..c6e7885 --- /dev/null +++ b/src/pages/DomandeArchive/index.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { __ } from '@wordpress/i18n'; + +// components +import AllDomandeArchiveTable from './components/AllDomandeArchiveTable'; + +const Domande = () => { + + return ( +
+
+

{__('Archivio domande', 'gepafin')}

+
+ +
+ +
+

{__('Domande pubblicate', 'gepafin')}

+ +
+
+ ) +} + +export default Domande; \ No newline at end of file diff --git a/src/pages/ResetPassword/index.js b/src/pages/ResetPassword/index.js index 05a1438..a43724f 100644 --- a/src/pages/ResetPassword/index.js +++ b/src/pages/ResetPassword/index.js @@ -1,9 +1,9 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useRef, useState, useEffect, useMemo } from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { useForm } from 'react-hook-form'; import { classNames } from 'primereact/utils'; -import { isEmpty } from 'ramda'; -import { useNavigate } from 'react-router-dom'; +import { isEmpty, isNil } from 'ramda'; +import { useNavigate, useSearchParams } from 'react-router-dom'; // tools import AuthenticationService from '../../service/authentication-service'; @@ -24,7 +24,9 @@ const ResetPassword = () => { const token = useStore().main.token(); const [loading, setLoading] = useState(false); const [resetPassToken, setResetPassToken] = useState(''); + const [resetPassEmail, setResetPassEmail] = useState(''); const errorMsgs = useRef(null); + let [searchParams] = useSearchParams(); const { control, handleSubmit, @@ -47,12 +49,33 @@ const ResetPassword = () => { } if (request.token && !isEmpty(request.token)) { - AuthenticationService.resetPassword(request, getCallback, errCallback); + AuthenticationService.resetPassword(request, getCallbackReset, errCallback); } else { AuthenticationService.forgotPassword(request, getCallback, errCallback); } }; + const getCallbackReset = (data) => { + if (data.status === 'SUCCESS') { + errorMsgs.current.show([ + { + sticky: true, severity: 'success', summary: '', + detail: data.message, + closable: true + } + ]); + } else { + errorMsgs.current.show([ + { + sticky: true, severity: 'error', summary: '', + detail: data.message, + closable: true + } + ]); + } + setLoading(false); + } + const getCallback = (data) => { if (data.status === 'SUCCESS') { setResetPassToken(data.data) @@ -87,9 +110,18 @@ const ResetPassword = () => { }, [token]); useEffect(() => { - setValue('token', resetPassToken); + console.log(resetPassToken, resetPassEmail); reset(); - }, [resetPassToken]) + setValue('token', resetPassToken); + setValue('email', resetPassEmail); + }, [resetPassToken, resetPassEmail]); + + useEffect(() => { + const token = searchParams.get('token'); + const email = searchParams.get('email'); + setResetPassToken(token); + setResetPassEmail(email); + }, [searchParams]); return (
@@ -113,7 +145,7 @@ const ResetPassword = () => { placeholder="sample@example.com" /> - {!isEmpty(resetPassToken) + {resetPassToken && !isEmpty(resetPassToken) ? { })} /> : null} - {!isEmpty(resetPassToken) + {resetPassToken && !isEmpty(resetPassToken) ? { }} /> : null} - {!isEmpty(resetPassToken) + {resetPassToken && !isEmpty(resetPassToken) ? { @@ -124,6 +125,18 @@ const routes = ({ role, chosenCompanyId }) => { {'ROLE_PRE_INSTRUCTOR' === role ? : null} {'ROLE_INSTRUCTOR_MANAGER' === role ? : null} }/> + + {'ROLE_SUPER_ADMIN' === role ? : null} + {'ROLE_BENEFICIARY' === role ? : null} + {'ROLE_PRE_INSTRUCTOR' === role ? : null} + {'ROLE_INSTRUCTOR_MANAGER' === role ? : null} + }/> + + {'ROLE_SUPER_ADMIN' === role ? : null} + {'ROLE_BENEFICIARY' === role ? : null} + {'ROLE_PRE_INSTRUCTOR' === role ? : null} + {'ROLE_INSTRUCTOR_MANAGER' === role ? : null} + }/> {'ROLE_SUPER_ADMIN' === role ? : null} {'ROLE_BENEFICIARY' === role ? : null} diff --git a/src/store/actions.js b/src/store/actions.js index baeb35e..448daa6 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -43,16 +43,5 @@ export const actionsBeta = (set, get, api) => ({ const newElements = newFields.toSpliced(hoverIndex, 0, prevFields[dragIndex]); set.formElements(newElements); } - }, - addFlowData: (data) => { - const initial = get.flowData(); - const exists = initial ? initial.filter(o => parseInt(o.formId) === parseInt(data.formId)) : []; - - if (exists.length) { - const newData = initial.map(o => parseInt(o.formId) === parseInt(data.formId) ? data : o); - set.flowData(newData); - } else { - set.flowData([...initial, data]); - } } });