From 095ecf03e310a5c93670fdbcef23f020d2760ceb Mon Sep 17 00:00:00 2001 From: Vitalii Kiiko Date: Wed, 13 Nov 2024 13:52:48 +0100 Subject: [PATCH] Feature: preview application by ID by admin - added ApplicationPreview page for admin; - added a new table to admin dashboard with applications in draft; --- src/assets/scss/components/appForm.scss | 4 + src/assets/scss/components/appPage.scss | 2 +- src/assets/scss/components/layout.scss | 11 + src/assets/scss/components/misc.scss | 13 +- src/pages/BandoApplication/index.js | 4 +- src/pages/BandoApplicationPreview/index.js | 412 ++++++++++++++++++ .../DraftApplicationsTable/index.js | 199 +++++++++ src/pages/Dashboard/index.js | 22 +- .../MyLatestSubmissionsTable/index.js | 2 +- src/pages/DomandaEditPreInstructor/index.js | 16 +- .../components/AllDomandeTable/index.js | 10 +- src/pages/Domande/index.js | 9 + src/routes.js | 3 +- 13 files changed, 679 insertions(+), 28 deletions(-) create mode 100644 src/pages/BandoApplicationPreview/index.js create mode 100644 src/pages/Dashboard/components/DraftApplicationsTable/index.js diff --git a/src/assets/scss/components/appForm.scss b/src/assets/scss/components/appForm.scss index bb2c482..c114ba9 100644 --- a/src/assets/scss/components/appForm.scss +++ b/src/assets/scss/components/appForm.scss @@ -30,6 +30,10 @@ line-height: normal; margin-left: 10px; } + + &.p-error { + color: var(--message-error-color) + } } small { diff --git a/src/assets/scss/components/appPage.scss b/src/assets/scss/components/appPage.scss index fab7e1e..c280975 100644 --- a/src/assets/scss/components/appPage.scss +++ b/src/assets/scss/components/appPage.scss @@ -312,7 +312,7 @@ #f8d282 20px ); - .p-button { + .p-button-outlined { background: white; } } diff --git a/src/assets/scss/components/layout.scss b/src/assets/scss/components/layout.scss index d4e3572..7c6eb60 100644 --- a/src/assets/scss/components/layout.scss +++ b/src/assets/scss/components/layout.scss @@ -134,6 +134,17 @@ img { } } +.blockingOverlay { + position: absolute; + z-index: 999; + inset: 0; + background-color: rgba(255,255,255,0.3) +} + +button[disabled] { + filter: grayscale(1); +} + @media (max-width: 800px) { .inner { flex-direction: column; diff --git a/src/assets/scss/components/misc.scss b/src/assets/scss/components/misc.scss index 8d5a701..c276636 100644 --- a/src/assets/scss/components/misc.scss +++ b/src/assets/scss/components/misc.scss @@ -106,19 +106,18 @@ padding: 1rem 1.5rem; } -.blockingOverlay { - position: absolute; - z-index: 999; - inset: 0; - background-color: rgba(255,255,255,0.3) -} - .p-accordion-header-text, .p-accordion-content { p { margin: 0; } } +.p-disabled, .p-disabled * { + cursor: not-allowed; + pointer-events: auto; + user-select: none; +} + .p-inputgroup.flex-1 { align-items: center; } diff --git a/src/pages/BandoApplication/index.js b/src/pages/BandoApplication/index.js index cbc7d22..4767ebb 100644 --- a/src/pages/BandoApplication/index.js +++ b/src/pages/BandoApplication/index.js @@ -606,8 +606,8 @@ const BandoApplication = () => { //console.log('validations', validations, o.name) return ['paragraph'].includes(o.name) && text - ?
-
+ ?
+
{renderHtmlContent(text.value)}
diff --git a/src/pages/BandoApplicationPreview/index.js b/src/pages/BandoApplicationPreview/index.js new file mode 100644 index 0000000..74b79d4 --- /dev/null +++ b/src/pages/BandoApplicationPreview/index.js @@ -0,0 +1,412 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import { useParams } from 'react-router-dom'; +import { head, isEmpty, pathOr } from 'ramda'; +import { useForm } from 'react-hook-form'; +import 'quill/dist/quill.core.css'; + +// store +import { storeSet, useStore } from '../../store'; + +// api +import ApplicationService from '../../service/application-service'; + +// tools +import { + isPIVA, + isCodiceFiscale, + isCAP, + isIBAN, + isEmail, + isEmailPEC, + isUrl, + isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables +} from '../../helpers/validators'; +import renderHtmlContent from '../../helpers/renderHtmlContent'; +import set404FromErrorResponse from '../../helpers/set404FromErrorResponse'; + +// components +import { Skeleton } from 'primereact/skeleton'; +import { Button } from 'primereact/button'; +import FormField from '../../components/FormField'; +import { Toast } from 'primereact/toast'; +import { Messages } from 'primereact/messages'; +import ApplicationSteps from '../BandoApplication/ApplicationSteps'; +import BlockingOverlay from '../../components/BlockingOverlay'; + +const BandoApplicationPreview = () => { + const { id } = useParams(); + const [formData, setFormData] = useState([]); + const [formInitialData, setFormInitialData] = useState(null); + const [bandoTitle, setBandoTitle] = useState(''); + const [bandoId, setBandoId] = useState(0); + const [formId, setFormId] = useState(''); + const [totalSteps, setTotalSteps] = useState(0); + const [applicationStatus, setApplicationStatus] = useState(''); + const [activeStep, setActiveStep] = useState(1); + const isAsyncRequest = useStore().main.isAsyncRequest(); + const toast = useRef(null); + const formMsgs = useRef(null); + const { + control, + handleSubmit, + formState: { errors }, + setValue, + trigger, + register, + getValues, + reset + } = useForm({ + defaultValues: useMemo(() => { + return formInitialData ? formInitialData : {} + }, [formInitialData]), + mode: 'onChange' + }); + const validationFns = { + isPIVA, + isCodiceFiscale, + isCAP, + isIBAN, + isEmail, + isEmailPEC, + isUrl, + isMarcaDaBollo, + minChecks, + maxChecks, + nonEmptyTables + } + const activeStepIndex = activeStep - 1; + const values = getValues(); + + const onValidate = () => { + const applId = getApplicationId(); + storeSet.main.setAsyncRequest(); + formMsgs.current.clear(); + + ApplicationService.validateApplication(applId, {}, validateApplicationCallback, errValidateApplicationCallback); + }; + + const onSubmit = () => { + }; + + const validateApplicationCallback = (data) => { + if (data.status === 'SUCCESS') { + toast.current.show({ + severity: 'success', + summary: '', + detail: data.message + }); + } + storeSet.main.unsetAsyncRequest(); + } + + const errValidateApplicationCallback = (data) => { + if (toast.current) { + toast.current.show({ + severity: 'error', + summary: '', + detail: data.message + }); + } + storeSet.main.unsetAsyncRequest(); + } + + const saveDraft = (saveAndMove = '') => { + trigger(); + } + + const getApplicationId = () => { + const parsed = parseInt(id) + return !isNaN(parsed) ? parsed : 0; + } + + const goBackward = () => { + storeSet.main.setAsyncRequest(); + ApplicationService.getApplicationForm(id, getApplFormCallback, errGetApplFormCallbacks, [ + ['formId', formId], + ['action', 'PREVIOUS'] + ]); + } + + const goForward = () => { + storeSet.main.setAsyncRequest(); + ApplicationService.getApplicationForm(id, getApplFormCallback, errGetApplFormCallbacks, [ + ['formId', formId], + ['action', 'NEXT'] + ]); + } + + const getApplFormCallback = (data) => { + if (data.status === 'SUCCESS') { + setBandoTitle(data.data.callTitle); + setBandoId(data.data.callId); + setFormData(data.data.applicationFormResponse.content); + setFormId(data.data.formId); + setTotalSteps(data.data.totalFormSteps); + setApplicationStatus(data.data.applicationStatus) + setActiveStep(data.data.currentStep); + + /*const chosenCompanyId = storeGet.main.chosenCompanyId(); + const companies = storeGet.main.companies(); + const company = head(companies.filter(o => o.id === chosenCompanyId));*/ + let formDataInitial = {}; + let dynamicData = { + company: {}, + user: {} + }; + + /*if (company) { + dynamicData = Object.keys(company).reduce((acc, cur) => { + if ([ + 'companyName', 'vatNumber', 'codiceFiscale', 'address', 'phoneNumber', + 'city', 'province', 'cap', 'country', 'pec', 'email', 'contactName', 'contactEmail' + ].includes(cur)) { + acc.company[cur] = company[cur]; + } + return acc; + }, dynamicData); + } + + const userData = storeGet.main.userData(); + Object.keys(userData).reduce((acc, cur) => { + if ([ + 'email', 'firstName', 'lastName', 'phoneNumber', 'codiceFiscale' + ].includes(cur)) { + acc.user[cur] = userData[cur]; + } + if (['dateOfBirth'].includes(cur)) { + acc.user[cur] = new Date(userData[cur]); + } + return acc; + }, dynamicData);*/ + + if (data.data.applicationFormResponse.content) { + // eslint-disable-next-line array-callback-return + data.data.applicationFormResponse.content.map((o) => { + if (o.dynamicData && !isEmpty(o.dynamicData)) { + formDataInitial[o.id] = pathOr('', o.dynamicData.split('.'), dynamicData); + } + }) + } + + if (data.data.applicationFormResponse.formFields) { + const submitData = data.data.applicationFormResponse.formFields.map((o) => ({ + fieldId: o.fieldId, + fieldValue: o.fieldValue + })); + formDataInitial = submitData.reduce((acc, cur) => { + if (cur.fieldValue) { + acc[cur.fieldId] = cur.fieldValue; + } + return acc; + }, formDataInitial); + } + + reset(); + setFormInitialData(formDataInitial); + } + storeSet.main.unsetAsyncRequest(); + } + + const errGetApplFormCallbacks = (data) => { + storeSet.main.unsetAsyncRequest(); + if (data.status === 'VALIDATION_ERROR') { + if (toast.current) { + toast.current.show({ + severity: 'error', + summary: '', + detail: data.message + }); + } + } else { + set404FromErrorResponse(data); + } + } + + const onDownloadApplicationPdf = () => { + const applId = getApplicationId(); + storeSet.main.setAsyncRequest(); + + ApplicationService.downloadApplicationPdf(applId, {}, getPdfCallback, errPdfCallback); + } + + const getPdfCallback = (data) => { + const applId = getApplicationId(); + const pdfFile = new Blob([data], { type: 'application/octet-stream' }) + const url = window.URL.createObjectURL(pdfFile); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `application-${applId}.pdf`); + document.body.appendChild(link); + link.click(); + link.remove(); + storeSet.main.unsetAsyncRequest(); + } + + const errPdfCallback = (data) => { + set404FromErrorResponse(data); + storeSet.main.unsetAsyncRequest(); + } + + const actionBtns =
+ {activeStep > 1 && activeStep <= totalSteps + ?
+ + useEffect(() => { + if (formInitialData) { + //reset(); + Object.keys(formInitialData).map(k => setValue(k, formInitialData[k])); + trigger(); + } + }, [formInitialData]); + + useEffect(() => { + const applId = getApplicationId(); + + if (applId) { + storeSet.main.setAsyncRequest(); + ApplicationService.getApplicationForm(applId, getApplFormCallback, errGetApplFormCallbacks); + } + }, [id]); + + return ( +
+ {!isAsyncRequest + ?
+

{sprintf(__('Domanda per il Bando: %s', 'gepafin'), bandoTitle)}

+
+ : <> + + + } + +
+ + + +
+ + + + +
+ +
+
+ {actionBtns} +
+ + {formData.map(o => { + const label = head(o.settings.filter(o => o.name === 'label')); + const text = head(o.settings.filter(o => o.name === 'text')); + const placeholder = head(o.settings.filter(o => o.name === 'placeholder')); + const options = head(o.settings.filter(o => o.name === 'options')); + const tableColumns = head(o.settings.filter(o => o.name === 'table_columns')); + const step = head(o.settings.filter(o => o.name === 'step')); + const mime = head(o.settings.filter(o => o.name === 'mime')); + let mimeValue = ''; + + if (mime) { + mimeValue = mime.value.map(o => o.code ? o.code : o.ext); + } + + const validations = Object.keys(o.validators).reduce((acc, cur) => { + if (o.validators[cur]) { + if (['min', 'max', 'minLength', 'maxLength', 'maxSize'].includes(cur)) { + acc[cur] = parseInt(o.validators[cur]); + } else if ('pattern' === cur) { + acc[cur] = new RegExp(o.validators[cur]); + } else if ('isRequired' === cur) { + //acc[cur] = o.validators[cur]; + acc['required'] = true; + } else if ('custom' === cur && validationFns[o.validators[cur]]) { + if (!acc.validate) { + acc.validate = {}; + } + acc.validate[o.validators[cur]] = validationFns[o.validators[cur]]; + } + } + + return acc; + }, {}); + //console.log('validations', validations, o.name) + + return ['paragraph'].includes(o.name) && text + ?
+
+ {renderHtmlContent(text.value)} +
+
+ : + })} + +
+ +
+ {__('Azioni rapide', 'gepafin')} +
+ +
+ {actionBtns} +
+ +
+
+ ) + +} + +export default BandoApplicationPreview; \ No newline at end of file diff --git a/src/pages/Dashboard/components/DraftApplicationsTable/index.js b/src/pages/Dashboard/components/DraftApplicationsTable/index.js new file mode 100644 index 0000000..63518ff --- /dev/null +++ b/src/pages/Dashboard/components/DraftApplicationsTable/index.js @@ -0,0 +1,199 @@ +import React, { useState, useEffect } from 'react'; +import { __ } from '@wordpress/i18n'; +import { uniq, is } from 'ramda'; + +// tools +import getBandoLabel from '../../../../helpers/getBandoLabel'; +import getBandoSeverity from '../../../../helpers/getBandoSeverity'; + +// store +import { useStore } from '../../../../store'; + +// api +import ApplicationService from '../../../../service/application-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 { ProgressBar } from 'primereact/progressbar'; +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 DraftApplicationsTable = () => { + const chosenCompanyId = useStore().main.chosenCompanyId(); + const [localAsyncRequest, setLocalAsyncRequest] = useState(false); + const [items, setItems] = useState(null); + const [filters, setFilters] = useState(null); + const [globalFilterValue, setGlobalFilterValue] = useState(''); + const [statuses, setStatuses] = useState([]); + + useEffect(() => { + setLocalAsyncRequest(true); + ApplicationService.getApplications(getApplCallback, errGetApplCallback, [ + ['statuses', ['DRAFT', 'AWAITING', 'READY']] + ]) + }, [chosenCompanyId]); + + const getApplCallback = (data) => { + if (data.status === 'SUCCESS') { + if (is(Array, data.data)) { + setItems(getFormattedBandiData(data.data)); + setStatuses(uniq(items.map(o => o.status))) + initFilters(); + } + } + setLocalAsyncRequest(false); + } + + const errGetApplCallback = (data) => { + setLocalAsyncRequest(false); + } + + const getFormattedBandiData = (data) => { + return [...(data || [])].map((d) => { + d.callEndDate = new Date(d.callEndDate); + d.modifiedDate = new Date(d.modifiedDate); + + 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 }, + callTitle: { + operator: FilterOperator.AND, + constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] + }, + modifiedDate: { + operator: FilterOperator.AND, + constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] + }, + callEndDate: { + operator: FilterOperator.AND, + constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] + }, + status: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] }, + }); + setGlobalFilterValue(''); + }; + + const renderHeader = () => { + return ( +
+
+ ); + }; + + /*const dateModifyBodyTemplate = (rowData) => { + return formatDate(rowData.modifiedDate); + }; + + const dateEndBodyTemplate = (rowData) => { + return formatDate(rowData.callEndDate); + }; + + const dateFilterTemplate = (options) => { + return options.filterCallback(e.value, options.index)} + dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999"/>; + };*/ + + const statusBodyTemplate = (rowData) => { + return ; + }; + + const statusFilterTemplate = (options) => { + return options.filterCallback(e.value, options.index)} + itemTemplate={statusItemTemplate} placeholder="Select One" className="p-column-filter" + showClear/>; + }; + + const progressBodyTemplate = (options) => { + return ; + }; + + const statusItemTemplate = (option) => { + return ; + }; + + const actionsBodyTemplate = (rowData) => { + return +
{__('Utenti registrati', 'gepafin')} + locales="it-IT"/>
{__('Domande in pre-istruttoria', 'gepafin')} + locales="it-IT"/>
{__('Domande in bozza', 'gepafin')} + locales="it-IT"/>
{__('Aziende', 'gepafin')} + locales="it-IT"/>
{__('Totale finanziamenti attivi', 'gepafin')} @@ -110,8 +111,8 @@ const Dashboard = () => { style: 'currency', currency: 'EUR', currencyDisplay: 'symbol' - }} - locales="en-US" /> + }} + locales="en-US"/>
@@ -130,6 +131,13 @@ const Dashboard = () => { +
+ +
+

{__('Domande in bozza', 'gepafin')}

+ +
+ {/*
diff --git a/src/pages/DashboardBeneficiario/components/MyLatestSubmissionsTable/index.js b/src/pages/DashboardBeneficiario/components/MyLatestSubmissionsTable/index.js index ad627e5..608b2ba 100644 --- a/src/pages/DashboardBeneficiario/components/MyLatestSubmissionsTable/index.js +++ b/src/pages/DashboardBeneficiario/components/MyLatestSubmissionsTable/index.js @@ -39,7 +39,7 @@ const MyLatestSubmissionsTable = () => { setLocalAsyncRequest(true); ApplicationService.getApplications(getApplCallback, errGetApplCallback, [ ['companyId', chosenCompanyId], - ['statuses', ['DRAFT', 'SUBMIT', 'AWAITING', 'READY', 'DISCARD']] + ['statuses', ['DRAFT', 'AWAITING', 'READY']] ]) }, [chosenCompanyId]); diff --git a/src/pages/DomandaEditPreInstructor/index.js b/src/pages/DomandaEditPreInstructor/index.js index 65de77d..e3871e3 100644 --- a/src/pages/DomandaEditPreInstructor/index.js +++ b/src/pages/DomandaEditPreInstructor/index.js @@ -148,14 +148,22 @@ const DomandaEditPreInstructor = () => { const doApprove = () => { const formData = { - status: 'APPROVED' + applicationStatus: 'APPROVED', + criteria: klona(data.criteria), + checklist: klona(data.checklist), + files: klona(data.files), + note: data.note } ApplicationEvaluationService.updateEvaluation(data.assignedApplicationId, formData, updateStatusCallback, errUpdateStatusCallback); } const doReject = () => { const formData = { - status: 'REJECTED' + applicationStatus: 'REJECTED', + criteria: klona(data.criteria), + checklist: klona(data.checklist), + files: klona(data.files), + note: data.note } ApplicationEvaluationService.updateEvaluation(data.assignedApplicationId, formData, updateStatusCallback, errUpdateStatusCallback); } @@ -537,14 +545,14 @@ const DomandaEditPreInstructor = () => { {data.id ?