Merge branch 'develop' of github.com:Kitzanos/GEPAFIN-FE into develop

This commit is contained in:
Vitalii Kiiko
2025-05-13 10:19:50 +02:00
10 changed files with 319 additions and 117 deletions

View File

@@ -1,90 +1,91 @@
{ {
"name": "bflows-gepafin", "name": "bflows-gepafin",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11", "@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@babel/preset-react": "7.26.3", "@babel/preset-react": "7.26.3",
"@date-fns/tz": "1.2.0", "@date-fns/tz": "1.2.0",
"@emailjs/browser": "4.4.1", "@emailjs/browser": "4.4.1",
"@number-flow/react": "0.5.9", "@number-flow/react": "0.5.9",
"@sentry/browser": "9.11.0", "@sentry/browser": "9.11.0",
"@stomp/stompjs": "7.1.1", "@stomp/stompjs": "7.1.1",
"@tanstack/react-table": "8.21.2", "@tanstack/react-table": "8.21.2",
"@wordpress/i18n": "5.21.0", "@wordpress/i18n": "5.21.0",
"@wordpress/react-i18n": "4.21.0", "@wordpress/react-i18n": "4.21.0",
"codice-fiscale-js": "2.3.22", "codice-fiscale-js": "2.3.22",
"copy-to-clipboard": "3.3.3", "copy-to-clipboard": "3.3.3",
"deep-object-diff": "1.1.9", "deep-object-diff": "1.1.9",
"dompurify": "3.2.5", "dompurify": "3.2.5",
"expression-language": "1.2.0", "expression-language": "1.2.0",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"hotkeys-js": "3.13.9", "hotkeys-js": "3.13.9",
"html-react-parser": "5.2.3", "html-react-parser": "5.2.3",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"klona": "2.0.6", "klona": "2.0.6",
"leader-line-new": "1.1.9", "leader-line-new": "1.1.9",
"luxon": "3.6.1", "luxon": "3.6.1",
"mathjs": "14.4.0", "mathjs": "14.4.0",
"mustache": "4.2.0", "mustache": "4.2.0",
"object-path-immutable": "4.1.2", "object-path-immutable": "4.1.2",
"primeicons": "7.0.0", "primeicons": "7.0.0",
"primereact": "10.9.4", "primereact": "10.9.4",
"quill": "2.0.3", "quill": "2.0.3",
"ramda": "0.30.1", "ramda": "0.30.1",
"react": "19.1.0", "react": "19.1.0",
"react-dnd": "16.0.1", "react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1", "react-dnd-html5-backend": "16.0.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hook-form": "7.55.0", "react-hook-form": "7.55.0",
"react-router-dom": "7.5.0", "react-router-dom": "7.5.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"recharts": "2.15.2", "recharts": "2.15.2",
"sockjs-client": "1.6.1", "sockjs-client": "1.6.1",
"validate.js": "0.13.1", "validate.js": "0.13.1",
"zustand": "5.0.3", "zustand": "5.0.3",
"zustand-x": "6.1.0" "zustand-x": "6.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.27.0", "@babel/cli": "7.27.0",
"@babel/core": "7.26.10", "@babel/core": "7.26.10",
"@babel/plugin-syntax-jsx": "7.25.9", "@babel/plugin-syntax-jsx": "7.25.9",
"@wordpress/babel-plugin-makepot": "6.21.0", "@wordpress/babel-plugin-makepot": "6.21.0",
"babel-plugin-macros": "3.1.0", "babel-plugin-macros": "3.1.0",
"node-wp-i18n": "1.2.7", "node-wp-i18n": "1.2.7",
"sass": "1.86.3", "sass": "1.86.3",
"sass-loader": "16.0.5" "sass-loader": "16.0.5"
}, },
"scripts": { "scripts": {
"start": "GENERATE_SOURCEMAP=false react-scripts start", "start": "GENERATE_SOURCEMAP=false react-scripts start",
"start:dev": "cp environments/dev/* public/loaded-files && rm public/loaded-files/dev.env && cp environments/dev/dev.env .env && PORT=8000 react-scripts start --mode development", "start2": "react-scripts start",
"start:prod": "cp environments/prod/* public/loaded-files && rm public/loaded-files/prod.env && cp environments/prod/prod.env .env && react-scripts start --mode production", "start:dev": "cp environments/dev/* public/loaded-files && rm public/loaded-files/dev.env && cp environments/dev/dev.env .env && PORT=8000 react-scripts start --mode development",
"build": "react-scripts build", "start:prod": "cp environments/prod/* public/loaded-files && rm public/loaded-files/prod.env && cp environments/prod/prod.env .env && react-scripts start --mode production",
"build:dev": "cp environments/dev/* public/loaded-files && rm public/loaded-files/dev.env && cp environments/dev/dev.env .env && react-scripts build --mode development", "build": "react-scripts build",
"build:prod": "cp environments/prod/* public/loaded-files && rm public/loaded-files/prod.env && cp environments/prod/prod.env .env && react-scripts build --mode production", "build:dev": "cp environments/dev/* public/loaded-files && rm public/loaded-files/dev.env && cp environments/dev/dev.env .env && react-scripts build --mode development",
"test": "react-scripts test", "build:prod": "cp environments/prod/* public/loaded-files && rm public/loaded-files/prod.env && cp environments/prod/prod.env .env && react-scripts build --mode production",
"eject": "react-scripts eject", "test": "react-scripts test",
"make-pot": "wpi18n makepot --domain-path=languages --domain=gepafin" "eject": "react-scripts eject",
}, "make-pot": "wpi18n makepot --domain-path=languages --domain=gepafin"
"eslintConfig": { },
"extends": [ "eslintConfig": {
"react-app", "extends": [
"react-app/jest" "react-app",
], "react-app/jest"
"rules": { ],
"react-hooks/exhaustive-deps": "off" "rules": {
"react-hooks/exhaustive-deps": "off"
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
} }
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
} }

