diff --git a/.github/workflows/deploy_on_cdn.yml b/.github/workflows/deploy_on_cdn.yml index 92241390b..3c11541f4 100644 --- a/.github/workflows/deploy_on_cdn.yml +++ b/.github/workflows/deploy_on_cdn.yml @@ -78,11 +78,12 @@ jobs: export REACT_APP_ANALYTICS_MOCKED='false' yarn install --ignore-scripts --frozen-lockfile - if [ "${{ inputs.environment }}" = "dev" ]; then - yarn run generate:api-portal-next - else - yarn run generate:api-portal - fi + # if [ "${{ inputs.environment }}" = "dev" ]; then + # yarn run generate:api-portal-next + # else + # yarn run generate:api-portal + # fi + yarn run generate:api-portal yarn build - name: Login diff --git a/package.json b/package.json index ff0654d06..54b6d186b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pagopa-selfcare-backoffice-frontend", - "version": "1.45.0", + "version": "1.45.0-7-CHK-4675-multi-iban-handler", "homepage": "ui", "private": true, "scripts": { @@ -26,9 +26,8 @@ "prettify": "prettier --write \"./**/*.{ts,tsx}\"", "generate": "npm run generate:api-portal", "clean:api-portal": "rimraf src/api/generated/portal && rimraf openApi/generated", - "generate:api-portal": "wget https://raw.githubusercontent.com/pagopa/pagopa-selfcare-ms-backoffice-backend/main/openapi/openapi.json -O ./openApi/portal-api-docs.json && npm run generate:client", - "generate:api-portal-next": "wget https://raw.githubusercontent.com/pagopa/pagopa-selfcare-ms-backoffice-backend/next/openapi/openapi.json -O ./openApi/portal-api-docs.json && npm run generate:client", - "generate:api-portal-pr": "wget https://raw.githubusercontent.com/pagopa/pagopa-selfcare-ms-backoffice-backend/main/openapi/openapi.json -O ./openApi/portal-api-docs.json && npm run generate:client", + "generate:api-portal": "wget https://raw.githubusercontent.com/pagopa/pagopa-selfcare-ms-backoffice-backend/CHK-4674-massive-iban/openapi/openapi.json -O ./openApi/portal-api-docs.json && npm run generate:client", + "generate:api-portal-pr": "wget https://raw.githubusercontent.com/pagopa/pagopa-selfcare-ms-backoffice-backend/CHK-4674-massive-iban/openapi/openapi.json -O ./openApi/portal-api-docs.json && npm run generate:client", "generate:api-portal-local": "npm run generate:client", "generate:client": "jq 'walk(if type == \"object\" and has(\"parameters\") then .parameters |= map(select(.name != \"X-Request-Id\")) else . end)' ./openApi/portal-api-docs.json > ./openApi/portal-api-docs.json.temp && mv ./openApi/portal-api-docs.json.temp ./openApi/portal-api-docs.json && yarn run clean:api-portal && mkdirp openApi/generated && gen-api-models --api-spec openApi/portal-api-docs.json --out-dir src/api/generated/portal --no-strict --request-types --response-decoders --client && node openApi/scripts/api-portal_fixPostGen.js" }, diff --git a/public/file/multipleIbanExample.csv b/public/file/multipleIbanExample.csv new file mode 100644 index 000000000..a58c9a859 --- /dev/null +++ b/public/file/multipleIbanExample.csv @@ -0,0 +1,4 @@ +"descrizione","iban","dataattivazioneiban","operazione" +"Conto Principale","IT60X0542811101000000123456","2030-10-01","CREATE" +"Conto Secondario","IT49S0300203280447684177591","2030-05-15","UPDATE" +"Vecchio Conto","IT60X0542811101000000123456","2030-01-20","DELETE" \ No newline at end of file diff --git a/src/api/BackofficeClient.ts b/src/api/BackofficeClient.ts index 269b9cea5..6239c96a1 100644 --- a/src/api/BackofficeClient.ts +++ b/src/api/BackofficeClient.ts @@ -105,6 +105,7 @@ import { CreateStationMaintenance } from './generated/portal/CreateStationMainte import { InstitutionBaseResources } from './generated/portal/InstitutionBaseResources'; import { InstitutionDetail } from './generated/portal/InstitutionDetail'; import { QuicksightEmbedUrlResponse } from './generated/portal/QuicksightEmbedUrlResponse'; +import { IbanBulkOperationRequest } from './generated/portal/IbanBulkOperationRequest'; import { IbanDeletionRequest } from './generated/portal/IbanDeletionRequest'; import { IbanDeletionRequests } from './generated/portal/IbanDeletionRequests'; @@ -1089,6 +1090,17 @@ export const BackofficeApi = { }); return extractResponse(result, 200, onRedirectToLogin); }, + + handleBulkIbanOperations: async ( + creditorinstitutioncode: string, + ibanBulkOperationRequest: IbanBulkOperationRequest + ): Promise => { + const result = await backofficeClient.bulkIbanOperations({ + 'ci-code': creditorinstitutioncode, + body: ibanBulkOperationRequest, + }); + return extractResponse(result, 201, onRedirectToLogin); + } }, ibanDeletionRequest: { diff --git a/src/locale/it.json b/src/locale/it.json index 5135f0a22..02d6f734a 100644 --- a/src/locale/it.json +++ b/src/locale/it.json @@ -1018,7 +1018,7 @@ "fields": { "ibanUploadTypes": { "single": "Caricamento singolo", - "multiple": "Caricamento massivo" + "multiple": "Gestione multipla" }, "iban": { "ibanCode": "Codice IBAN", @@ -1075,6 +1075,17 @@ "ecOwnerNotValid": "Codice fiscale non valido", "bankIbanConflict": "Impossibile creare l’IBAN in quanto risulta essere già censito sul sistema" } + }, + "handleMultiIbanEditIbanPage": { + "title": "Gestione multipla", + "subtitle": "Carica il file con la lista degli IBAN da aggiungere, aggiornare o rimuovere.", + "csvForm": { + "dropzoneLabel": "Trascina qui il file con la lista degli IBAN oppure carica il file.", + "rejectedLabel": "Formato file non supportato. Usa solo file CSV." + }, + "helpText": "Non sai come preparare la lista?", + "helpLink": "Scarica il file di esempio", + "validationSummary": "Verranno aggiunti {{toAdd}}, aggiornati {{toUpdate}} e rimossi {{toDelete}}." }, "commissionBundlesPage": { "title": "Pacchetti commissioni", diff --git a/src/pages/iban/addEditIban/AddEditIbanForm.tsx b/src/pages/iban/addEditIban/AddEditIbanForm.tsx index f9074a389..24038fc0c 100644 --- a/src/pages/iban/addEditIban/AddEditIbanForm.tsx +++ b/src/pages/iban/addEditIban/AddEditIbanForm.tsx @@ -1,10 +1,13 @@ + import {useTranslation} from 'react-i18next'; import {theme} from '@pagopa/mui-italia'; import { + Alert, Button, FormControl, FormControlLabel, Grid, + Link, Paper, Radio, RadioGroup, @@ -14,6 +17,7 @@ import { Typography, } from '@mui/material'; import {Box} from '@mui/system'; +import {SingleFileInput} from '@pagopa/mui-italia'; import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; import {useEffect, useState} from 'react'; @@ -23,14 +27,17 @@ import {useFormik} from 'formik'; import {useHistory} from 'react-router-dom'; import {useErrorDispatcher, useLoading} from '@pagopa/selfcare-common-frontend'; import {add, differenceInCalendarDays} from 'date-fns'; +import { format } from 'date-fns'; import ROUTES from '../../../routes'; import {LOADING_TASK_CREATE_IBAN} from '../../../utils/constants'; import {IbanFormAction, IbanOnCreation} from '../../../model/Iban'; import {useAppSelector} from '../../../redux/hooks'; import {partiesSelectors} from '../../../redux/slices/partiesSlice'; import {extractProblemJson} from '../../../utils/client-utils'; -import {createIban, updateIban} from '../../../services/ibanService'; +import {createIban, handleBulkIbanOperations, updateIban} from '../../../services/ibanService'; import {isIbanValidityDateEditable, isValidIBANNumber} from '../../../utils/common-utils'; +import {validateIbanCsvData, ValidationResult} from '../../../utils/iban-csv-to-upload-parser'; +import { OperationEnum } from '../../../api/generated/portal/IbanOperation'; import AddEditIbanFormSectionTitle from './components/AddEditIbanFormSectionTitle'; type Props = { @@ -46,6 +53,9 @@ const AddEditIbanForm = ({goBack, ibanBody, formAction}: Props) => { const {t} = useTranslation(); const [subject, setSubject] = useState('me'); const [uploadType, setUploadType] = useState('single'); + const [file, setFile] = useState(null); + const [validationResult, setValidationResult] = useState(null); + const [showValidation, setShowValidation] = useState(false); const history = useHistory(); const addError = useErrorDispatcher(); const setLoading = useLoading(LOADING_TASK_CREATE_IBAN); @@ -64,6 +74,41 @@ const AddEditIbanForm = ({goBack, ibanBody, formAction}: Props) => { const changeUploadType = (event: any) => { setUploadType(event.target.value); + if (event.target.value === 'single') { + setFile(null); + setValidationResult(null); + setShowValidation(false); + } + }; + + const handleFileSelect = async (selectedFile: File) => { + setFile(selectedFile); + setShowValidation(false); + setValidationResult(null); + + try { + const text = await selectedFile.text(); + const result = validateIbanCsvData(text); + setValidationResult(result); + setShowValidation(true); + } catch (error) { + addError({ + id: 'CSV_PARSE_ERROR', + blocking: false, + error: error as Error, + techDescription: 'Error parsing CSV file', + toNotify: true, + displayableTitle: 'Errore lettura file', + displayableDescription: 'Impossibile leggere il file CSV', + component: 'Toast', + }); + } + }; + + const handleFileRemove = () => { + setFile(null); + setValidationResult(null); + setShowValidation(false); }; const initialFormData = (ibanBody?: IbanOnCreation) => @@ -123,30 +168,30 @@ const AddEditIbanForm = ({goBack, ibanBody, formAction}: Props) => { }; const enableSubmit = (values: IbanOnCreation) => { - const baseCondition = - values.iban !== '' && - values.description !== '' && - values.validity_date && - values.validity_date.getTime() > 0 && - values.due_date && - values.due_date.getTime() > 0; - if (uploadType === 'single') { + const baseCondition = + values.iban !== '' && + values.description !== '' && + values.validity_date && + values.validity_date.getTime() > 0 && + values.due_date && + values.due_date.getTime() > 0; + if (baseCondition && subject === 'me') { return true; } else { return baseCondition && subject === 'anotherOne' && values.creditor_institution_code !== ''; } } else { - return true; + return file !== null && validationResult !== null && validationResult.valid; } }; // eslint-disable-next-line sonarjs/cognitive-complexity const submit = async (values: IbanOnCreation) => { - if (uploadType === 'single') { - setLoading(true); - try { + setLoading(true); + try { + if (uploadType === 'single') { if (formAction === IbanFormAction.Create) { await createIban(ecCode, { iban: values.iban.toUpperCase().trim(), @@ -165,27 +210,39 @@ const AddEditIbanForm = ({goBack, ibanBody, formAction}: Props) => { is_active: true, }); } - - history.push(ROUTES.IBAN); - } catch (reason: any) { - const problemJson = extractProblemJson(reason); - if (problemJson?.status === 409) { - formik.setFieldError('iban', t('addEditIbanPage.validationMessage.bankIbanConflict')); - } else { - addError({ - id: 'CREATE_UPDATE_IBAN', - blocking: false, - error: reason as Error, - techDescription: `An error occurred while adding/editing iban`, - toNotify: true, - displayableTitle: t('addEditIbanPage.errors.createIbanTitle'), - displayableDescription: t('addEditIbanPage.errors.createIbanMessage'), - component: 'Toast', - }); + } else { + if (validationResult && validationResult.valid) { + await handleBulkIbanOperations(ecCode, { + operations: validationResult.data.map(item => ({ + creditorInstitutionCode: ecCode, + ibanValue: item.iban.toUpperCase().trim(), + operation: OperationEnum[item.operazione], + validityDate: format(item.dataattivazioneiban, 'yyyy-MM-dd'), + description: item.descrizione + })) + }); } - } finally { - setLoading(false); } + + history.push(ROUTES.IBAN); + } catch (reason: any) { + const problemJson = extractProblemJson(reason); + if (problemJson?.status === 409) { + formik.setFieldError('iban', t('addEditIbanPage.validationMessage.bankIbanConflict')); + } else { + addError({ + id: 'CREATE_UPDATE_IBAN', + blocking: false, + error: reason as Error, + techDescription: `An error occurred while adding/editing iban`, + toNotify: true, + displayableTitle: t('addEditIbanPage.errors.createIbanTitle'), + displayableDescription: t('addEditIbanPage.errors.createIbanMessage'), + component: 'Toast', + }); + } + } finally { + setLoading(false); } }; @@ -229,140 +286,197 @@ const AddEditIbanForm = ({goBack, ibanBody, formAction}: Props) => { checked={uploadType === 'multiple'} value="multiple" control={} - disabled label={t('addEditIbanPage.addForm.fields.ibanUploadTypes.multiple')} data-testid="upload-multiple-test" /> - - - {t('addEditIbanPage.title')} - - - {t('addEditIbanPage.subtitle')} - + {uploadType === 'single' ? ( + + + {t('addEditIbanPage.title')} + - - - } - /> - - - formik.handleChange(e)} - error={formik.touched.iban && Boolean(formik.errors.iban)} - helperText={formik.touched.iban && formik.errors.iban} + + {t('addEditIbanPage.subtitle')} + + + + + } + /> + + + formik.handleChange(e)} + error={formik.touched.iban && Boolean(formik.errors.iban)} + helperText={formik.touched.iban && formik.errors.iban} inputProps={{ 'data-testid': 'iban-test', }} - /> - + /> + - - formik.handleChange(e)} - error={formik.touched.description && Boolean(formik.errors.description)} - helperText={formik.touched.description && formik.errors.description} + + formik.handleChange(e)} + error={formik.touched.description && Boolean(formik.errors.description)} + helperText={formik.touched.description && formik.errors.description} inputProps={{ 'data-testid': 'description-test', }} - /> + /> + - - - - } - /> - - - - + + } + /> + + + + formik.setFieldValue('validity_date', e)} - renderInput={(params: TextFieldProps) => ( - formik.setFieldValue('validity_date', e)} + renderInput={(params: TextFieldProps) => ( + - )} - shouldDisableDate={(date: Date) => date < new Date()} - /> - - - - - formik.setFieldValue('due_date', e)} - renderInput={(params: TextFieldProps) => ( - + )} + shouldDisableDate={(date: Date) => date < new Date()} + /> + + + + + formik.setFieldValue('due_date', e)} + renderInput={(params: TextFieldProps) => ( + - )} - shouldDisableDate={(date: Date) => date < formik.values.validity_date} - /> - + id="dueDate" + name="dueDate" + type="date" + size="small" + error={formik.touched.due_date && Boolean(formik.errors.due_date)} + helperText={formik.touched.due_date && formik.errors.due_date} + /> + )} + shouldDisableDate={(date: Date) => date < formik.values.validity_date} + /> + + - + + + + ) : ( + + + + {t('handleMultiIbanEditIbanPage.title')} + + + + {t('handleMultiIbanEditIbanPage.subtitle')} + + + + - - + + + {t('handleMultiIbanEditIbanPage.helpText')}{' '} + {t('handleMultiIbanEditIbanPage.helpLink')} + + + {showValidation && validationResult && ( + + {validationResult.valid ? ( + + {t('handleMultiIbanEditIbanPage.validationSummary', { + toAdd: validationResult.summary.toAdd, + toUpdate: validationResult.summary.toUpdate, + toDelete: validationResult.summary.toDelete + })} + + ) : ( + + + {validationResult.errors.map((error, index) => ( +
  • + {error} +
  • + ))} +
    +
    + )} +
    + )} +
    + )} +