diff --git a/package.json b/package.json index a463d6b..e9c3a7e 100644 --- a/package.json +++ b/package.json @@ -1,90 +1,91 @@ { - "name": "bflows-gepafin", - "version": "1.0.0", - "private": true, - "dependencies": { - "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/preset-react": "7.26.3", - "@date-fns/tz": "1.2.0", - "@emailjs/browser": "4.4.1", - "@number-flow/react": "0.5.9", - "@sentry/browser": "9.11.0", - "@stomp/stompjs": "7.1.1", - "@tanstack/react-table": "8.21.2", - "@wordpress/i18n": "5.21.0", - "@wordpress/react-i18n": "4.21.0", - "codice-fiscale-js": "2.3.22", - "copy-to-clipboard": "3.3.3", - "deep-object-diff": "1.1.9", - "dompurify": "3.2.5", - "expression-language": "1.2.0", - "fast-deep-equal": "3.1.3", - "hotkeys-js": "3.13.9", - "html-react-parser": "5.2.3", - "jwt-decode": "4.0.0", - "klona": "2.0.6", - "leader-line-new": "1.1.9", - "luxon": "3.6.1", - "mathjs": "14.4.0", - "mustache": "4.2.0", - "object-path-immutable": "4.1.2", - "primeicons": "7.0.0", - "primereact": "10.9.4", - "quill": "2.0.3", - "ramda": "0.30.1", - "react": "19.1.0", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", - "react-dom": "19.1.0", - "react-hook-form": "7.55.0", - "react-router-dom": "7.5.0", - "react-scripts": "5.0.1", - "recharts": "2.15.2", - "sockjs-client": "1.6.1", - "validate.js": "0.13.1", - "zustand": "5.0.3", - "zustand-x": "6.1.0" - }, - "devDependencies": { - "@babel/cli": "7.27.0", - "@babel/core": "7.26.10", - "@babel/plugin-syntax-jsx": "7.25.9", - "@wordpress/babel-plugin-makepot": "6.21.0", - "babel-plugin-macros": "3.1.0", - "node-wp-i18n": "1.2.7", - "sass": "1.86.3", - "sass-loader": "16.0.5" - }, - "scripts": { - "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", - "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": "react-scripts build", - "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:prod": "cp environments/prod/* public/loaded-files && rm public/loaded-files/prod.env && cp environments/prod/prod.env .env && react-scripts build --mode production", - "test": "react-scripts test", - "eject": "react-scripts eject", - "make-pot": "wpi18n makepot --domain-path=languages --domain=gepafin" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ], - "rules": { - "react-hooks/exhaustive-deps": "off" + "name": "bflows-gepafin", + "version": "1.0.0", + "private": true, + "dependencies": { + "@babel/plugin-proposal-private-property-in-object": "7.21.11", + "@babel/preset-react": "7.26.3", + "@date-fns/tz": "1.2.0", + "@emailjs/browser": "4.4.1", + "@number-flow/react": "0.5.9", + "@sentry/browser": "9.11.0", + "@stomp/stompjs": "7.1.1", + "@tanstack/react-table": "8.21.2", + "@wordpress/i18n": "5.21.0", + "@wordpress/react-i18n": "4.21.0", + "codice-fiscale-js": "2.3.22", + "copy-to-clipboard": "3.3.3", + "deep-object-diff": "1.1.9", + "dompurify": "3.2.5", + "expression-language": "1.2.0", + "fast-deep-equal": "3.1.3", + "hotkeys-js": "3.13.9", + "html-react-parser": "5.2.3", + "jwt-decode": "4.0.0", + "klona": "2.0.6", + "leader-line-new": "1.1.9", + "luxon": "3.6.1", + "mathjs": "14.4.0", + "mustache": "4.2.0", + "object-path-immutable": "4.1.2", + "primeicons": "7.0.0", + "primereact": "10.9.4", + "quill": "2.0.3", + "ramda": "0.30.1", + "react": "19.1.0", + "react-dnd": "16.0.1", + "react-dnd-html5-backend": "16.0.1", + "react-dom": "19.1.0", + "react-hook-form": "7.55.0", + "react-router-dom": "7.5.0", + "react-scripts": "5.0.1", + "recharts": "2.15.2", + "sockjs-client": "1.6.1", + "validate.js": "0.13.1", + "zustand": "5.0.3", + "zustand-x": "6.1.0" + }, + "devDependencies": { + "@babel/cli": "7.27.0", + "@babel/core": "7.26.10", + "@babel/plugin-syntax-jsx": "7.25.9", + "@wordpress/babel-plugin-makepot": "6.21.0", + "babel-plugin-macros": "3.1.0", + "node-wp-i18n": "1.2.7", + "sass": "1.86.3", + "sass-loader": "16.0.5" + }, + "scripts": { + "start": "GENERATE_SOURCEMAP=false react-scripts start", + "start2": "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", + "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": "react-scripts build", + "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:prod": "cp environments/prod/* public/loaded-files && rm public/loaded-files/prod.env && cp environments/prod/prod.env .env && react-scripts build --mode production", + "test": "react-scripts test", + "eject": "react-scripts eject", + "make-pot": "wpi18n makepot --domain-path=languages --domain=gepafin" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ], + "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" - ] - } -} +} \ No newline at end of file diff --git a/src/assets/scss/components/appPage.scss b/src/assets/scss/components/appPage.scss index a608ea4..59ad1cc 100644 --- a/src/assets/scss/components/appPage.scss +++ b/src/assets/scss/components/appPage.scss @@ -413,6 +413,10 @@ flex-wrap: wrap; } +.appPageSection__actions:empty { + display: none; +} + .appPageSection__tableActions { display: flex; gap: 10px; diff --git a/src/components/FormField/components/Fileupload/index.js b/src/components/FormField/components/Fileupload/index.js index a8218a7..b812910 100644 --- a/src/components/FormField/components/Fileupload/index.js +++ b/src/components/FormField/components/Fileupload/index.js @@ -13,6 +13,7 @@ import getPropeMimeLabels from '../../../../helpers/getPropeMimeLabels'; import { FileUpload } from 'primereact/fileupload'; import { Tag } from 'primereact/tag'; import { Button } from 'primereact/button'; +import { Messages } from 'primereact/messages'; import { defaultMaxFileSize, mimeTypes } from '../../../../configData'; import getFormatedFileSizeText from '../../../../helpers/getFormatedFileSizeText'; @@ -47,24 +48,48 @@ const Fileupload = ({ const [acceptFormats, setAcceptFormats] = useState(''); const [formatsForInput, setFormatsForInput] = useState(''); const inputRef = useRef(); + const messagesRef = useRef(null); const customBase64Uploader = (event) => { const formData = new FormData() + const filesToUpload = []; + const uploadedFiles = inputRef.current ? inputRef.current.getUploadedFiles() : []; + const currentFiles = stateFieldData; + 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') { - setStateFieldData(data.data); - const uploadedFiles = inputRef.current.getUploadedFiles(); - setDataFn(fieldName, [...uploadedFiles, ...data.data], { shouldValidate: true }); + setStateFieldData(prevState => [...prevState, ...data.data]); + const currentUploadedFiles = inputRef.current.getUploadedFiles() || []; inputRef.current.setFiles([]); + + setDataFn(fieldName, [...currentUploadedFiles, ...data.data], { shouldValidate: true }); + saveFormCallback(); } } @@ -204,6 +229,7 @@ const Fileupload = ({ return ( sourceId || sourceId === 0 ? <> + {label}{config.required || config.isRequired ? * : null} diff --git a/src/components/FormField/components/FileuploadAsync/index.js b/src/components/FormField/components/FileuploadAsync/index.js index b422c78..a1404e4 100644 --- a/src/components/FormField/components/FileuploadAsync/index.js +++ b/src/components/FormField/components/FileuploadAsync/index.js @@ -13,6 +13,8 @@ import { FileUpload } from 'primereact/fileupload'; import { Tag } from 'primereact/tag'; import { Button } from 'primereact/button'; import { head, isEmpty } from 'ramda'; +import { Messages } from 'primereact/messages'; + import { defaultMaxFileSize, mimeTypes } from '../../../../configData'; import getFormatedFileSizeText from '../../../../helpers/getFormatedFileSizeText'; @@ -42,24 +44,50 @@ const FileuploadAsync = ({ const [acceptFormats, setAcceptFormats] = useState(''); const [formatsForInput, setFormatsForInput] = useState(''); const inputRef = useRef(); + const messagesRef = useRef(null); const customBase64Uploader = (event) => { const formData = new FormData() + const filesToUpload = []; + const uploadedFiles = inputRef.current ? inputRef.current.getUploadedFiles() : []; + const currentFiles = stateFieldData; + 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); + } + } - FileUploadService.uploadFile(sourceId, formData, callback, errorCallback, [ - ['documentType', doctype.toUpperCase()], - ['sourceType', source.toUpperCase()] - ]); + if(filesToUpload.length > 0){ + FileUploadService.uploadFile(sourceId, formData, callback, errorCallback, [ + ['documentType', doctype.toUpperCase()], + ['sourceType', source.toUpperCase()] + ]); + } }; - const callback = (data) => { + const callback = (data, uploadedFiles) => { if (data.status === 'SUCCESS') { - setStateFieldData(data.data); - const uploadedFiles = inputRef.current.getUploadedFiles(); - setDataFn(fieldName, [...uploadedFiles, ...data.data], { shouldValidate: true }); + setStateFieldData(prevState => [...prevState, ...data.data]); + const currentUploadedFiles = inputRef.current.getUploadedFiles() || []; + + inputRef.current.setUploadedFiles([...currentUploadedFiles, ...data.data]); inputRef.current.setFiles([]); + + setDataFn(fieldName, [...currentUploadedFiles, ...data.data], { shouldValidate: true }); } } @@ -160,7 +188,7 @@ const FileuploadAsync = ({ useEffect(() => { if (inputRef.current) { - inputRef.current.setUploadedFiles(defaultValue); + inputRef.current.setUploadedFiles(defaultValue); } setStateFieldData(defaultValue); }, [defaultValue]); @@ -186,6 +214,7 @@ const FileuploadAsync = ({ return ( sourceId && sourceId !== 0 ? <> + {label}{config.required ? '*' : null} {acceptFormats ? ' (' + getPropeMimeLabels(accept) + ')' : null} diff --git a/src/configData.js b/src/configData.js index 57c712a..1ad8478 100644 --- a/src/configData.js +++ b/src/configData.js @@ -262,4 +262,9 @@ export const classificationType = [ 'name': 'GENERICO', 'idTipoprotocollo': 3 } -]; \ No newline at end of file +]; + +export const resendEmailLabelsByType = { + APPLICATION_AMENDMENT_REQUESTED: 'Invia email (nuovo soccorso)', + APPLICATION_AMENDMENT_REMINDER: 'Invia email (sollecito)' +} \ No newline at end of file diff --git a/src/pages/DashboardPreInstructor/components/DomandeTablePreInstructorAsync/index.js b/src/pages/DashboardPreInstructor/components/DomandeTablePreInstructorAsync/index.js index d451b1b..0e7550b 100644 --- a/src/pages/DashboardPreInstructor/components/DomandeTablePreInstructorAsync/index.js +++ b/src/pages/DashboardPreInstructor/components/DomandeTablePreInstructorAsync/index.js @@ -131,6 +131,7 @@ const DomandeTablePreInstructorAsync = ({ userId = null, statuses = [] }) => { }; const actionsBodyTemplate = (rowData) => { + console.log('status', rowData.status) if (rowData.status === 'AWAITING') { return { const label = ['OPEN', 'SOCCORSO'].includes(rowData.status) && userData.id === rowData.userId ? __('Valuta', 'gepafin') : __('Mostra', 'gepafin'); - return - - + return ( + + + + + {rowData.applicationStatus === 'REJECTED' && ( + + )} + + ) } } diff --git a/src/pages/SoccorsoEditPreInstructor/components/SoccorsoResendEmails/index.js b/src/pages/SoccorsoEditPreInstructor/components/SoccorsoResendEmails/index.js new file mode 100644 index 0000000..88a081a --- /dev/null +++ b/src/pages/SoccorsoEditPreInstructor/components/SoccorsoResendEmails/index.js @@ -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 ? <> + + {filtered + .map(o => resendEmail(o)} + label={pathOr('Re-inivia email', ['emailScenario'], resendEmailLabelsByType)} + icon="pi pi-send" iconPos="right"/>)} + > : null + ) +} + +export default SoccorsoResendEmails; \ No newline at end of file diff --git a/src/pages/SoccorsoEditPreInstructor/index.js b/src/pages/SoccorsoEditPreInstructor/index.js index 6a331b7..303b874 100644 --- a/src/pages/SoccorsoEditPreInstructor/index.js +++ b/src/pages/SoccorsoEditPreInstructor/index.js @@ -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 { 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 { klona } from 'klona'; import { useForm } from 'react-hook-form'; @@ -28,6 +28,7 @@ import FormField from '../../components/FormField'; import { Editor } from 'primereact/editor'; import { InputNumber } from 'primereact/inputnumber'; import SoccorsoComunications from './components/SoccorsoComunications'; +import SoccorsoResendEmails from './components/SoccorsoResendEmails'; const SoccorsoEditPreInstructor = () => { @@ -43,6 +44,7 @@ const SoccorsoEditPreInstructor = () => { const [internalNote, setInternalNote] = useState(''); const toast = useRef(null); const [formInitialData, setFormInitialData] = useState({}); + const emailSendResponse = pathOr([], ['emailSendResponse'], data); const { control, handleSubmit, @@ -346,6 +348,11 @@ const SoccorsoEditPreInstructor = () => { setIsLoadingReminding(false); } + const updateEmailSendResponses = useCallback((newEmailData) => { + const newData = wrap(data).set(['emailSendResponse'], newEmailData).value(); + setData(newData); + }, [data]); + useEffect(() => { if (formInitialData) { Object.keys(formInitialData).map(k => setValue(k, formInitialData[k])); @@ -506,7 +513,8 @@ const SoccorsoEditPreInstructor = () => { { label={__('Chiudi Soccorso Istruttorio', 'gepafin')} icon="pi pi-times" iconPos="right"/> + + + diff --git a/src/pages/SoccorsoIstruttorioPreInstructor/components/SoccorsiPreInstructorTableAsync/index.js b/src/pages/SoccorsoIstruttorioPreInstructor/components/SoccorsiPreInstructorTableAsync/index.js index a1c5a85..58b61c4 100644 --- a/src/pages/SoccorsoIstruttorioPreInstructor/components/SoccorsiPreInstructorTableAsync/index.js +++ b/src/pages/SoccorsoIstruttorioPreInstructor/components/SoccorsiPreInstructorTableAsync/index.js @@ -22,6 +22,7 @@ import ProperBandoLabel from '../../../../components/ProperBandoLabel'; import { Dropdown } from 'primereact/dropdown'; import { Tag } from 'primereact/tag'; import { Calendar } from 'primereact/calendar'; +import SoccorsoResendEmails from '../../../SoccorsoEditPreInstructor/components/SoccorsoResendEmails'; const SoccorsiPreInstructorTableAsync = ({ userId = null }) => { 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) => { - return - - + return + + + + updateRowData(rowData.id, updateResponse)}/> + } const statusBodyTemplate = (rowData) => { diff --git a/src/service/email-service.js b/src/service/email-service.js new file mode 100644 index 0000000..eda31e4 --- /dev/null +++ b/src/service/email-service.js @@ -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] + ]); + }; + +}