updated form fields and application logic;

This commit is contained in:
Vitalii Kiiko
2024-09-12 17:17:48 +02:00
parent 19e17ec2d7
commit a8471ba7aa
42 changed files with 1423 additions and 231 deletions

58
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"luxon": "3.5.0", "luxon": "3.5.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.8.2", "primereact": "^10.8.2",
"quill": "^2.0.2",
"ramda": "0.30.1", "ramda": "0.30.1",
"react": "18.3.1", "react": "18.3.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
@@ -8384,6 +8385,11 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -12672,11 +12678,26 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
}, },
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lodash.mapvalues": { "node_modules/lodash.mapvalues": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz",
@@ -13373,6 +13394,11 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"node_modules/parchment": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A=="
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -15025,6 +15051,38 @@
} }
] ]
}, },
"node_modules/quill": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
"quill-delta": "^5.1.0"
},
"engines": {
"npm": ">=8.2.3"
}
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
"dependencies": {
"fast-diff": "^1.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/quill/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/raf": { "node_modules/raf": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",

View File

@@ -19,6 +19,7 @@
"luxon": "3.5.0", "luxon": "3.5.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.8.2", "primereact": "^10.8.2",
"quill": "^2.0.2",
"ramda": "0.30.1", "ramda": "0.30.1",
"react": "18.3.1", "react": "18.3.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",

View File

@@ -102,7 +102,7 @@
.appPageSection__withBorder { .appPageSection__withBorder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 7px; gap: 16px;
width: 100%; width: 100%;
padding: 17px; padding: 17px;
border-radius: 6px; border-radius: 6px;
@@ -131,7 +131,7 @@
padding: 5px 0; padding: 5px 0;
&.rowContent { &.rowContent {
padding: 17px 0; padding: 7px 0;
} }
p { p {
@@ -194,6 +194,7 @@
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
justify-content: space-between; justify-content: space-between;
margin: 0;
span { span {
font-size: 14px; font-size: 14px;

View File

@@ -1,4 +1,5 @@
.formBuilder { .formBuilder {
position: relative;
display: flex; display: flex;
gap: 20px; gap: 20px;
width: 100%; width: 100%;

View File

@@ -12,8 +12,8 @@ body {
margin: 0; margin: 0;
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
p, span:not(.p-button-label, .p-button-icon, .p-badge, .p-message-detail), input, label:not(.p-error), p, span:not(.p-button-label, .p-button-icon, .p-badge, .p-message-detail, .p-highlight),
textarea, a, li, h1, h2, h3, h4, h5, h6, div, th, td { input, label:not(.p-error), textarea, a, li, h1, h2, h3, h4, h5, h6, div, th, td {
color: var(--global-textColor); color: var(--global-textColor);
} }
} }

View File

@@ -83,6 +83,13 @@
margin: 0; margin: 0;
} }
.blockingOverlay {
position: absolute;
z-index: 999;
inset: 0;
background-color: rgba(255,255,255,0.3)
}
.mb-2 { .mb-2 {
margin-bottom: 4px; margin-bottom: 4px;
} }

View File

@@ -0,0 +1,8 @@
import React from 'react';
const BlockingOverlay = ({ shouldDisplay = false }) => {
return shouldDisplay
? <div className="blockingOverlay"></div> : null;
}
export default BlockingOverlay;

View File

@@ -22,13 +22,15 @@ const NodeInitialForm = ({ data: { id, label = '' } }) => {
useEffect(() => { useEffect(() => {
const forms = storeGet.main.flowForms(); const forms = storeGet.main.flowForms();
const form = head(forms.filter(o => String(o.id) === String(id))) if (forms.length > 2) {
const relevantFields = form const form = head(forms.filter(o => String(o.id) === String(id)))
? form.content const relevantFields = form
.filter(o => ['radio', 'select'].includes(o.name)) ? form.content
.map(o => ({ name: o.id, label: o.label })) .filter(o => ['radio', 'select'].includes(o.name))
: []; .map(o => ({ name: o.id, label: o.label }))
setOptions(relevantFields); : [];
setOptions(relevantFields);
}
}, [id]); }, [id]);
return ( return (

View File

@@ -55,11 +55,10 @@ const FlowBuilder = ({ initialForm = 0, finalForm = 0 }) => {
id: formId, id: formId,
type: 'output', type: 'output',
data: { label: o.label, id: formId }, data: { label: o.label, id: formId },
position: { x: 0, y: 300 }, position: { x: 0, y: forms.length === 2 ? 150 : 300 },
} }
} else { } else {
const x = coordinates.splice(0, 1); const x = coordinates.splice(0, 1);
console.log('x', x)
obj = { obj = {
id: formId, id: formId,
type: 'intermediateForm', type: 'intermediateForm',
@@ -78,11 +77,15 @@ const FlowBuilder = ({ initialForm = 0, finalForm = 0 }) => {
if (formId !== String(initialForm) && formId !== String(finalForm)) { if (formId !== String(initialForm) && formId !== String(finalForm)) {
edges.push({ id: `${initialForm}->${formId}`, source: String(initialForm), target: formId, type: 'smoothstep' }); edges.push({ id: `${initialForm}->${formId}`, source: String(initialForm), target: formId, type: 'smoothstep' });
} }
if (formId !== String(finalForm) && formId !== String(initialForm) && String(finalForm) !== '0') { if (formId !== String(initialForm) && formId !== String(finalForm) && String(finalForm) !== '0') {
edges.push({ id: `${formId}->${finalForm}`, source: formId, target: String(finalForm), type: 'smoothstep' }); edges.push({ id: `${formId}->${finalForm}`, source: formId, target: String(finalForm), type: 'smoothstep' });
} }
}); });
console.log('edges', edges, initialNodes);
if (forms.length === 2 && initialForm && finalForm) {
edges.push({ id: `${initialForm}->${finalForm}`, source: String(initialForm), target: String(finalForm), type: 'smoothstep' });
}
setNodes(initialNodes); setNodes(initialNodes);
setEdges(edges); setEdges(edges);
storeSet.main.flowEdges(edges); storeSet.main.flowEdges(edges);

View File

@@ -0,0 +1,57 @@
import React, { useState, useCallback } from 'react';
import { classNames } from 'primereact/utils';
import { Controller } from 'react-hook-form';
import { Checkbox } from 'primereact/checkbox';
const Checkboxes = ({
fieldName,
label,
control,
errors,
defaultValue = [],
config = {},
infoText = null,
options = []
}) => {
const [fieldVal, setFieldVal] = useState(defaultValue);
const onCheckboxesChange = useCallback((e, updateFn) => {
let data = [...fieldVal];
if (e.checked) {
data.push(e.value);
} else {
data.splice(data.indexOf(e.value), 1);
}
setFieldVal(data);
updateFn(data);
}, [fieldVal]);
const input = <Controller
name={fieldName}
control={control}
defaultValue={fieldVal}
rules={config}
render={({ field, fieldState }) =>
options.map(o => <div className="appForm__fieldItem" key={o.name}>
<Checkbox
inputId={`${fieldName}_${o.name}`}
name={fieldName}
value={o.name}
onChange={(e) => onCheckboxesChange(e, field.onChange)}
checked={field.value.includes(o.name)}
className={classNames({ 'p-invalid': fieldState.invalid })}/>
<label htmlFor={`${fieldName}_${o.name}`}>{o.label}</label>
</div>)}/>
return (
<>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required || config.isRequired ? '*' : null}
</label>
{input}
{infoText ? <small>{infoText}</small> : null}
</>)
}
export default Checkboxes;

View File

@@ -18,7 +18,7 @@ const Datepicker = ({
return ( return (
<> <>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
<Controller <Controller
name={fieldName} name={fieldName}

View File

@@ -15,8 +15,8 @@ const NumberInput = ({
inputgroup = false, inputgroup = false,
icon = null, icon = null,
locale = 'it-IT', locale = 'it-IT',
minFractionDigits = 2, minFractionDigits = 0,
step = 1, maxFractionDigits = 1,
min, min,
max max
}) => { }) => {
@@ -31,13 +31,14 @@ const NumberInput = ({
onValueChange={(e) => field.onChange(e.value)} onValueChange={(e) => field.onChange(e.value)}
min={min} min={min}
max={max} max={max}
locale={locale} minFractionDigits={minFractionDigits} step={step} locale={locale}
minFractionDigits={minFractionDigits}
className={classNames({ 'p-invalid': fieldState.invalid })}/> className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/> )}/>
return ( return (
<> <>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
{inputgroup {inputgroup
? <div className="p-inputgroup flex-1"> ? <div className="p-inputgroup flex-1">

View File

@@ -4,34 +4,35 @@ import { Controller } from 'react-hook-form';
import { RadioButton } from 'primereact/radiobutton'; import { RadioButton } from 'primereact/radiobutton';
const Radio = ({ const Radio = ({
fieldName, fieldName,
label, label,
control, control,
errors, errors,
defaultValue, defaultValue,
config = {}, config = {},
infoText = null, infoText = null,
options = [] options = []
}) => { }) => {
const input = <Controller const input = <Controller
name={fieldName} name={fieldName}
control={control} control={control}
defaultValue={defaultValue} defaultValue={defaultValue}
rules={config} rules={config}
render={({ field, fieldState }) => render={({ field, fieldState }) =>
options.map(o => <div className="appForm__fieldItem" key={o.name}> options.map(o => <div className="appForm__fieldItem" key={o.name}>
<RadioButton <RadioButton
id={`${fieldName}_${o.name}`} inputId={`${fieldName}_${o.name}`}
name={fieldName} name={fieldName}
value={o.name} value={o.name}
onChange={(e) => field.onChange(e.value)} onChange={(e) => field.onChange(e.value)}
checked={field.value === o.name}/> checked={field.value === o.name}
<label htmlFor={`${fieldName}_${o.name}`}>{o.label}</label> className={classNames({ 'p-invalid': fieldState.invalid })}/>
</div>)}/> <label htmlFor={`${fieldName}_${o.name}`}>{o.label}</label>
</div>)}/>
return ( return (
<> <>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
{input} {input}
{infoText ? <small>{infoText}</small> : null} {infoText ? <small>{infoText}</small> : null}

View File

@@ -27,13 +27,14 @@ const Select = ({
onChange={(e) => field.onChange(e.value)} onChange={(e) => field.onChange(e.value)}
options={options} options={options}
optionLabel="label" optionLabel="label"
optionValue="name"
placeholder={placeholder} placeholder={placeholder}
className={classNames({ 'p-invalid': fieldState.invalid })}/> className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/> )}/>
return ( return (
<> <>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
{inputgroup {inputgroup
? <div className="p-inputgroup flex-1"> ? <div className="p-inputgroup flex-1">

View File

@@ -34,7 +34,7 @@ const Switch = ({
<> <>
<div className="appForm__row"> <div className="appForm__row">
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] }, 'mr-8')}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] }, 'mr-8')}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
<div className="appForm__row"> <div className="appForm__row">
{offLabel ? <span>{offLabel}</span> : null} {offLabel ? <span>{offLabel}</span> : null}

View File

@@ -16,7 +16,7 @@ const TextArea = ({
return ( return (
<> <>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
<Controller <Controller
name={fieldName} name={fieldName}

View File

@@ -31,7 +31,7 @@ const TextInput = ({
return ( return (
<> <>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null} {label}{config.required || config.isRequired ? '*' : null}
</label> </label>
{inputgroup {inputgroup
? <div className="p-inputgroup flex-1"> ? <div className="p-inputgroup flex-1">

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { classNames } from 'primereact/utils';
import { Controller } from 'react-hook-form';
import { Editor } from 'primereact/editor';
const Wysiwyg = ({
fieldName,
label,
control,
rows = 3,
errors,
defaultValue,
config = {},
infoText = null
}) => {
const renderHeader = () => {
return (
<span className="ql-formats">
<button className="ql-bold" aria-label="Bold"></button>
<button className="ql-italic" aria-label="Italic"></button>
<button className="ql-underline" aria-label="Underline"></button>
<button className="ql-link" aria-label="Link"></button>
</span>
);
};
const header = renderHeader();
return (
<>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required || config.isRequired ? '*' : null}
</label>
<Controller
name={fieldName}
control={control}
defaultValue={defaultValue}
rules={config}
render={({ field, fieldState }) => (
<Editor
id={field.name}
{...field}
headerTemplate={header}
onTextChange={(e) => field.onChange(e.htmlValue)}
style={{ height: 80 * rows }}
className={classNames({ 'p-invalid': fieldState.invalid })}
/>
)}/>
{infoText ? <small>{infoText}</small> : null}
</>)
}
export default Wysiwyg;

View File

@@ -12,6 +12,8 @@ import NumberInput from './components/NumberInput';
import Switch from './components/Switch'; import Switch from './components/Switch';
import Select from './components/Select'; import Select from './components/Select';
import Radio from './components/Radio'; import Radio from './components/Radio';
import Wysiwyg from './components/Wysiwyg';
import Checkboxes from './components/Checkboxes';
const FormField = (props) => { const FormField = (props) => {
const fields = { const fields = {
@@ -23,7 +25,9 @@ const FormField = (props) => {
numberinput: NumberInput, numberinput: NumberInput,
switch: Switch, switch: Switch,
select: Select, select: Select,
radio: Radio radio: Radio,
wysiwyg: Wysiwyg,
checkboxes: Checkboxes
} }
const Comp = !isNil(fields[props.type]) ? fields[props.type] : null; const Comp = !isNil(fields[props.type]) ? fields[props.type] : null;

View File

@@ -3,6 +3,7 @@ import { classNames } from 'primereact/utils';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { head, isEmpty, isNil, pluck } from 'ramda'; import { head, isEmpty, isNil, pluck } from 'ramda';
import { diff } from 'deep-object-diff'; import { diff } from 'deep-object-diff';
import { klona } from 'klona';
// components // components
import { InputText } from 'primereact/inputtext'; import { InputText } from 'primereact/inputtext';
@@ -97,8 +98,10 @@ const FormFieldRepeater = ({
const storeFieldData = data ?? []; const storeFieldData = data ?? [];
setStateFieldData(storeFieldData); setStateFieldData(storeFieldData);
setStateOptionsData(prevState => { setStateOptionsData(prevState => {
const ids = pluck('id', storeFieldData) const ids = pluck('lookUpDataId', prevState)
const objectsToAdd = storeFieldData.filter(o => ids.includes(o.id)); const objectsToAdd = klona(storeFieldData)
.filter(o => !ids.includes(o.lookUpDataId))
.map(o => ({...o, id: null}));
return [...prevState, ...objectsToAdd]; return [...prevState, ...objectsToAdd];
}); });
} }
@@ -120,7 +123,7 @@ const FormFieldRepeater = ({
{stateFieldData.map((o, i) => <div key={i} className={classNames('appForm__repeaterItem')}> {stateFieldData.map((o, i) => <div key={i} className={classNames('appForm__repeaterItem')}>
<div className="p-inputgroup flex-1"> <div className="p-inputgroup flex-1">
{properField(o, i)} {properField(o, i)}
<Button icon="pi pi-times" className="p-button-danger" onClick={() => removeItem(i)}/> <Button type="button" icon="pi pi-times" className="p-button-danger" onClick={() => removeItem(i)}/>
</div> </div>
{isNil(o.lookUpDataId) && infoText ? <small>{infoText}</small> : null} {isNil(o.lookUpDataId) && infoText ? <small>{infoText}</small> : null}
</div>)} </div>)}

View File

@@ -1,7 +1,7 @@
import React, { useRef, useEffect, useState, useCallback } from 'react'; import React, { useRef, useEffect, useState, useCallback } from 'react';
import { classNames } from 'primereact/utils'; import { classNames } from 'primereact/utils';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { head, isNil, pluck } from 'ramda'; import { head, isEmpty, isNil, pluck } from 'ramda';
// components // components
import { InputText } from 'primereact/inputtext'; import { InputText } from 'primereact/inputtext';
@@ -9,6 +9,8 @@ import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu'; import { Menu } from 'primereact/menu';
import { Dropdown } from 'primereact/dropdown'; import { Dropdown } from 'primereact/dropdown';
import { InputNumber } from 'primereact/inputnumber'; import { InputNumber } from 'primereact/inputnumber';
import { diff } from 'deep-object-diff';
import { klona } from 'klona';
const FormFieldRepeaterCriteria = ({ const FormFieldRepeaterCriteria = ({
data, data,
@@ -99,6 +101,22 @@ const FormFieldRepeaterCriteria = ({
}) })
}, []); }, []);
useEffect(() => {
const diffData = diff(data[fieldName], stateFieldData);
if (!isEmpty(diffData)) {
const storeFieldData = data[fieldName] ?? [];
setStateFieldData(storeFieldData);
setStateOptionsData(prevState => {
const ids = pluck('lookUpDataId', prevState)
const objectsToAdd = klona(storeFieldData)
.filter(o => !ids.includes(o.lookUpDataId))
.map(o => ({...o, id: null, score: 0}));
return [...prevState, ...objectsToAdd];
});
}
}, [data]);
useEffect(() => { useEffect(() => {
setStateOptionsData([...options]); setStateOptionsData([...options]);
}, [options]); }, [options]);
@@ -126,7 +144,7 @@ const FormFieldRepeaterCriteria = ({
<label>{__('Nome criterio di valutazione', 'gepafin')}</label> <label>{__('Nome criterio di valutazione', 'gepafin')}</label>
<div className="p-inputgroup flex-1"> <div className="p-inputgroup flex-1">
{properField(o, i)} {properField(o, i)}
<Button icon="pi pi-times" className="p-button-danger" onClick={() => removeItem(i)}/> <Button type="button" icon="pi pi-times" className="p-button-danger" onClick={() => removeItem(i)}/>
</div> </div>
{isNil(o.lookUpDataId) && infoText ? <small>{infoText}</small> : null} {isNil(o.lookUpDataId) && infoText ? <small>{infoText}</small> : null}
</div> </div>

View File

@@ -30,7 +30,7 @@ const AppSidebar = () => {
{ {
label: __('Domande in lavorazione', 'gepafin'), label: __('Domande in lavorazione', 'gepafin'),
icon: 'pi pi-file', icon: 'pi pi-file',
href: '/bids', href: '/applications',
id: 11, id: 11,
enable: intersection(permissions, ['APPLY_CALLS']).length enable: intersection(permissions, ['APPLY_CALLS']).length
}, },

View File

@@ -3,12 +3,16 @@ import { __ } from '@wordpress/i18n';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
// components // components
import MyLatestSubmissionsTable from '../DashboardBenefeciario/components/MyLatestSubmissionsTable'; import MyLatestSubmissionsTable from '../DashboardBeneficiario/components/MyLatestSubmissionsTable';
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
const Bandi = () => { const Applications = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const gotToBandiDisponibili = () => {
navigate('')
}
return( return(
<div className="appPage"> <div className="appPage">
<div className="appPage__pageHeader"> <div className="appPage__pageHeader">
@@ -29,6 +33,10 @@ const Bandi = () => {
<div className="appPageSection"> <div className="appPageSection">
<div className="appPageSection__actions"> <div className="appPageSection__actions">
<Button
disabled={true}
onClick={gotToBandiDisponibili}
label={__('Bandi disponibili', 'gepafin')} icon="pi pi-bookmark" iconPos="right"/>
<Button <Button
disabled={true} disabled={true}
outlined outlined
@@ -41,4 +49,4 @@ const Bandi = () => {
) )
} }
export default Bandi; export default Applications;

View File

@@ -146,7 +146,7 @@ const AllBandiTable = () => {
style={{ minWidth: '10rem' }} style={{ minWidth: '10rem' }}
body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/> body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }} <Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }}
style={{ minWidth: '12rem' }} body={statusBodyTemplate} filter style={{ width: '120px' }} body={statusBodyTemplate} filter
filterElement={statusFilterTemplate}/> filterElement={statusFilterTemplate}/>
<Column header={__('Azioni', 'gepafin')} <Column header={__('Azioni', 'gepafin')}
body={actionsBodyTemplate}/> body={actionsBodyTemplate}/>

View File

@@ -0,0 +1,155 @@
import React, { useState, useEffect } from 'react';
import { __ } from '@wordpress/i18n';
import { useNavigate, useParams } from 'react-router-dom';
import { klona } from 'klona';
import { head, is, isNil } from 'ramda';
import { useForm } from 'react-hook-form';
// store
import { storeSet, useStore } from '../../store';
// api
import FormsService from '../../service/forms-service';
// components
import { Skeleton } from 'primereact/skeleton';
import { Button } from 'primereact/button';
import FormField from '../../components/FormField';
import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
import { formData as testformData } from '../../tempData';
import BandoService from '../../service/bando-service';
const BandoApplication = () => {
const { id } = useParams();
const navigate = useNavigate();
const [formData, setFormData] = useState([]);
const [formName, setFormName] = useState('');
const isAsyncRequest = useStore().main.isAsyncRequest();
const {
control,
handleSubmit,
formState: { errors },
getValues,
} = useForm({ defaultValues: {}, mode: 'onChange' });
const values = getValues();
const onSubmit = (formData) => {
const newFormData = Object.keys(formData).reduce((acc, cur) => {
acc.push({
'fieldId': cur,
'fieldValue': formData[cur] && formData[cur].getMonth ? formData[cur].toISOString() : formData[cur]
});
return acc;
}, []);
console.log('newFormData', newFormData)
};
const getBandoId = () => {
const parsed = parseInt(id)
return !isNaN(parsed) ? parsed : 0;
}
const getCallback = (data) => {
if (data.status === 'SUCCESS') {
/*const forms = data.data;
setFormName(forms[0].label);
const elements = klona(forms[0].content);
setFormData(elements);*/
//console.log('testformData.content', testformData.content);
setFormName(testformData.label);
setFormData(testformData.content);
}
storeSet.main.unsetAsyncRequest();
}
const errGetCallbacks = (data) => {
set404FromErrorResponse(data);
storeSet.main.unsetAsyncRequest();
}
useEffect(() => {
const bandoId = getBandoId();
if (bandoId) {
storeSet.main.setAsyncRequest();
FormsService.getFormsForCall(bandoId, getCallback, errGetCallbacks);
}
}, [id]);
return (
<div className="appPage">
{!isAsyncRequest
? <div className="appPage__pageHeader">
<h1>{formName}</h1>
</div>
: <>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="2rem" className="mb-8"></Skeleton>
</>}
<div className="appPage__spacer"></div>
{!isAsyncRequest
? <div className="appPage__content">
<form className="appForm" onSubmit={handleSubmit(onSubmit)}>
{formData.map(o => {
const label = head(o.settings.filter(o => o.name === 'label'));
const placeholder = head(o.settings.filter(o => o.name === 'placeholder'));
const options = head(o.settings.filter(o => o.name === 'options'));
const step = head(o.settings.filter(o => o.name === 'step'));
const mime = head(o.settings.filter(o => o.name === 'mime').join(','));
return <FormField
key={o.id}
type={o.name}
fieldName={o.id}
label={label ? label.value : ''}
placeholder={placeholder ? placeholder.value : ''}
control={control}
errors={errors}
defaultValue={values[o.id]}
maxFractionDigits={step}
accept={mime}
config={o.validators}
options={options ? options.value : []}
/>
})}
<div className="appPage__spacer"></div>
<div className="appPageSection__hr">
<span>{__('Azioni rapide', 'gepafin')}</span>
</div>
<div className="appPageSection">
<div className="appPageSection__actions">
<Button
outlined
label={__('Salva bozza', 'gepafin')} icon="pi pi-save" iconPos="right"/>
<Button
type="button"
disabled={true}
onClick={() => {
}}
label={__('Vai avanti', 'gepafin')} icon="pi pi-arrow-right" iconPos="right"/>
</div>
</div>
</form>
</div>
: <>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="2rem" className="mb-8"></Skeleton>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="4rem" className="mb-8"></Skeleton>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="2rem" className="mb-8"></Skeleton>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="4rem"></Skeleton>
</>}
</div>
)
}
export default BandoApplication;

View File

@@ -66,8 +66,9 @@ const BandoEditFormStep2 = forwardRef(function ({ initialData, getFormErrors },
summary: '', summary: '',
detail: __('Il bando è stato aggiornato corretamente!', 'gepafin') detail: __('Il bando è stato aggiornato corretamente!', 'gepafin')
}); });
setFormInitialData(data.data); const newFormData = {...formInitialData, ...data.data};
reset(); setFormInitialData(newFormData);
reset(newFormData);
} }
} }

View File

@@ -21,6 +21,7 @@ import BandoEditFormStep2 from './components/BandoEditFormStep2';
import { Messages } from 'primereact/messages'; import { Messages } from 'primereact/messages';
import FormsService from '../../service/forms-service'; import FormsService from '../../service/forms-service';
import set404FromErrorResponse from '../../helpers/set404FromErrorResponse'; import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
import BlockingOverlay from '../../components/BlockingOverlay';
const BandoEdit = () => { const BandoEdit = () => {
const isAsyncRequest = useStore().main.isAsyncRequest(); const isAsyncRequest = useStore().main.isAsyncRequest();
@@ -280,7 +281,7 @@ const BandoEdit = () => {
model={stepItems} model={stepItems}
activeIndex={activeStep} activeIndex={activeStep}
readOnly={false}/> readOnly={false}/>
: null} : <BlockingOverlay shouldDisplay={isAsyncRequest}/>}
<div className="appPage__spacer"></div> <div className="appPage__spacer"></div>

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState, useCallback, useRef } from 'react'; import React, { useEffect, useState, useCallback, useRef } from 'react';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { isEmpty } from 'ramda'; import { isEmpty, head } from 'ramda';
// store // store
import { storeSet, useStore } from '../../store'; import { storeSet, useStore } from '../../store';
@@ -17,6 +17,8 @@ import { Button } from 'primereact/button';
import { Dropdown } from 'primereact/dropdown'; import { Dropdown } from 'primereact/dropdown';
import FlowBuilder from '../../components/FlowBuilder'; import FlowBuilder from '../../components/FlowBuilder';
import { Messages } from 'primereact/messages'; import { Messages } from 'primereact/messages';
import FlowService from '../../service/flow-service';
import { confirmPopup, ConfirmPopup } from 'primereact/confirmpopup';
const BandoFlowEdit = () => { const BandoFlowEdit = () => {
const { id } = useParams(); const { id } = useParams();
@@ -39,17 +41,76 @@ const BandoFlowEdit = () => {
navigate(`/tenders/${bandoId}/forms`); navigate(`/tenders/${bandoId}/forms`);
} }
const confirmDelete = (event) => {
confirmPopup({
target: event.currentTarget,
message: __('Sei sicuro di reset questo flow?', 'gepafin'),
acceptLabel: __('Si', 'gepafin'),
icon: 'pi pi-info-circle',
defaultFocus: 'reject',
acceptClassName: 'p-button-danger',
accept: doDelete,
reject: () => {}
});
};
const doDelete = () => {
storeSet.main.flowData([]);
storeSet.main.flowEdges([]);
setInitialForm(0);
setFinalForm(0);
}
const updateInitialForm = (value) => {
setInitialForm(value);
if (forms.length === 2) {
const finalForm = head(forms.filter(o => o.id !== value));
if (finalForm) {
setFinalForm(finalForm.id);
}
}
}
const shoudDisableSaving = useCallback(() => { const shoudDisableSaving = useCallback(() => {
return isEmpty(flowData) || isEmpty(flowEdges) || isEmpty(initialForm) || isEmpty(finalForm); return forms.length > 2
? isEmpty(flowData) || isEmpty(flowEdges) || isEmpty(initialForm) || isEmpty(finalForm)
: isEmpty(flowEdges) || isEmpty(initialForm);
}, [flowData, flowEdges]); }, [flowData, flowEdges]);
const doSave = () => { const doSave = () => {
console.log('doSave', { storeSet.main.setAsyncRequest();
const bandoId = getBandoId();
const body = {
initialForm, initialForm,
finalForm, finalForm,
flowData, flowData,
flowEdges flowEdges
}); };
if (flowMsgs.current) {
flowMsgs.current.clear();
}
FlowService.createFlow(bandoId, body, getFlowCreateCallback, errGetFlowCreateCallback);
}
const getFlowCreateCallback = (data) => {
if (data.status === 'SUCCESS') {
if (flowMsgs.current) {
flowMsgs.current.show([
{
id: '99',
sticky: true, severity: 'success', summary: '',
detail: __('Flow è salvato.', 'gepafin'),
closable: false
}
]);
}
}
storeSet.main.unsetAsyncRequest();
}
const errGetFlowCreateCallback = (data) => {
set404FromErrorResponse(data);
storeSet.main.unsetAsyncRequest();
} }
const getFormsCallback = (data) => { const getFormsCallback = (data) => {
@@ -66,10 +127,27 @@ const BandoFlowEdit = () => {
storeSet.main.unsetAsyncRequest(); storeSet.main.unsetAsyncRequest();
} }
const getFlowCallback = (data) => {
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);
}
storeSet.main.unsetAsyncRequest();
}
const errGetFlowCallback = (data) => {
set404FromErrorResponse(data);
storeSet.main.unsetAsyncRequest();
}
useEffect(() => { useEffect(() => {
const bandoId = getBandoId(); const bandoId = getBandoId();
storeSet.main.setAsyncRequest(); storeSet.main.setAsyncRequest();
FormsService.getFormsForCall(bandoId, getFormsCallback, errGetFormsCallback); FormsService.getFormsForCall(bandoId, getFormsCallback, errGetFormsCallback);
FlowService.getFlow(bandoId, getFlowCallback, errGetFlowCallback)
}, [id]); }, [id]);
useEffect(() => { useEffect(() => {
@@ -112,7 +190,7 @@ const BandoFlowEdit = () => {
<Dropdown <Dropdown
id="initialForm" id="initialForm"
value={initialForm} value={initialForm}
onChange={(e) => setInitialForm(e.value)} onChange={(e) => updateInitialForm(e.value)}
optionDisabled={(opt) => finalForm === opt.value} optionDisabled={(opt) => finalForm === opt.value}
options={formOptions} options={formOptions}
optionLabel="label" optionLabel="label"
@@ -157,6 +235,13 @@ const BandoFlowEdit = () => {
disabled={shoudDisableSaving()} disabled={shoudDisableSaving()}
label={__('Salva', 'gepafin')} icon="pi pi-save" iconPos="right"/> label={__('Salva', 'gepafin')} icon="pi pi-save" iconPos="right"/>
</div> </div>
<div className="appPageSection__actions">
<ConfirmPopup/>
<Button
onClick={confirmDelete}
severity="warning"
label={__('Reset', 'gepafin')} icon="pi pi-refresh" iconPos="right"/>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { __ } from '@wordpress/i18n';
// components
import ElementSettingRepeater from '../ElementSettingRepeater';
import { InputText } from 'primereact/inputtext';
const ElementSetting = ({ setting, changeFn, updateDataFn }) => {
const settingLabels = {
label: __('Label', 'gepafin'),
placeholder: __('Segnaposto', 'gepafin'),
step: __('Precisione decimale', 'gepafin'),
options: __('Opzioni', 'gepafin'),
mime: __('Tipo di file', 'gepafin'),
}
return <div className="formElementSettings__field" key={setting.name}>
<label htmlFor={setting.name}>{settingLabels[setting.name]}</label>
{setting.name === 'options'
? <ElementSettingRepeater value={setting.value} name={setting.name} setDataFn={updateDataFn}/>
: <InputText id={setting.name} aria-describedby={`${setting.name}-help`}
value={setting.value}
onChange={(e) => changeFn(e.target.value, setting.name)}/>}
</div>
}
export default ElementSetting;

View File

@@ -12,7 +12,7 @@ import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import { TabView, TabPanel } from 'primereact/tabview'; import { TabView, TabPanel } from 'primereact/tabview';
import { InputSwitch } from 'primereact/inputswitch'; import { InputSwitch } from 'primereact/inputswitch';
import ElementSettingRepeater from './components/ElementSettingRepeater'; import ElementSetting from './components/ElementSetting';
const BuilderElementSettings = ({ closeSettings }) => { const BuilderElementSettings = ({ closeSettings }) => {
const elements = useStore().main.formElements(); const elements = useStore().main.formElements();
@@ -97,15 +97,11 @@ const BuilderElementSettings = ({ closeSettings }) => {
<TabView className="formElementSettings__tabs"> <TabView className="formElementSettings__tabs">
<TabPanel header={__('Presentation', 'gepafin')}> <TabPanel header={__('Presentation', 'gepafin')}>
{settings {settings
? settings.map((o) => <div className="formElementSettings__field" key={o.name}> ? settings.map((o) => <ElementSetting
<label htmlFor={o.name}>{o.name}</label> key={o.name}
{o.name === 'options' setting={o}
? changeFn={onChange}
<ElementSettingRepeater value={o.value} name={o.name} setDataFn={onUpdateOptions}/> updateDataFn={onUpdateOptions}/>)
: <InputText id={o.name} aria-describedby={`${o.name}-help`}
value={o.value}
onChange={(e) => onChange(e.target.value, o.name)}/>}
</div>)
: null} : null}
</TabPanel> </TabPanel>
<TabPanel header={__('Validation', 'gepafin')}> <TabPanel header={__('Validation', 'gepafin')}>

View File

@@ -11,11 +11,13 @@ import BuilderElementItem from '../BuilderElementItem';
import { Sidebar } from 'primereact/sidebar'; import { Sidebar } from 'primereact/sidebar';
import BuilderElementSettings from '../BuilderElementSettings'; import BuilderElementSettings from '../BuilderElementSettings';
import BuilderDropzone from '../BuilderDropzone'; import BuilderDropzone from '../BuilderDropzone';
import BlockingOverlay from '../../../../components/BlockingOverlay';
const FormBuilder = () => { const FormBuilder = () => {
const elements = useStore().main.formElements(); const elements = useStore().main.formElements();
const elementItems = useStore().main.elementItems(); const elementItems = useStore().main.elementItems();
const activeElement = useStore().main.activeElement(); const activeElement = useStore().main.activeElement();
const isAsyncRequest = useStore().main.isAsyncRequest();
const renderField = useCallback((field, index) => { const renderField = useCallback((field, index) => {
return ( return (
@@ -65,6 +67,7 @@ const FormBuilder = () => {
{elementItems.map((item) => renderItem(item))} {elementItems.map((item) => renderItem(item))}
</ul> </ul>
</div> </div>
<BlockingOverlay shouldDisplay={isAsyncRequest}/>
</div> </div>
</> </>
) )

View File

@@ -113,7 +113,7 @@ const BandoFormsEdit = () => {
const getElementItemsCallback = (data) => { const getElementItemsCallback = (data) => {
if (data.status === 'SUCCESS') { if (data.status === 'SUCCESS') {
storeSet.main.elementItems(elementItems); storeSet.main.elementItems(elementItems.sort((a, b) => a.sortOrder - b.sortOrder));
//storeSet.main.elementItems(data.data); //storeSet.main.elementItems(data.data);
} }
storeSet.main.unsetAsyncRequest(); storeSet.main.unsetAsyncRequest();
@@ -184,7 +184,7 @@ const BandoFormsEdit = () => {
<div className="appPageSection"> <div className="appPageSection">
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
{!isAsyncRequest ? <FormBuilder/> : null} <FormBuilder/>
</DndProvider> </DndProvider>
</div> </div>
@@ -198,10 +198,12 @@ const BandoFormsEdit = () => {
label={__('Indietro', 'gepafin')} icon="pi pi-arrow-left" iconPos="left"/> label={__('Indietro', 'gepafin')} icon="pi pi-arrow-left" iconPos="left"/>
<Button <Button
onClick={() => doSave()} onClick={() => doSave()}
disabled={isAsyncRequest}
label={__('Salva progressi', 'gepafin')} icon="pi pi-save" iconPos="right"/> label={__('Salva progressi', 'gepafin')} icon="pi pi-save" iconPos="right"/>
<Button <Button
outlined outlined
onClick={openPreview} onClick={openPreview}
disabled={isAsyncRequest}
label={__('Visualizza Anteprima Beneficiario', 'gepafin')} icon="pi pi-image" iconPos="right"/> label={__('Visualizza Anteprima Beneficiario', 'gepafin')} icon="pi pi-image" iconPos="right"/>
{/*<Button {/*<Button
disabled={true} disabled={true}
@@ -212,6 +214,7 @@ const BandoFormsEdit = () => {
<ConfirmPopup /> <ConfirmPopup />
<Button <Button
onClick={confirmDelete} onClick={confirmDelete}
disabled={isAsyncRequest}
severity="danger" severity="danger"
label={__('Cancella', 'gepafin')} icon="pi pi-trash" iconPos="right"/> label={__('Cancella', 'gepafin')} icon="pi pi-trash" iconPos="right"/>
</div> </div>

View File

@@ -32,22 +32,6 @@ const BandoView = () => {
navigate(`/tenders/${id}`); navigate(`/tenders/${id}`);
} }
const scaricaBando = () => {
}
const scaricaModulistica = () => {
}
const submitQuestion = () => {
}
const saveToFavourites = () => {
}
const getCallback = (data) => { const getCallback = (data) => {
if (data.status === 'SUCCESS') { if (data.status === 'SUCCESS') {
setData(getFormattedBandiData(data.data)); setData(getFormattedBandiData(data.data));
@@ -196,7 +180,7 @@ const BandoView = () => {
<div className="appPageSection"> <div className="appPageSection">
<h2>{__('FAQ', 'gepafin')}</h2> <h2>{__('FAQ', 'gepafin')}</h2>
<Accordion> <Accordion>
{data.faq.map((o, i) => <AccordionTab key={i} header={o.question}> {data.faq.map((o, i) => <AccordionTab key={i} header={o.value}>
<p> <p>
{o.response} {o.response}
</p> </p>
@@ -228,20 +212,20 @@ const BandoView = () => {
type="button" type="button"
disabled={true} disabled={true}
outlined outlined
onClick={scaricaBando} onClick={() => {}}
label={__('Scarica Bando Completo', 'gepafin')} label={__('Scarica Bando Completo', 'gepafin')}
icon="pi pi-download" iconPos="right"/> icon="pi pi-download" iconPos="right"/>
<Button <Button
type="button" type="button"
disabled={true} disabled={true}
outlined outlined
onClick={scaricaModulistica} onClick={() => {}}
label={__('Scarica Modulistica', 'gepafin')} label={__('Scarica Modulistica', 'gepafin')}
icon="pi pi-download" iconPos="right"/> icon="pi pi-download" iconPos="right"/>
<Button <Button
type="button" type="button"
disabled={true} disabled={true}
onClick={submitQuestion} onClick={() => {}}
label={__('Presenta Domanda', 'gepafin')} label={__('Presenta Domanda', 'gepafin')}
icon="pi pi-save" iconPos="right"/> icon="pi pi-save" iconPos="right"/>
<Button <Button
@@ -249,7 +233,7 @@ const BandoView = () => {
outlined outlined
rounded rounded
disabled={true} disabled={true}
onClick={saveToFavourites} onClick={() => {}}
label={__('Aggiungi a Preferiti', 'gepafin')} label={__('Aggiungi a Preferiti', 'gepafin')}
icon="pi pi-heart" iconPos="left"/> icon="pi pi-heart" iconPos="left"/>
</div> </div>

View File

@@ -0,0 +1,271 @@
import React, { useState, useEffect, useRef } from 'react';
import { __, sprintf } from '@wordpress/i18n';
import { useNavigate, useParams } from 'react-router-dom';
import { is, isEmpty } from 'ramda';
// store
import { storeSet, useStore } from '../../store';
// tools
import getNumberWithCurrency from '../../helpers/getNumberWithCurrency';
import getDateFromISOstring from '../../helpers/getDateFromISOstring';
// components
import { Skeleton } from 'primereact/skeleton';
import { Accordion } from 'primereact/accordion';
import { AccordionTab } from 'primereact/accordion';
import { InputTextarea } from 'primereact/inputtextarea';
import { Button } from 'primereact/button';
import BandoService from '../../service/bando-service';
import { Messages } from 'primereact/messages';
import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
const BandoViewBeneficiario = () => {
const isAsyncRequest = useStore().main.isAsyncRequest();
const { id } = useParams();
const navigate = useNavigate();
const [data, setData] = useState({});
const [newQuestion, setNewQuestion] = useState('');
const bandoMsgs = useRef(null);
const closePreview = () => {
navigate(`/tenders/${id}`);
}
const scaricaBando = () => {
}
const scaricaModulistica = () => {
}
const submitApplication = () => {
navigate(`/tenders/${id}/application`);
}
const saveToFavourites = () => {
}
const getCallback = (data) => {
if (data.status === 'SUCCESS') {
setData(getFormattedBandiData(data.data));
}
storeSet.main.unsetAsyncRequest();
}
const errGetCallback = (data) => {
if (bandoMsgs.current && data.message) {
bandoMsgs.current.show([
{
sticky: true, severity: 'error', summary: '',
detail: data.message,
closable: true
}
]);
}
set404FromErrorResponse(data);
storeSet.main.unsetAsyncRequest();
}
const getFormattedBandiData = (data) => {
data.dates = data.dates.map(v => is(String, v) ? new Date(v) : (v ? v : ''));
return data;
};
useEffect(() => {
const parsed = parseInt(id)
const bandoId = !isNaN(parsed) ? parsed : 0;
BandoService.getBando(bandoId, getCallback, errGetCallback);
}, [id]);
return (
<div className="appPage">
{!isAsyncRequest && !isEmpty(data)
? <div className="appPage__pageHeader">
<h1>{data.name}</h1>
<p>
{__('Data:', 'gepafin')}
<span>{getDateFromISOstring(data.createdDate)}</span>
</p>
</div>
: <>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="2rem" className="mb-8"></Skeleton>
</>}
<div className="appPage__spacer"></div>
<Messages ref={bandoMsgs}/>
{!isAsyncRequest && !isEmpty(data)
? <div className="appPage__content">
<picture className="appPageSection__hero">
<source srcSet={data.images[0] ? data.images[0].filePath : ''}/>
<img src={data.images[0] ? data.images[0].filePath : ''} alt={data.name}/>
</picture>
<div className="appPageSection__withBorder">
<h2>{__('Descrizione breve', 'gepafin')}</h2>
<div className="row rowContent">
<p>{data.descriptionShort}</p>
</div>
</div>
<div className="appPageSection__row">
<div className="appPageSection__withBorder">
<p className="appPageSection__pMeta">
<span>{__('Importo totale', 'gepafin')}</span>
<span>{getNumberWithCurrency(data.amount)}</span>
</p>
<p className="appPageSection__pMeta">
<span>{__('Importo massimo per progetto', 'gepafin')}</span>
<span>{getNumberWithCurrency(data.amountMax)}</span>
</p>
</div>
<div className="appPageSection__withBorder">
<p className="appPageSection__pMeta">
<span>{__('Data apertura', 'gepafin')}</span>
<span>{getDateFromISOstring(data.dates[0])}</span>
</p>
<p className="appPageSection__pMeta">
<span>{__('Data chiusura', 'gepafin')}</span>
<span>{getDateFromISOstring(data.dates[1])}</span>
</p>
</div>
</div>
<div className="appPageSection__withBorder">
<h2>{__('Descrizione dettagliata', 'gepafin')}</h2>
<div className="row rowContent">
<p>{data.descriptionLong}</p>
</div>
</div>
<div className="appPageSection__withBorder">
<h2>{__('Requisiti di Partecipazione', 'gepafin')}</h2>
<div className="row rowContent">
<ul>
{data.aimedTo.map((o, i) => <li key={i}>
{o.value}
</li>)}
</ul>
</div>
</div>
<div className="appPageSection__withBorder">
<h2>{__('Documentazione Richiesta', 'gepafin')}</h2>
<div className="row rowContent">
<p>{data.documentationRequested}</p>
</div>
</div>
<div className="appPageSection__withBorder">
<h2>{__('Criteri di Valutazione', 'gepafin')}</h2>
<div className="row rowContent">
<ul>
{data.criteria.map((o, i) => <li key={i}>
{`${o.value} ${sprintf(__('(%d punti)'), o.score)}`}
</li>)}
</ul>
</div>
</div>
<div className="appPageSection__withBorder">
<h2>{__('Allegati', 'gepafin')}</h2>
<div className="row rowContent">
<ul>
{data.docs.map((o, i) => <li key={i}>
<a href={o.filePath} target="_blank" rel="noreferrer">{o.name}</a>
</li>)}
</ul>
</div>
</div>
<div className="appPageSection">
<h2>{__('FAQ', 'gepafin')}</h2>
<Accordion>
{data.faq.map((o, i) => <AccordionTab key={i} header={o.value}>
<p>
{o.response}
</p>
</AccordionTab>)}
</Accordion>
</div>
<div className="appPageSection">
<h2>{__('Non hai trovato la risposta che cercavi?', 'gepafin')}</h2>
<div className="appForm__field">
<label htmlFor="newQuestion">{__('Fai una domanda', 'gepafin')}</label>
<InputTextarea
id="newQuestion"
rows={7}
value={newQuestion}
placeholder={__('Digita qui la tua domanda', 'gepafin')}
onChange={(e) => setNewQuestion(e.target.value)}
aria-describedby="newQuestion-help"/>
<small id="newQuestion-help">
{__('Riceverai una notifica quando ti risponderemo', 'gepafin')}
</small>
</div>
</div>
<div className="appPageSection">
<h2>{__('Download Documenti', 'gepafin')}</h2>
<div className="appPageSection__actions">
<Button
type="button"
disabled={true}
outlined
onClick={scaricaBando}
label={__('Scarica Bando Completo', 'gepafin')}
icon="pi pi-download" iconPos="right"/>
<Button
type="button"
disabled={true}
outlined
onClick={scaricaModulistica}
label={__('Scarica Modulistica', 'gepafin')}
icon="pi pi-download" iconPos="right"/>
<Button
type="button"
onClick={submitApplication}
label={__('Presenta Domanda', 'gepafin')}
icon="pi pi-save" iconPos="right"/>
<Button
type="button"
outlined
rounded
disabled={true}
onClick={saveToFavourites}
label={__('Aggiungi a Preferiti', 'gepafin')}
icon="pi pi-heart" iconPos="left"/>
</div>
</div>
<div className="appPageSection__withBorder">
<h2>{__('Contatti per Assistenza', 'gepafin')}</h2>
<div className="row rowContent">
<p>Email: bandi@gepafin.it</p>
<p>Telefono: +39 075 123 4567</p>
</div>
</div>
</div>
: <>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="2rem" className="mb-8"></Skeleton>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="4rem" className="mb-8"></Skeleton>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="2rem" className="mb-8"></Skeleton>
<Skeleton width="20%" height="1rem" className="mb-2"></Skeleton>
<Skeleton width="100%" height="4rem"></Skeleton>
</>}
</div>
)
}
export default BandoViewBeneficiario;

View File

@@ -25,6 +25,7 @@ import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar'; import { Calendar } from 'primereact/calendar';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import ProperBandoLabel from '../../../../components/ProperBandoLabel'; import ProperBandoLabel from '../../../../components/ProperBandoLabel';
import { Link } from 'react-router-dom';
const LatestBandiTable = () => { const LatestBandiTable = () => {
@@ -73,7 +74,8 @@ const LatestBandiTable = () => {
const getCallback = (data) => { const getCallback = (data) => {
if (data.status === 'SUCCESS') { if (data.status === 'SUCCESS') {
setItems(getFormattedBandiData(data.data)); const newItems = data.data.filter(o => o.status === 'PUBLISH');
setItems(getFormattedBandiData(newItems));
setStatuses(uniq(data.data.map(o => o.status))) setStatuses(uniq(data.data.map(o => o.status)))
initFilters(); initFilters();
} }
@@ -87,8 +89,8 @@ const LatestBandiTable = () => {
const getFormattedBandiData = (data) => { const getFormattedBandiData = (data) => {
return [...(data || [])].map((d) => { return [...(data || [])].map((d) => {
d.start_date = new Date(d.start_date); d.start_date = new Date(d.dates[0]);
d.end_date = new Date(d.end_date); d.end_date = new Date(d.dates[1]);
return d; return d;
}); });
@@ -122,8 +124,7 @@ const LatestBandiTable = () => {
name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] }, name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
start_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] }, start_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
end_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] }, end_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
submissions: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] }, status: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] }
status: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
}); });
setGlobalFilterValue(''); setGlobalFilterValue('');
}; };
@@ -168,6 +169,12 @@ const LatestBandiTable = () => {
return <Tag value={getBandoLabel(option)} severity={getBandoSeverity(option)} />; return <Tag value={getBandoLabel(option)} severity={getBandoSeverity(option)} />;
}; };
const actionsBodyTemplate = (rowData) => {
return <Link to={`/tenders/${rowData.id}`}>
<Button severity="info" label={__('Modifica', 'gepafin')} icon="pi pi-pencil" size="small" iconPos="right" />
</Link>
}
const header = renderHeader(); const header = renderHeader();
return( return(
@@ -185,12 +192,10 @@ const LatestBandiTable = () => {
<Column header={__('Data Scadenza', 'gepafin')} filterField="end_date" dataType="date" <Column header={__('Data Scadenza', 'gepafin')} filterField="end_date" dataType="date"
style={{ minWidth: '10rem' }} style={{ minWidth: '10rem' }}
body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/> body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column header={__('Domande ricevute', 'gepafin')} filterField="submissions" dataType="numeric" <Column field="status" header={__('Stato', 'gepafin')}
style={{ minWidth: '10rem' }} field="submissions" style={{ width: '120px' }} body={statusBodyTemplate} />
filter filterElement={balanceFilterTemplate}/> <Column header={__('Azioni', 'gepafin')}
<Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }} body={actionsBodyTemplate}/>
style={{ minWidth: '12rem' }} body={statusBodyTemplate} filter
filterElement={statusFilterTemplate}/>
</DataTable> </DataTable>
</div> </div>
) )

View File

@@ -0,0 +1,172 @@
import React, { useState, useEffect} from 'react';
import { __ } from '@wordpress/i18n';
import { uniq } from 'ramda';
// tools
import getBandoLabel from '../../../../helpers/getBandoLabel';
import getBandoSeverity from '../../../../helpers/getBandoSeverity';
// store
import { storeSet } from '../../../../store';
// api
import BandoService from '../../../../service/bando-service';
// components
import { FilterMatchMode, FilterOperator } from 'primereact/api';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
import { Dropdown } from 'primereact/dropdown';
import { InputNumber } from 'primereact/inputnumber';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
import { Tag } from 'primereact/tag';
import ProperBandoLabel from '../../../../components/ProperBandoLabel';
import { Link } from 'react-router-dom';
const LatestBandiTable = () => {
const [items, setItems] = useState(null);
const [filters, setFilters] = useState(null);
const [loading, setLoading] = useState(false);
const [globalFilterValue, setGlobalFilterValue] = useState('');
const [statuses, setStatuses] = useState([]);
useEffect(() => {
storeSet.main.setAsyncRequest();
BandoService.getBandi(getCallback, errGetCallbacks);
}, []);
const getCallback = (data) => {
if (data.status === 'SUCCESS') {
const newItems = data.data.filter(o => o.status === 'PUBLISH');
setItems(getFormattedBandiData(newItems));
setStatuses(uniq(data.data.map(o => o.status)))
initFilters();
}
storeSet.main.unsetAsyncRequest();
}
const errGetCallbacks = (data) => {
console.log('errGetCallbacks', data)
storeSet.main.unsetAsyncRequest();
}
const getFormattedBandiData = (data) => {
return [...(data || [])].map((d) => {
d.start_date = new Date(d.dates[0]);
d.end_date = new Date(d.dates[1]);
return d;
});
};
const formatDate = (value) => {
return value.toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const clearFilter = () => {
initFilters();
};
const onGlobalFilterChange = (e) => {
const value = e.target.value;
let _filters = { ...filters };
_filters['global'].value = value;
setFilters(_filters);
setGlobalFilterValue(value);
};
const initFilters = () => {
setFilters({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
start_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
end_date: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
status: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] }
});
setGlobalFilterValue('');
};
const renderHeader = () => {
return (
<div className="appTableHeader">
<Button type="button" icon="pi pi-filter-slash" label={__('Pulisci', 'gepafin')} outlined onClick={clearFilter} />
<IconField iconPosition="left">
<InputIcon className="pi pi-search" />
<InputText value={globalFilterValue} onChange={onGlobalFilterChange} placeholder={__('Cerca', 'gepafin')} />
</IconField>
</div>
);
};
const dateStartBodyTemplate = (rowData) => {
return formatDate(rowData.start_date);
};
const dateEndBodyTemplate = (rowData) => {
return formatDate(rowData.end_date);
};
const dateFilterTemplate = (options) => {
return <Calendar value={options.value} onChange={(e) => options.filterCallback(e.value, options.index)} dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999" />;
};
const balanceFilterTemplate = (options) => {
return <InputNumber value={options.value} onChange={(e) => options.filterCallback(e.value, options.index)} />;
};
const statusBodyTemplate = (rowData) => {
return <ProperBandoLabel status={rowData.status}/>;
};
const statusFilterTemplate = (options) => {
return <Dropdown value={options.value} options={statuses} onChange={(e) => options.filterCallback(e.value, options.index)} itemTemplate={statusItemTemplate} placeholder="Select One" className="p-column-filter" showClear />;
};
const statusItemTemplate = (option) => {
return <Tag value={getBandoLabel(option)} severity={getBandoSeverity(option)} />;
};
const actionsBodyTemplate = (rowData) => {
return <Link to={`/tenders/${rowData.id}/preview`}>
<Button severity="info" label={__('Partecipa', 'gepafin')} icon="pi pi-arrow-right" size="small" iconPos="right" />
</Link>
}
const header = renderHeader();
return(
<div className="appPageSection__table">
<DataTable value={items} paginator showGridlines rows={10} loading={loading} dataKey="id"
filters={filters}
globalFilterFields={['name', 'status']}
header={header}
emptyMessage="Nothing found." onFilter={(e) => setFilters(e.filters)}>
<Column field="name" header={__('Nome Bando', 'gepafin')} filter filterPlaceholder="Search by name"
style={{ minWidth: '12rem' }}/>
<Column header={__('Data Pubblicazione', 'gepafin')} filterField="start_date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateStartBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column header={__('Data Scadenza', 'gepafin')} filterField="end_date" dataType="date"
style={{ minWidth: '10rem' }}
body={dateEndBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column field="status" header={__('Stato', 'gepafin')}
style={{ width: '120px' }} body={statusBodyTemplate} />
<Column header={__('Azioni', 'gepafin')}
body={actionsBodyTemplate}/>
</DataTable>
</div>
)
}
export default LatestBandiTable;

View File

@@ -38,7 +38,8 @@ const MyLatestSubmissionsTable = () => {
modify_date: '2024-08-30T00:00:00+00:00', modify_date: '2024-08-30T00:00:00+00:00',
progress: 50, progress: 50,
status: 'DRAFT', status: 'DRAFT',
id: 11 id: 11,
callId: 11
}, },
{ {
name: 'Bando Sostenibilità 2024', name: 'Bando Sostenibilità 2024',
@@ -46,7 +47,8 @@ const MyLatestSubmissionsTable = () => {
modify_date: '2024-08-15T00:00:00+00:00', modify_date: '2024-08-15T00:00:00+00:00',
progress: 25, progress: 25,
status: 'DRAFT', status: 'DRAFT',
id: 9 id: 9,
callId: 12
} }
] ]
setItems(getFormattedBandiData(items)); setItems(getFormattedBandiData(items));
@@ -138,7 +140,7 @@ const MyLatestSubmissionsTable = () => {
}; };
const actionsBodyTemplate = (rowData) => { const actionsBodyTemplate = (rowData) => {
return <Link to={`/bids/${rowData.id}`}> return <Link to={`/tenders/${rowData.callId}/application`}>
<Button severity="info" label={__('Modifica', 'gepafin')} icon="pi pi-pencil" size="small" iconPos="right" /> <Button severity="info" label={__('Modifica', 'gepafin')} icon="pi pi-pencil" size="small" iconPos="right" />
</Link> </Link>
} }
@@ -161,7 +163,7 @@ const MyLatestSubmissionsTable = () => {
style={{ minWidth: '10rem' }} style={{ minWidth: '10rem' }}
body={dateModifyBodyTemplate} filter filterElement={dateFilterTemplate}/> body={dateModifyBodyTemplate} filter filterElement={dateFilterTemplate}/>
<Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }} <Column field="status" header={__('Stato', 'gepafin')} filterMenuStyle={{ width: '14rem' }}
style={{ minWidth: '12rem' }} body={statusBodyTemplate} filter style={{ width: '120px' }} body={statusBodyTemplate} filter
filterElement={statusFilterTemplate}/> filterElement={statusFilterTemplate}/>
<Column header={__('Progressi', 'gepafin')} <Column header={__('Progressi', 'gepafin')}
style={{ minWidth: '10rem' }} field="progress" style={{ minWidth: '10rem' }} field="progress"

View File

@@ -3,15 +3,15 @@ import { __ } from '@wordpress/i18n';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
// components // components
import LatestBandiTable from '../Dashboard/components/LatestBandiTable'; import LatestBandiTable from './components/LatestBandiTable';
import MyLatestSubmissionsTable from './components/MyLatestSubmissionsTable'; import MyLatestSubmissionsTable from './components/MyLatestSubmissionsTable';
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
const DashboardBenefeciario = () => { const DashboardBeneficiario = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const goToAllSubmissions = () => { const goToAllSubmissions = () => {
navigate('/bids/new'); navigate('/bids');
} }
return( return(
@@ -83,4 +83,4 @@ const DashboardBenefeciario = () => {
) )
} }
export default DashboardBenefeciario; export default DashboardBeneficiario;

View File

@@ -5,7 +5,8 @@ import PageNotFound from './pages/PageNotFound';
import Login from './pages/Login'; import Login from './pages/Login';
import ProtectedRoute from './components/ProtectedRoute'; import ProtectedRoute from './components/ProtectedRoute';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import DashboardBenefeciario from './pages/DashboardBenefeciario'; import DashboardBeneficiario from './pages/DashboardBeneficiario';
import BandoViewBeneficiario from './pages/BandoViewBeneficiario';
import DefaultLayout from './layouts/DefaultLayout'; import DefaultLayout from './layouts/DefaultLayout';
import Bandi from './pages/Bandi'; import Bandi from './pages/Bandi';
import BandoEdit from './pages/BandoEdit'; import BandoEdit from './pages/BandoEdit';
@@ -14,7 +15,8 @@ import BandoFormsEdit from './pages/BandoFormsEdit';
import BandoForms from './pages/BandoForms'; import BandoForms from './pages/BandoForms';
import BandoFormsPreview from './pages/BandoFormsPreview'; import BandoFormsPreview from './pages/BandoFormsPreview';
import BandoFlowEdit from './pages/BandoFlowEdit'; import BandoFlowEdit from './pages/BandoFlowEdit';
import Bids from './pages/Bids'; import Applications from './pages/Applications';
import BandoApplication from './pages/BandoApplication';
const routes = ({ role }) => { const routes = ({ role }) => {
return ( return (
@@ -22,7 +24,7 @@ const routes = ({ role }) => {
<Route element={<ProtectedRoute/>}> <Route element={<ProtectedRoute/>}>
<Route path="/" element={<DefaultLayout> <Route path="/" element={<DefaultLayout>
{'ROLE_SUPER_ADMIN' === role ? <Dashboard/> : null} {'ROLE_SUPER_ADMIN' === role ? <Dashboard/> : null}
{'ROLE_BENEFICIARY' === role ? <DashboardBenefeciario/> : null} {'ROLE_BENEFICIARY' === role ? <DashboardBeneficiario/> : null}
</DefaultLayout>}/> </DefaultLayout>}/>
<Route path="/tenders" element={<DefaultLayout> <Route path="/tenders" element={<DefaultLayout>
{'ROLE_SUPER_ADMIN' === role ? <Bandi/> : null} {'ROLE_SUPER_ADMIN' === role ? <Bandi/> : null}
@@ -32,6 +34,7 @@ const routes = ({ role }) => {
</DefaultLayout>}/> </DefaultLayout>}/>
<Route path="/tenders/:id/preview" element={<DefaultLayout> <Route path="/tenders/:id/preview" element={<DefaultLayout>
{'ROLE_SUPER_ADMIN' === role ? <BandoView/> : null} {'ROLE_SUPER_ADMIN' === role ? <BandoView/> : null}
{'ROLE_BENEFICIARY' === role ? <BandoViewBeneficiario/> : null}
</DefaultLayout>}/> </DefaultLayout>}/>
<Route path="/tenders/:id/preview-evaluation" element={<DefaultLayout> <Route path="/tenders/:id/preview-evaluation" element={<DefaultLayout>
{'ROLE_SUPER_ADMIN' === role ? <BandoView/> : null} {'ROLE_SUPER_ADMIN' === role ? <BandoView/> : null}
@@ -48,8 +51,11 @@ const routes = ({ role }) => {
<Route path="/tenders/:id/flow" element={<DefaultLayout> <Route path="/tenders/:id/flow" element={<DefaultLayout>
{'ROLE_SUPER_ADMIN' === role ? <BandoFlowEdit/> : null} {'ROLE_SUPER_ADMIN' === role ? <BandoFlowEdit/> : null}
</DefaultLayout>}/> </DefaultLayout>}/>
<Route path="/bids" element={<DefaultLayout> <Route path="/tenders/:id/application/" element={<DefaultLayout>
{'ROLE_BENEFICIARY' === role ? <Bids/> : null} {'ROLE_BENEFICIARY' === role ? <BandoApplication/> : null}
</DefaultLayout>}/>
<Route path="/applications" element={<DefaultLayout>
{'ROLE_BENEFICIARY' === role ? <Applications/> : null}
</DefaultLayout>}/> </DefaultLayout>}/>
</Route> </Route>
<Route exact path="/login" element={<Login/>}/> <Route exact path="/login" element={<Login/>}/>

View File

@@ -0,0 +1,14 @@
import { NetworkService } from './network-service';
const API_BASE_URL = process.env.REACT_APP_API_EXECUTION_ADDRESS;
export default class FlowService {
static getFlow = (id, callback, errCallback) => {
NetworkService.get(`${API_BASE_URL}/flow/call/${id}`, callback, errCallback);
};
static createFlow = (id, body, callback, errCallback) => {
NetworkService.put(`${API_BASE_URL}/flow/call/${id}`, body, callback, errCallback);
};
}

View File

@@ -95,22 +95,22 @@ export const formData = {
label: 'La forma per Innovazione digitale 2024', label: 'La forma per Innovazione digitale 2024',
content: [ content: [
{ {
"id": "aec5ee1885", "id": "a9a8aeb479",
"name": "textinput", "name": "textinput",
"label": "Text Input", "label": "Testo Breve",
"settings": [ "settings": [
{ {
"name": "label", "name": "label",
"value": "Text input" "value": "Testo Breve"
}, },
{ {
"name": "placeholder", "name": "placeholder",
"value": "Placeholder text" "value": ""
} }
], ],
"validators": { "validators": {
"isRequired": false, "isRequired": true,
"minLength": null, "minLength": "3",
"maxLength": null, "maxLength": null,
"pattern": null, "pattern": null,
"custom": null "custom": null
@@ -118,17 +118,17 @@ export const formData = {
"dbId": 1 "dbId": 1
}, },
{ {
"id": "a730f1f4d0", "id": "a20469fc97",
"name": "textarea", "name": "textarea",
"label": "Text Area", "label": "Testo Lungo",
"settings": [ "settings": [
{ {
"name": "label", "name": "label",
"value": "Text area" "value": "Testo Lungo"
}, },
{ {
"name": "placeholder", "name": "placeholder",
"value": "Placeholder text" "value": ""
} }
], ],
"validators": { "validators": {
@@ -141,69 +141,46 @@ export const formData = {
"dbId": 2 "dbId": 2
}, },
{ {
"id": "aa8746a7c3", "id": "a21dc560f6",
"name": "textinput", "name": "wysiwyg",
"label": "P.IVA", "label": "Campo di Testo Formattato",
"settings": [ "settings": [
{ {
"name": "label", "name": "label",
"value": "P.IVA" "value": "Testo Formattato"
},
{
"name": "placeholder",
"value": "Insert p.iva number"
}
],
"validators": {
"isRequired": true,
"minLength": null,
"maxLength": null,
"pattern": null,
"custom": "isValidVAT"
},
"dbId": 4
},
{
"id": "ae3dde17cd",
"name": "radio",
"label": "Radio Input",
"settings": [
{
"name": "label",
"value": "Radio input"
},
{
"name": "options",
"value": [
{
"name": "opt1",
"label": "Opt1"
}
]
}
],
"validators": {
"isRequired": false,
"min": null,
"max": null,
"custom": null
},
"dbId": 5
},
{
"id": "abf838016f",
"name": "textinput",
"label": "Number Input",
"settings": [
{
"name": "label",
"value": "Number"
}, },
{ {
"name": "placeholder", "name": "placeholder",
"value": "" "value": ""
} }
], ],
"validators": {
"isRequired": false,
"minLength": null,
"maxLength": null,
"pattern": null,
"custom": null
},
"dbId": 3
},
{
"id": "a5c3860c1a",
"name": "numberinput",
"label": "Campo Numerico",
"settings": [
{
"name": "label",
"value": "Numero"
},
{
"name": "placeholder",
"value": 0
},
{
"name": "step",
"value": 0
}
],
"validators": { "validators": {
"isRequired": false, "isRequired": false,
"min": null, "min": null,
@@ -211,7 +188,146 @@ export const formData = {
"pattern": null, "pattern": null,
"custom": null "custom": null
}, },
"dbId": 3 "dbId": 4
},
{
"id": "a7252ecc8d",
"name": "radio",
"label": "Scelta Singola",
"settings": [
{
"name": "label",
"value": "Scelta Singola"
},
{
"name": "options",
"value": [
{
"name": "o8df4ffa62",
"label": "Radio opzione A"
},
{
"name": "o3ed6fb4d8",
"label": "Radio opzione B"
}
]
}
],
"validators": {
"isRequired": true,
"custom": null
},
"dbId": 5
},
{
"id": "a778783c9d",
"name": "select",
"label": "Menu a Tendina",
"settings": [
{
"name": "label",
"value": "Menu a Tendina"
},
{
"name": "options",
"value": [
{
"name": "od9f50d8a8",
"label": "Opzione A"
},
{
"name": "o8cb208732",
"label": "Opzione B"
}
]
}
],
"validators": {
"isRequired": false,
"custom": null
},
"dbId": 6
},
{
"id": "afee29df1a",
"name": "switch",
"label": "Casella di Spunta",
"settings": [
{
"name": "label",
"value": "Casella di Spunta"
}
],
"validators": {
"isRequired": false
},
"dbId": 8
},
{
"id": "a5fdbd77df",
"name": "checkboxes",
"label": "Scelta Multipla",
"settings": [
{
"name": "label",
"value": "Scelta Multipla"
},
{
"name": "options",
"value": [
{
"name": "o55ea20665",
"label": "Opz checkbox A"
},
{
"name": "oc10db3d79",
"label": "Opz checkbox B"
}
]
}
],
"validators": {
"isRequired": true,
"custom": null
},
"dbId": 7
},
{
"id": "a2810fd8a1",
"name": "fileupload",
"label": "Caricamento File",
"settings": [
{
"name": "label",
"value": "Caricamento File"
},
{
"name": "mime",
"value": ['image/jpeg', 'image/png']
}
],
"validators": {
"isRequired": true,
"maxSize": 100000,
"custom": null
},
"dbId": 10
},
{
"id": "ae14c94da7",
"name": "datepicker",
"label": "Data",
"settings": [
{
"name": "label",
"value": "Data"
}
],
"validators": {
"isRequired": true,
"custom": null
},
"dbId": 9
} }
] ]
}; };
@@ -219,16 +335,18 @@ export const formData = {
export const elementItems = [ export const elementItems = [
{ {
id: 1, id: 1,
sortOrder: 1,
name: 'textinput', name: 'textinput',
label: 'Text Input', label: 'Testo Breve',
description: 'Per risposte concise (nomi, titoli, brevi descrizioni)',
settings: [ settings: [
{ {
name: "label", name: "label",
value: "Text input" value: "Testo Breve"
}, },
{ {
name: "placeholder", name: "placeholder",
value: "Placeholder text" value: ""
} }
], ],
validators: { validators: {
@@ -241,16 +359,18 @@ export const elementItems = [
}, },
{ {
id: 2, id: 2,
sortOrder: 2,
name: 'textarea', name: 'textarea',
label: 'Text Area', label: 'Testo Lungo',
description: 'Campo di testo esteso per paragrafi, descrizioni, proposte',
settings: [ settings: [
{ {
name: "label", name: "label",
value: "Text area" value: "Testo Lungo"
}, },
{ {
name: "placeholder", name: "placeholder",
value: "Placeholder text" value: ""
} }
], ],
validators: { validators: {
@@ -263,15 +383,44 @@ export const elementItems = [
}, },
{ {
id: 3, id: 3,
name: 'numberinput', sortOrder: 3,
label: 'Number Input', name: 'wysiwyg',
label: 'Campo di Testo Formattato',
description: 'Editor avanzato per testo con formattazione',
settings: [ settings: [
{ {
name: "label", name: "label",
value: "Number" value: "Testo Formattato"
}, },
{ {
name: "placeholder", name: "placeholder",
value: ""
}
],
validators: {
isRequired: false,
minLength: null,
maxLength: null,
custom: null
}
},
{
id: 4,
sortOrder: 4,
name: 'numberinput',
label: 'Campo Numerico',
description: "Per l'inserimento di valori numerici (quantità, importi, percentuali)",
settings: [
{
name: "label",
value: "Numero"
},
{
name: "placeholder",
value: 0
},
{
name: "step",
value: 0 value: 0
} }
], ],
@@ -283,43 +432,20 @@ export const elementItems = [
custom: null custom: null
} }
}, },
{
id: 4,
name: 'textinput',
label: 'P.IVA',
settings: [
{
name: "label",
value: "P.IVA"
},
{
name: "placeholder",
value: "Insert p.iva number"
}
],
validators: {
isRequired: true,
minLength: null,
maxLength: null,
pattern: null,
custom: 'isValidVAT'
}
},
{ {
id: 5, id: 5,
sortOrder: 5,
name: 'radio', name: 'radio',
label: 'Radio Input', label: 'Scelta Singola',
description: 'Gruppo di opzioni per selezione singola',
settings: [ settings: [
{ {
name: "label", name: "label",
value: "Radio input" value: "Scelta Singola"
}, },
{ {
name: "options", name: "options",
value: [ value: []
{ name: "opt1", label: "Opt1" },
{ name: "opt2", label: "Opt2" }
]
} }
], ],
validators: { validators: {
@@ -329,19 +455,18 @@ export const elementItems = [
}, },
{ {
id: 6, id: 6,
sortOrder: 6,
name: 'select', name: 'select',
label: 'Select', label: 'Menu a Tendina',
description: 'Selezione da opzioni predefinite',
settings: [ settings: [
{ {
name: "label", name: "label",
value: "Select" value: "Menu a Tendina"
}, },
{ {
name: "options", name: "options",
value: [ value: []
{ name: "opt1", label: "Opt1" },
{ name: "opt2", label: "Opt2" }
]
} }
], ],
validators: { validators: {
@@ -351,21 +476,85 @@ export const elementItems = [
}, },
{ {
id: 7, id: 7,
name: 'datepicker', sortOrder: 7,
label: 'Datepicker', name: 'checkboxes',
label: 'Scelta Multipla',
description: 'Gruppo di opzioni per selezione singola o multipla',
settings: [ settings: [
{ {
name: "label", name: "label",
value: "Datepicker" value: "Scelta Multipla"
},
{
name: "options",
value: []
}
],
validators: {
isRequired: false,
min: null,
max: null,
custom: null
}
},
{
id: 8,
sortOrder: 8,
name: 'switch',
label: 'Casella di Spunta',
description: 'Per selezioni binarie, accettazioni, conferme',
settings: [
{
name: "label",
value: "Casella di Spunta"
}
],
validators: {
isRequired: false
}
},
{
id: 9,
sortOrder: 9,
name: 'datepicker',
label: 'Data',
description: 'Selezione di data',
settings: [
{
name: "label",
value: "Data"
} }
], ],
validators: { validators: {
isRequired: false, isRequired: false,
custom: null custom: null
} }
},
{
id: 10,
sortOrder: 10,
name: 'fileupload',
label: 'Caricamento File',
description: "Per l'upload di documenti o immagini",
settings: [
{
name: "label",
value: "Caricamento File"
},
{
name: "mime",
value: []
}
],
validators: {
isRequired: false,
maxSize: 100000,
custom: null
}
} }
] ]
/*
const flowData = { const flowData = {
"initialForm":9, "initialForm":9,
"finalForm":13, "finalForm":13,
@@ -412,4 +601,4 @@ const flowData = {
"type":"smoothstep" "type":"smoothstep"
} }
] ]
} }*/

View File

@@ -4962,6 +4962,11 @@ eventemitter3@^4.0.0:
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
eventemitter3@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz"
integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
events@^3.2.0: events@^3.2.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
@@ -5039,6 +5044,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^3.2.9, fast-glob@^3.3.0: fast-glob@^3.2.9, fast-glob@^3.3.0:
version "3.3.2" version "3.3.2"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
@@ -6939,11 +6949,26 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.mapvalues@^4.6.0: lodash.mapvalues@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz" resolved "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz"
@@ -7510,6 +7535,11 @@ param-case@^3.0.4:
dot-case "^3.0.4" dot-case "^3.0.4"
tslib "^2.0.3" tslib "^2.0.3"
parchment@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz"
integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@@ -8331,6 +8361,25 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quill-delta@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz"
integrity sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==
dependencies:
fast-diff "^1.3.0"
lodash.clonedeep "^4.5.0"
lodash.isequal "^4.5.0"
quill@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz"
integrity sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==
dependencies:
eventemitter3 "^5.0.1"
lodash-es "^4.17.21"
parchment "^3.0.0"
quill-delta "^5.1.0"
raf@^3.4.1: raf@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz"