From af51627e41df512b30422f07e285cb0c765db583 Mon Sep 17 00:00:00 2001 From: tristancusiCGI Date: Thu, 14 Nov 2024 14:22:41 -0800 Subject: [PATCH 1/2] * changed viewLocations title to be more clear, splitting it into two lines and adding the title field names. --- .../bcer-data-portal/app/src/views/ViewLocation.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/bcer-data-portal/app/src/views/ViewLocation.tsx b/packages/bcer-data-portal/app/src/views/ViewLocation.tsx index 54aa6366..313c8301 100644 --- a/packages/bcer-data-portal/app/src/views/ViewLocation.tsx +++ b/packages/bcer-data-portal/app/src/views/ViewLocation.tsx @@ -469,7 +469,15 @@ function LocationsContent() { authConfig && <> - {data.business.businessName} {data.doingBusinessAs ? "("+data.doingBusinessAs+")": ""} + + Business Name: {data.business.businessName} + {data.doingBusinessAs && ( + <> +
+ Doing Business As: {data.doingBusinessAs} + + )} +
From 8d2ff87fd3691a75726851a806d9fee2e4b93ad2 Mon Sep 17 00:00:00 2001 From: chloe-yuu <84537348+chloe-yuu@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:50:22 -0800 Subject: [PATCH 2/2] adding loading functionality to confirm location page on retailer site --- .../MyBusinessComponents/ConfirmLocations.tsx | 64 +++----- .../components/MyBusiness/MyBusinessNav.tsx | 139 ++++++++-------- .../app/src/components/stepper/Bottom.tsx | 4 +- .../app/src/hooks/useCsvValidator.ts | 150 ++++++++++++------ 4 files changed, 195 insertions(+), 162 deletions(-) diff --git a/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessComponents/ConfirmLocations.tsx b/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessComponents/ConfirmLocations.tsx index e3ea1e6f..85830f05 100644 --- a/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessComponents/ConfirmLocations.tsx +++ b/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessComponents/ConfirmLocations.tsx @@ -5,6 +5,7 @@ import { Checkbox, FormControlLabel, makeStyles, Typography } from '@mui/materia import SaveAltIcon from '@mui/icons-material/SaveAlt' import { StyledButton, StyledTable, StyledConfirmDialog } from 'vaping-regulation-shared-components'; import WarningIcon from '@mui/icons-material/Warning'; +import CircularProgress from '@mui/material/CircularProgress'; import { BusinessLocationHeaders } from '@/constants/localEnums'; import { BusinessInfoContext } from '@/contexts/BusinessInfo'; @@ -17,7 +18,6 @@ import { LocationUtil } from '@/utils/location.util'; import FullScreen from '@/components/generic/FullScreen'; import TableWrapper from '@/components/generic/TableWrapper'; import { getInitialPagination } from '@/utils/util'; -import { useAxiosGet } from '@/hooks/axios'; const PREFIX = 'ConfirmLocations'; @@ -110,7 +110,12 @@ const Root = styled('div')(({ theme }) => ({ }, })); -export default function ConfirmLocations () { +interface ConfirmLocationsProps { + isLoading: boolean; + setIsLoading: (loading: boolean) => void; +} + +export default function ConfirmLocations({ isLoading, setIsLoading }: ConfirmLocationsProps) { const [businessInfo, setBusinessInfo] = useContext(BusinessInfoContext); const [targetRow, setTargetRow] = useState(); @@ -119,18 +124,19 @@ export default function ConfirmLocations () { const [filterTable, setFilterTable] = useState(false); const viewFullscreenTable = useState(false); const [newLocations, setNewLocations] = useState>([]); - const {errors: uploadErrors, validatedData, validateCSV} = useCsvValidator(); - const [{ data: addressExistsData }, checkAddressExists] = useAxiosGet('', { manual: true }); - const [duplicateWarning, setDuplicateWarning] = useState(""); - const [duplicateCount, setDuplicateCount] = useState(0); + const { errors: uploadErrors, validatedData, validateCSV, duplicateWarning, duplicateCount } = useCsvValidator(); useEffect(() => { - setNewLocations(businessInfo.locations.filter((l: any) => !l.id)); //reset newLocations + setNewLocations(businessInfo.locations.filter((l: any) => !l.id)); //locations without id are new }, [businessInfo.locations]) useEffect(() => { - validateCSV(BusinessCsvValidation, newLocations) - verifyDuplicates(); + const validateAndCheckDuplicates = async () => { + setIsLoading(true); + await validateCSV(BusinessCsvValidation, newLocations); + setIsLoading(false); + }; + validateAndCheckDuplicates(); }, [newLocations]); useEffect(() => { @@ -158,42 +164,14 @@ export default function ConfirmLocations () { setOpenEdit(true); } - const docheckAddressExists = async(fullAddress: string) => { - const response = await checkAddressExists({ url: `/location/check-address-exists?address=${fullAddress}` }); - return response.data; + if (isLoading) { + return ( +
+ +
+ ); } - const verifyDuplicates = async () => { - if (newLocations.length === 0) return; - - const updatedLocations = [...newLocations]; - let duplicateWarnings = ""; - let duplicateCount = 0; - let hasChanges = false; - - for (let i = 0; i < newLocations.length; i++) { - const location = newLocations[i]; - if (!location.tableData) location.tableData = { id: i }; - if (location.addressLine1) { - const addressExists = await docheckAddressExists(location.addressLine1); - if (addressExists) { - duplicateWarnings = duplicateWarnings + location.addressLine1 + '; '; - duplicateCount++; - } - if (updatedLocations[i].addressExists !== addressExists) { - updatedLocations[i] = { ...location, addressExists: addressExists }; - hasChanges = true; - } - } - } - if (hasChanges) {// Update the businessInfo state with the new locations only if there are changes - const existingLocations = businessInfo.locations.filter((l: any) => !!l.id); - setBusinessInfo({ ...businessInfo, locations: [...existingLocations, ...updatedLocations] }); - } - setDuplicateWarning(duplicateWarnings); - setDuplicateCount(duplicateCount); - }; - return ( (
diff --git a/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessNav.tsx b/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessNav.tsx index bd29e093..f6ada75a 100644 --- a/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessNav.tsx +++ b/packages/bcer-retailer-app/app/src/components/MyBusiness/MyBusinessNav.tsx @@ -54,77 +54,78 @@ const StyledChatBubbleOutlineIcon = styled(ChatBubbleOutlineIcon)({ paddingRight: '25px', }); -// NB: move steps to their own file -// the presence/logic of execIf should be revisited -const steps = [ - { - icon: WorkOutlineIcon, - label: 'Business Details', - title: 'Confirm Your Business Details', - path: '/business/details', - component: , - helpText:

Please confirm the business details that were entered when registering for your BCeID. - If you sell e-substances from this location, please add it as a location in the "Add Business Locations" section. - You must also add any additional locations from which you sell e-substances.

, - showSubscription: true, - canAdvanceChecks: [ - { - property: 'locations', - validate: (val: Array) => val.length, - }, - { - property: 'detailsComplete', - validate: (val: boolean) => val, - } - ], - onAdvance: [{ - endpoint: process.env.BASE_URL + '/submission', - method: 'PATCH' as Method, - execIf: { validate: () => true } - }] - }, - { - icon: MapOutlinedIcon, - label: 'Confirm Locations', - title: 'Confirm New Business Locations', - path: '/business/map', - component: , - helpText: 'Confirm the details of the business locations that you have added on the previous page. You will be able to update this information at any time. Upon completion of this section you will be able to complete a Notice of Intent to sell E-substances and submit Product and Manufacturing Reports for each location you have listed.', - canAdvanceChecks: [ - { - property: 'uploadErrors', - validate: (val: Array) => val?.length === 0, - } - ], - onAdvance: [{ - endpoint: process.env.BASE_URL + '/submission', - method: 'PATCH' as Method, - execIf: { - property: 'entry', - validate: (val?: string) => val === 'upload', - }, - }] - }, - { - icon: AssignmentTurnedInOutlinedIcon, - label: 'Confirm Business Details and Submit', - title: 'Confirm Business Details and Submit', - path: '/business/confirm', - component: , - helpText: '', - onAdvance: [{ - datakey: 'submissionId', - endpoint: process.env.BASE_URL + '/submission/save', - method: 'POST' as Method, - execIf: { validate: () => true } - }] - }, -] - export default function MyBusinessNav () { const [appGlobal, setAppGlobal] = useContext(AppGlobalContext); const { pathname } = useLocation(); + const [isLoading, setIsLoading] = useState(false); //A loading state for the stepper Next button + + // NB: move steps to their own file + // the presence/logic of execIf should be revisited + const steps = [ + { + icon: WorkOutlineIcon, + label: 'Business Details', + title: 'Confirm Your Business Details', + path: '/business/details', + component: , + helpText:

Please confirm the business details that were entered when registering for your BCeID. + If you sell e-substances from this location, please add it as a location in the "Add Business Locations" section. + You must also add any additional locations from which you sell e-substances.

, + showSubscription: true, + canAdvanceChecks: [ + { + property: 'locations', + validate: (val: Array) => val.length, + }, + { + property: 'detailsComplete', + validate: (val: boolean) => val, + } + ], + onAdvance: [{ + endpoint: process.env.BASE_URL + '/submission', + method: 'PATCH' as Method, + execIf: { validate: () => true } + }] + }, + { + icon: MapOutlinedIcon, + label: 'Confirm Locations', + title: 'Confirm New Business Locations', + path: '/business/map', + component: , + helpText: 'Confirm the details of the business locations that you have added on the previous page. You will be able to update this information at any time. Upon completion of this section you will be able to complete a Notice of Intent to sell E-substances and submit Product and Manufacturing Reports for each location you have listed.', + canAdvanceChecks: [ + { + property: 'uploadErrors', + validate: (val: Array) => val?.length === 0, + } + ], + onAdvance: [{ + endpoint: process.env.BASE_URL + '/submission', + method: 'PATCH' as Method, + execIf: { + property: 'entry', + validate: (val?: string) => val === 'upload', + }, + }] + }, + { + icon: AssignmentTurnedInOutlinedIcon, + label: 'Confirm Business Details and Submit', + title: 'Confirm Business Details and Submit', + path: '/business/confirm', + component: , + helpText: '', + onAdvance: [{ + datakey: 'submissionId', + endpoint: process.env.BASE_URL + '/submission/save', + method: 'POST' as Method, + execIf: { validate: () => true } + }] + }, + ] // Fetch initial business details from local storage const initialBusinessDetails = JSON.parse(localStorage.getItem('BusinessDetailesValues') || '{}'); @@ -144,12 +145,11 @@ export default function MyBusinessNav () { uploadErrors: [] }) - const stepperOptions = steps.map(element => ({ path: element.path, icon: element.icon, label: element.label })) - const [{ data: profile, error: profileError }] = useAxiosPost('/users/profile'); const [{ loading, error, response, data: submission }, get] = useAxiosGet('/submission', { manual: true }); const [{ loading: postLoading, error: postError, data: newSubmission }, post] = useAxiosPost('/submission', { manual: true }); + const stepperOptions = steps.map(element => ({ path: element.path, icon: element.icon, label: element.label })) // Fetch initial data useEffect(() => { @@ -297,6 +297,7 @@ export default function MyBusinessNav () { dataForContext={businessInfo} steps={steps.map(step => ({ path: step.path }))} currentStep={currentStep} + isLoading={isLoading} {...steps[currentStep]} /> )} diff --git a/packages/bcer-retailer-app/app/src/components/stepper/Bottom.tsx b/packages/bcer-retailer-app/app/src/components/stepper/Bottom.tsx index 712666d4..711733ab 100644 --- a/packages/bcer-retailer-app/app/src/components/stepper/Bottom.tsx +++ b/packages/bcer-retailer-app/app/src/components/stepper/Bottom.tsx @@ -44,6 +44,7 @@ type BottomStepperProps = { steps: Array<{ path: string }>; currentStep: number; onAdvance?: OnAdvance[]; + isLoading?: boolean; } export default function Bottom ({ @@ -55,6 +56,7 @@ export default function Bottom ({ onAdvance, steps, currentStep, + isLoading, }: BottomStepperProps) { @@ -155,7 +157,7 @@ export default function Bottom ({ ) : ( {'Next'} diff --git a/packages/bcer-retailer-app/app/src/hooks/useCsvValidator.ts b/packages/bcer-retailer-app/app/src/hooks/useCsvValidator.ts index 601898b3..11837c91 100644 --- a/packages/bcer-retailer-app/app/src/hooks/useCsvValidator.ts +++ b/packages/bcer-retailer-app/app/src/hooks/useCsvValidator.ts @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import * as yup from 'yup'; import { useAxiosGet } from './axios'; -import Axios from 'axios' import { GeoCodeUtil } from '@/utils/geoCoder.util'; const HealthAuthorities: { [key: string]: string } = { @@ -14,58 +13,111 @@ const HealthAuthorities: { [key: string]: string } = { }; export const useCsvValidator = () => { - const [errors, setErrors] = useState>() + const [errors, setErrors] = useState>(); const [validatedData, setValidatedData] = useState>(); + const [{ data: addressExistsData }, checkAddressExists] = useAxiosGet('', { manual: true }); + const [duplicateWarning, setDuplicateWarning] = useState(''); + const [duplicateCount, setDuplicateCount] = useState(0); - return { - errors, - validatedData, - validateCSV: async(validationSchema: yup.ObjectSchema, uploadData: Array) => { - let errorArray: Array = []; - let validatedDataArray: Array = []; - - await Promise.all(uploadData.map(async(element, index) => { - element.location_type = element.location_type ? element.location_type : 'physical'; - element.error = undefined; - try { - const validatedDto = await validationSchema.validateSync(element, { abortEarly: false }); - - if (validatedDto.addressLine1 !== "") { - try { - const data = await GeoCodeUtil.geoCodeAddress(validatedDto.addressLine1); + const docheckAddressExists = async (fullAddress: string) => { + try { + const response = await checkAddressExists({ url: `/location/check-address-exists?address=${fullAddress}` }); + return response.data; + } catch (error) { + console.error('Error checking address exists:', error); + } + }; + + const validateCSV = async (validationSchema: yup.ObjectSchema, uploadData: Array) => { + let errorArray: Array = []; + let validatedDataArray: Array = []; + let duplicateWarnings = ''; + let duplicateCount = 0; + let hasChanges = false; + + const updatedLocations = [...uploadData]; + + await Promise.all( + uploadData.map(async (element, index) => { + element.location_type = element.location_type ? element.location_type : 'physical'; + element.error = undefined; - // Features prop will only ever have length 0 or 1 + if (!element.tableData) { element.tableData = { id: index };} + try { + const validatedDto = await validationSchema.validateSync(element, { abortEarly: false }); + // Check for errors + if (validatedDto.addressLine1 !== '') { + try { + const data = await GeoCodeUtil.geoCodeAddress(validatedDto.addressLine1); + // Features prop will only ever have length 0 or 1 + if (data.features.length === 0 || data.features[0]?.properties.precisionPoints < 70) { + errorArray.push({ + row: index + 2, + field: 'Geocoder Error', + message: 'We were unable to find a matching address. Please edit the location details.', + }); + element.error = true; + } else { + const ha = await GeoCodeUtil.getHealthAuthority( + `/location/determine-health-authority?lat=${data.features[0].geometry.coordinates[1]}&long=${data.features[0].geometry.coordinates[0]}` + ); + const formattedHealthAuthority = ha.toLowerCase(); + element.geoAddressConfidence = data.features[0].properties.precisionPoints; + element.longitude = data.features[0].geometry.coordinates[0]; + element.latitude = data.features[0].geometry.coordinates[1]; + element.health_authority = formattedHealthAuthority; + element.health_authority_display = HealthAuthorities[formattedHealthAuthority]; + } + } catch (requestError) { + console.log(requestError); + } + } - if (data.features.length === 0 || data.features[0]?.properties.precisionPoints < 70) { - errorArray.push({row: index + 2, field: 'Geocoder Error', message: 'We were unable to find a matching address. Please edit the location details.'}) - element.error = true; - } else { - const ha = await GeoCodeUtil.getHealthAuthority(`/location/determine-health-authority?lat=${data.features[0].geometry.coordinates[1]}&long=${data.features[0].geometry.coordinates[0]}`) - const formattedHealthAuthority = ha.toLowerCase() - // Set the element's confidence interval, latitude, longitude, health authority, and props from the retured geocoder data - element.geoAddressConfidence = data.features[0].properties.precisionPoints - element.longitude = data.features[0].geometry.coordinates[0] - element.latitude = data.features[0].geometry.coordinates[1] - element.health_authority = formattedHealthAuthority - element.health_authority_display = HealthAuthorities[formattedHealthAuthority] - } - } catch (requestError) { - console.log(requestError) - } - } - validatedDataArray.push(element); - } catch (validationError) { - console.log(validationError) + //Check for duplicates (when there is no error and addressLine1 is not empty) + if (!element.error && validatedDto.addressLine1) { + const addressExists = await docheckAddressExists(validatedDto.addressLine1); + if (addressExists) { + duplicateWarnings += validatedDto.addressLine1 + '; '; + duplicateCount++; + } + if (updatedLocations[index].addressExists !== addressExists) { + updatedLocations[index] = { ...element, addressExists: addressExists }; + hasChanges = true; + } + } + validatedDataArray.push(element); + } catch (validationError) { + //@ts-ignore + if (Array.isArray(validationError.inner)) { //@ts-ignore - // needed to access validationError.inner - validationError.inner.map((error: any) => errorArray.push({row: index + 2, field: error.path, message: error.message})) as any - element.error = true; - validatedDataArray.push(element) - } - })) + validationError.inner.forEach((error: any) => { + errorArray.push({ + row: index + 2, + field: error.path, + message: error.message, + }); + }); + } else { + console.error("Validation error does not have an 'inner' array:", validationError); + } - setErrors(errorArray) - setValidatedData(validatedDataArray) - } + element.error = true; + validatedDataArray.push(element); + } + }) + ); + + setErrors(errorArray); + setValidatedData(validatedDataArray); + setDuplicateWarning(duplicateWarnings); + setDuplicateCount(duplicateCount); } -} \ No newline at end of file + + return { + errors, + validatedData, + duplicateWarning, + duplicateCount, + validateCSV, + }; +}; \ No newline at end of file