View File

@@ -413,6 +413,10 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.appPageSection__actions:empty {
display: none;
}
.appPageSection__tableActions { .appPageSection__tableActions {
display: flex; display: flex;
gap: 10px; gap: 10px;

View File

@@ -13,6 +13,7 @@ import getPropeMimeLabels from '../../../../helpers/getPropeMimeLabels';
import { FileUpload } from 'primereact/fileupload'; import { FileUpload } from 'primereact/fileupload';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
import { Messages } from 'primereact/messages';
import { defaultMaxFileSize, mimeTypes } from '../../../../configData'; import { defaultMaxFileSize, mimeTypes } from '../../../../configData';
import getFormatedFileSizeText from '../../../../helpers/getFormatedFileSizeText'; import getFormatedFileSizeText from '../../../../helpers/getFormatedFileSizeText';
@@ -47,24 +48,48 @@ const Fileupload = ({
const [acceptFormats, setAcceptFormats] = useState(''); const [acceptFormats, setAcceptFormats] = useState('');
const [formatsForInput, setFormatsForInput] = useState(''); const [formatsForInput, setFormatsForInput] = useState('');
const inputRef = useRef(); const inputRef = useRef();
const messagesRef = useRef(null);
const customBase64Uploader = (event) => { const customBase64Uploader = (event) => {
const formData = new FormData() const formData = new FormData()
const filesToUpload = [];
const uploadedFiles = inputRef.current ? inputRef.current.getUploadedFiles() : [];
const currentFiles = stateFieldData;
for (const file of event.files) { for (const file of event.files) {
formData.append('file', file) const isDuplicate = [...uploadedFiles, ...currentFiles].some(
(uploadedFile)=>uploadedFile.name === file.name
);
if(isDuplicate){
messagesRef.current.show({
severity: 'error',
summary: __('Attenzione', 'gepafin'),
detail: `Il file con nome "${file.name}" è già stato caricato.`,
life: 10000
});
} else {
formData.append('file', file)
filesToUpload.push(file);
}
}
if(filesToUpload.length > 0 ){
FileUploadService.uploadFile(sourceId, formData, callback, errorCallback, [
['documentType', doctype.toUpperCase()],
['sourceType', source.toUpperCase()]
]);
} }
FileUploadService.uploadFile(sourceId, formData, callback, errorCallback, [
['documentType', doctype.toUpperCase()],
['sourceType', source.toUpperCase()]
]);
}; };
const callback = (data) => { const callback = (data, uploadedFiles) => {
if (data.status === 'SUCCESS') { if (data.status === 'SUCCESS') {
setStateFieldData(data.data); setStateFieldData(prevState => [...prevState, ...data.data]);
const uploadedFiles = inputRef.current.getUploadedFiles(); const currentUploadedFiles = inputRef.current.getUploadedFiles() || [];
setDataFn(fieldName, [...uploadedFiles, ...data.data], { shouldValidate: true });
inputRef.current.setFiles([]); inputRef.current.setFiles([]);
setDataFn(fieldName, [...currentUploadedFiles, ...data.data], { shouldValidate: true });
saveFormCallback(); saveFormCallback();
} }
} }
@@ -204,6 +229,7 @@ const Fileupload = ({
return ( return (
sourceId || sourceId === 0 sourceId || sourceId === 0
? <> ? <>
<Messages ref={messagesRef} />
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}> <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required || config.isRequired ? {label}{config.required || config.isRequired ?
<span className="appForm__field--required">*</span> : null} <span className="appForm__field--required">*</span> : null}

View File

@@ -13,6 +13,8 @@ import { FileUpload } from 'primereact/fileupload';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
import { head, isEmpty } from 'ramda'; import { head, isEmpty } from 'ramda';
import { Messages } from 'primereact/messages';
import { defaultMaxFileSize, mimeTypes } from '../../../../configData'; import { defaultMaxFileSize, mimeTypes } from '../../../../configData';
import getFormatedFileSizeText from '../../../../helpers/getFormatedFileSizeText'; import getFormatedFileSizeText from '../../../../helpers/getFormatedFileSizeText';
@@ -42,24 +44,50 @@ const FileuploadAsync = ({
const [acceptFormats, setAcceptFormats] = useState(''); const [acceptFormats, setAcceptFormats] = useState('');
const [formatsForInput, setFormatsForInput] = useState(''); const [formatsForInput, setFormatsForInput] = useState('');
const inputRef = useRef(); const inputRef = useRef();
const messagesRef = useRef(null);
const customBase64Uploader = (event) => { const customBase64Uploader = (event) => {
const formData = new FormData() const formData = new FormData()
const filesToUpload = [];
const uploadedFiles = inputRef.current ? inputRef.current.getUploadedFiles() : [];
const currentFiles = stateFieldData;
for (const file of event.files) { for (const file of event.files) {
formData.append('file', file) const isDuplicate = [...uploadedFiles, ...currentFiles].some(
(uploadedFile)=>uploadedFile.name === file.name
);
if(isDuplicate){
messagesRef.current.show({
severity: 'error',
summary: __('Attenzione', 'gepafin'),
detail: `Il file con nome "${file.name}" è già stato caricato.`,
life: 10000
});
} else {
formData.append('file', file)
filesToUpload.push(file);
}
}
if(filesToUpload.length > 0){
FileUploadService.uploadFile(sourceId, formData, callback, errorCallback, [
['documentType', doctype.toUpperCase()],
['sourceType', source.toUpperCase()]
]);
} }
FileUploadService.uploadFile(sourceId, formData, callback, errorCallback, [
['documentType', doctype.toUpperCase()],
['sourceType', source.toUpperCase()]
]);
}; };
const callback = (data) => { const callback = (data, uploadedFiles) => {
if (data.status === 'SUCCESS') { if (data.status === 'SUCCESS') {
setStateFieldData(data.data); setStateFieldData(prevState => [...prevState, ...data.data]);
const uploadedFiles = inputRef.current.getUploadedFiles(); const currentUploadedFiles = inputRef.current.getUploadedFiles() || [];
setDataFn(fieldName, [...uploadedFiles, ...data.data], { shouldValidate: true });
inputRef.current.setUploadedFiles([...currentUploadedFiles, ...data.data]);
inputRef.current.setFiles([]); inputRef.current.setFiles([]);
setDataFn(fieldName, [...currentUploadedFiles, ...data.data], { shouldValidate: true });
} }
} }
@@ -186,6 +214,7 @@ const FileuploadAsync = ({
return ( return (
sourceId && sourceId !== 0 sourceId && sourceId !== 0
? <> ? <>
<Messages ref={messagesRef} />
<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 ? '*' : null}
{acceptFormats ? ' (' + getPropeMimeLabels(accept) + ')' : null} {acceptFormats ? ' (' + getPropeMimeLabels(accept) + ')' : null}

View File

@@ -263,3 +263,8 @@ export const classificationType = [
'idTipoprotocollo': 3 'idTipoprotocollo': 3
} }
]; ];
export const resendEmailLabelsByType = {
APPLICATION_AMENDMENT_REQUESTED: 'Invia email (nuovo soccorso)',
APPLICATION_AMENDMENT_REMINDER: 'Invia email (sollecito)'
}

View File

@@ -131,6 +131,7 @@ const DomandeTablePreInstructorAsync = ({ userId = null, statuses = [] }) => {
}; };
const actionsBodyTemplate = (rowData) => { const actionsBodyTemplate = (rowData) => {
console.log('status', rowData.status)
if (rowData.status === 'AWAITING') { if (rowData.status === 'AWAITING') {
return <Button return <Button
severity="info" severity="info"
@@ -143,9 +144,22 @@ const DomandeTablePreInstructorAsync = ({ userId = null, statuses = [] }) => {
const label = ['OPEN', 'SOCCORSO'].includes(rowData.status) && userData.id === rowData.userId const label = ['OPEN', 'SOCCORSO'].includes(rowData.status) && userData.id === rowData.userId
? __('Valuta', 'gepafin') ? __('Valuta', 'gepafin')
: __('Mostra', 'gepafin'); : __('Mostra', 'gepafin');
return <Link to={`/domande/${rowData.applicationId}`}> return (
<Button severity="info" label={label} icon="pi pi-eye" size="small" iconPos="right"/> <div className="appPageSection__tableActions lessGap">
</Link> <Link to={`/domande/${rowData.applicationId}`}>
<Button severity="info" label={label} icon="pi pi-eye" size="small" iconPos="right"/>
</Link>
{rowData.applicationStatus === 'REJECTED' && (
<Button
severity='success'
label={__('Riammetti', 'gepafin')}
icon='pi pi-arrow-circle-up'
size="small"
iconPos="right"
/>
)}
</div>
)
} }
} }

View File

@@ -0,0 +1,81 @@
import React, { useRef, useState } from 'react';
import { pathOr } from 'ramda';
// tools
import set404FromErrorResponse from '../../../../helpers/set404FromErrorResponse';
// api
import EmailService from '../../../../service/email-service';
import { Toast } from 'primereact/toast';
import { Button } from 'primereact/button';
import { resendEmailLabelsByType } from '../../../../configData';
const SoccorsoResendEmails = ({ emailsData = [], setDataEmailsSoccorso }) => {
const [isResendingRequest, setIsResendingRequest] = useState(false);
const toast = useRef(null);
const filtered = emailsData.filter(o => !o.isEmailSend);
const resendEmail = (data) => {
setIsResendingRequest(true);
EmailService.emailResend(
data.userActionId,
(resp) => resendingCallback(resp, data.userActionId),
errResendingCallback
);
}
const resendingCallback = (resp, id) => {
if (resp.status === 'SUCCESS') {
if (toast.current && resp.message) {
toast.current.show({
severity: 'success',
summary: '',
detail: resp.message
});
}
setTimeout(() => {
if (setDataEmailsSoccorso) {
const newEmailSendResponse = emailsData.map((o) => {
if (o.userActionId === id) {
o.isEmailSend = true;
}
return o;
});
setDataEmailsSoccorso(newEmailSendResponse);
}
setIsResendingRequest(false);
}, 1500);
}
}
const errResendingCallback = (resp) => {
if (toast.current && resp.message) {
toast.current.show({
severity: 'error',
summary: '',
detail: resp.message
});
}
set404FromErrorResponse(resp);
setIsResendingRequest(false);
}
return(
filtered.length > 0 ? <>
<Toast ref={toast}/>
{filtered
.map(o => <Button
severity="warning"
type="button"
disabled={isResendingRequest}
onClick={() => resendEmail(o)}
label={pathOr('Re-inivia email', ['emailScenario'], resendEmailLabelsByType)}
icon="pi pi-send" iconPos="right"/>)}
</> : null
)
}
export default SoccorsoResendEmails;

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useMemo } from 'react'; import React, { useState, useEffect, useRef, useMemo, useCallback } 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 { is, isEmpty } from 'ramda'; import { is, isEmpty, isNil, pathOr } from 'ramda';
import { wrap } from 'object-path-immutable'; import { wrap } from 'object-path-immutable';
import { klona } from 'klona'; import { klona } from 'klona';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -28,6 +28,7 @@ import FormField from '../../components/FormField';
import { Editor } from 'primereact/editor'; import { Editor } from 'primereact/editor';
import { InputNumber } from 'primereact/inputnumber'; import { InputNumber } from 'primereact/inputnumber';
import SoccorsoComunications from './components/SoccorsoComunications'; import SoccorsoComunications from './components/SoccorsoComunications';
import SoccorsoResendEmails from './components/SoccorsoResendEmails';
const SoccorsoEditPreInstructor = () => { const SoccorsoEditPreInstructor = () => {
@@ -43,6 +44,7 @@ const SoccorsoEditPreInstructor = () => {
const [internalNote, setInternalNote] = useState(''); const [internalNote, setInternalNote] = useState('');
const toast = useRef(null); const toast = useRef(null);
const [formInitialData, setFormInitialData] = useState({}); const [formInitialData, setFormInitialData] = useState({});
const emailSendResponse = pathOr([], ['emailSendResponse'], data);
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -346,6 +348,11 @@ const SoccorsoEditPreInstructor = () => {
setIsLoadingReminding(false); setIsLoadingReminding(false);
} }
const updateEmailSendResponses = useCallback((newEmailData) => {
const newData = wrap(data).set(['emailSendResponse'], newEmailData).value();
setData(newData);
}, [data]);
useEffect(() => { useEffect(() => {
if (formInitialData) { if (formInitialData) {
Object.keys(formInitialData).map(k => setValue(k, formInitialData[k])); Object.keys(formInitialData).map(k => setValue(k, formInitialData[k]));
@@ -506,7 +513,8 @@ const SoccorsoEditPreInstructor = () => {
<Button <Button
type="button" type="button"
onClick={sendReminder} onClick={sendReminder}
disabled={isLoadingReminding || ['CLOSE', 'EXPIRED'].includes(data.status)} disabled={isLoadingReminding || ['CLOSE', 'EXPIRED'].includes(data.status)
|| (!isNil(emailSendResponse) && !isEmpty(emailSendResponse))}
outlined outlined
label={__('Invia Sollecito', 'gepafin')} label={__('Invia Sollecito', 'gepafin')}
icon="pi pi-send" icon="pi pi-send"
@@ -532,6 +540,11 @@ const SoccorsoEditPreInstructor = () => {
label={__('Chiudi Soccorso Istruttorio', 'gepafin')} label={__('Chiudi Soccorso Istruttorio', 'gepafin')}
icon="pi pi-times" iconPos="right"/> icon="pi pi-times" iconPos="right"/>
</div> </div>
<div className="appPageSection__actions">
<SoccorsoResendEmails
emailsData={emailSendResponse}
setDataEmailsSoccorso={updateEmailSendResponses}/>
</div>
</div> </div>
</div> </div>

View File

@@ -22,6 +22,7 @@ import ProperBandoLabel from '../../../../components/ProperBandoLabel';
import { Dropdown } from 'primereact/dropdown'; import { Dropdown } from 'primereact/dropdown';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import { Calendar } from 'primereact/calendar'; import { Calendar } from 'primereact/calendar';
import SoccorsoResendEmails from '../../../SoccorsoEditPreInstructor/components/SoccorsoResendEmails';
const SoccorsiPreInstructorTableAsync = ({ userId = null }) => { const SoccorsiPreInstructorTableAsync = ({ userId = null }) => {
const [localAsyncRequest, setLocalAsyncRequest] = useState(false); const [localAsyncRequest, setLocalAsyncRequest] = useState(false);
@@ -94,10 +95,25 @@ const SoccorsiPreInstructorTableAsync = ({ userId = null }) => {
}); });
}; };
const updateRowData = useCallback((id, updateResponse) => {
const newItems = items.map((o) => {
if (o.id === id) {
o.emailSendResponse = updateResponse;
}
return o;
})
setItems(newItems);
}, [items]);
const actionsBodyTemplate = (rowData) => { const actionsBodyTemplate = (rowData) => {
return <Link to={`/domande/${rowData.applicationId}/soccorso/${rowData.id}`}> return <div className="appPageSection__tableActions lessGap">
<Button severity="info" label={__('Dettagli', 'gepafin')} size="small"/> <Link to={`/domande/${rowData.applicationId}/soccorso/${rowData.id}`}>
</Link> <Button severity="info" label={__('Dettagli', 'gepafin')} size="small"/>
</Link>
<SoccorsoResendEmails
emailsData={rowData.emailSendResponse}
setDataEmailsSoccorso={(updateResponse) => updateRowData(rowData.id, updateResponse)}/>
</div>
} }
const statusBodyTemplate = (rowData) => { const statusBodyTemplate = (rowData) => {

View File

@@ -0,0 +1,13 @@
import { NetworkService } from './network-service';
const API_BASE_URL = process.env.REACT_APP_API_EXECUTION_ADDRESS;
export default class EmailService {
static emailResend = (id, callback, errCallback) => {
NetworkService.post(`${API_BASE_URL}/email/resend`, {}, callback, errCallback, [
['userActionId', id]
]);
};
}