client/components/mma/delivery/records/DeliveryRecords.tsx (675 lines of code) (raw):

import { css } from '@emotion/react'; import { from, headlineBold28, neutral, palette, space, textSans15, textSans17, textSansBold17, until, } from '@guardian/source/foundations'; import { Button, Stack } from '@guardian/source/react-components'; import { capitalize } from 'lodash'; import { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; import { dateIsSameOrBefore, parseDate } from '../../../../../shared/dates'; import type { DeliveryAddress, DeliveryRecordApiItem, PaidSubscriptionPlan, } from '../../../../../shared/productResponse'; import { getMainPlan, isGift, isPaidSubscriptionPlan, } from '../../../../../shared/productResponse'; import type { DeliveryProblemType } from '../../../../../shared/productTypes'; import { holidaySuspensionDeliveryProblem } from '../../../../../shared/productTypes'; import { trackEvent } from '../../../../utilities/analytics'; import { CallCentreEmailAndNumbers } from '../../../shared/CallCenterEmailAndNumbers'; import { FormError } from '../../../shared/FormError'; import { InfoIconDark } from '../../shared/assets/InfoIconDark'; import type { ProductDescriptionListKeyValue } from '../../shared/ProductDescriptionListTable'; import { ProgressIndicator } from '../../shared/ProgressIndicator'; import { DeliveryAddressStep } from './DeliveryAddressStep'; import { DeliveryRecordCard } from './DeliveryRecordCard'; import type { DeliveryRecordDetail } from './deliveryRecordsApi'; import type { DeliveryRecordsContextInterface } from './DeliveryRecordsContainer'; import { checkForExistingDeliveryProblem, DeliveryRecordsContext, } from './DeliveryRecordsContainer'; import { PaginationNav } from './DeliveryRecordsPaginationNav'; import type { DeliveryRecordsProblemType } from './DeliveryRecordsProblemContext'; import { DeliveryRecordsAddressContext } from './DeliveryRecordsProblemContext'; import { DeliveryRecordProblemForm } from './DeliveryRecordsProblemForm'; import { ProductDetailsTable } from './ProductDetailsTable'; interface IdentityDetails { userId: string; } declare global { interface Window { identityDetails?: IdentityDetails; } } export enum PageStatus { ReadOnly, ReportIssueStep1, ReportIssueStep2, ContinueToReview, ReportIssueConfirmation, CannotReportProblem, } interface Step1FormValidationDetails { isValid: boolean; message?: string; } const checkForRecentHolidayStop = (records: DeliveryRecordDetail[]) => records.findIndex((record) => record.hasHolidayStop) > -1; export const DeliveryRecords = () => { const navigate = useNavigate(); const { productDetail, productType, data } = useContext( DeliveryRecordsContext, ) as DeliveryRecordsContextInterface; const [pageStatus, setPageStatus] = useState<PageStatus>( PageStatus.ReadOnly, ); const [currentPage, setCurrentPage] = useState(0); const [selectedProblemRecords, setSelectedProblemRecords] = useState< string[] >([]); const [step1formValidationState, setStep1formValidationState] = useState<boolean>(false); const [step1FormValidationDetails, setStep1FormValidationDetails] = useState<Step1FormValidationDetails>({ isValid: true }); const [step2formValidationState, setStep2formValidationState] = useState<boolean>(false); const [step2FormValidationDetails, setStep2FormValidationDetails] = useState<Step1FormValidationDetails>({ isValid: true }); const [step3formValidationState, setStep3formValidationState] = useState<boolean>(false); const [step3FormValidationDetails, setStep3FormValidationDetails] = useState<Step1FormValidationDetails>({ isValid: true }); const [addressInValidState, setAddressValidationState] = useState<boolean>(true); const [deliveryProblem, setDeliveryProblem] = useState<DeliveryRecordsProblemType>(); const [showTopCallCentreNumbers, setTopCallCentreNumbersVisibility] = useState<boolean>(false); const [choosenDeliveryProblem, setChoosenDeliveryProblem] = useState<string>(); const [showBottomCallCentreNumbers, setBottomCallCentreNumbersVisibility] = useState<boolean>(false); const [address, setAddress] = useState<DeliveryAddress | undefined>( productDetail.subscription.deliveryAddress, ); const [productsAffected, setProductsAffected] = useState< ProductDescriptionListKeyValue[] >([]); const mainPlan = getMainPlan( productDetail.subscription, ) as PaidSubscriptionPlan; if (!isPaidSubscriptionPlan(mainPlan)) { throw new Error( 'mainPlan is not a PaidSubscriptionPlan in deliveryRecords', ); } const subscriptionCurrency = mainPlan.currency; const hasExistingDeliveryProblem = checkForExistingDeliveryProblem( data.results, ); const isHolidayStopProblem = choosenDeliveryProblem === holidaySuspensionDeliveryProblem.label; const isCancelledSubscription = productDetail.subscription.cancelledAt; const subscriptionIsAutoRenewable = productDetail.subscription.autoRenew; const hasReportedProblemAndShouldBeContacted = hasExistingDeliveryProblem && productType.delivery?.records?.contactUserOnExistingProblemReport; const showProblemCredit = !isHolidayStopProblem && !isCancelledSubscription && subscriptionIsAutoRenewable && !hasReportedProblemAndShouldBeContacted; useEffect(() => { if (addressInValidState) { setStep3FormValidationDetails({ isValid: addressInValidState, }); setStep3formValidationState(!addressInValidState); } }, [addressInValidState]); const enableDeliveryInstructions = !!productType.delivery.enableDeliveryInstructionsUpdate; const step1FormRadioOptionCallback = (value: string) => setChoosenDeliveryProblem(value); const step1FormUpdateCallback = (isValid: boolean, message?: string) => { setStep1formValidationState(false); setStep1FormValidationDetails({ isValid, message }); }; const step1FormSubmitListener = ( selectedValue: string | undefined, selectedMessage: string | undefined, ) => { setDeliveryProblem({ category: selectedValue, message: selectedMessage, }); setStep1formValidationState(true); if (step1FormValidationDetails.isValid) { trackEvent({ eventCategory: 'delivery-problem', eventAction: 'continue_to_step_2_button_click', product: { productType: productType, productDetail: productDetail, }, eventLabel: productType.urlPart, }); setPageStatus(PageStatus.ReportIssueStep2); } }; const addRecordToDeliveryProblem = (id: string) => setSelectedProblemRecords([...selectedProblemRecords, id]); const removeRecordFromDeliveryProblem = (id: string) => setSelectedProblemRecords( selectedProblemRecords.filter( (existingId: string) => existingId !== id, ), ); const resultsPerPage = 7; const totalPages = Math.ceil(data.results.length / resultsPerPage); const scrollToTop = () => window.scrollTo(0, 0); const resetDeliveryRecordsPage = () => setPageStatus(PageStatus.ReadOnly); const filterData = (overridePageStatusCheck?: boolean) => { if ( (pageStatus !== PageStatus.ReadOnly && pageStatus !== PageStatus.CannotReportProblem) || overridePageStatusCheck ) { const numOfReportableRecords = productType.delivery.records.numberOfProblemRecordsToShow; const startOfToday = new Date(new Date().setHours(0, 0, 0, 0)); const isNotHolidayProblem = choosenDeliveryProblem !== holidaySuspensionDeliveryProblem.label; return data.results .filter((_) => { const startOfDeliveryDateDay = new Date( parseDate(_.deliveryDate).date.setHours(0, 0, 0, 0), ); return dateIsSameOrBefore( startOfDeliveryDateDay, startOfToday, ); }) .slice(0, numOfReportableRecords) .filter((_) => isNotHolidayProblem || _.hasHolidayStop) .filter((_) => !_.problemCaseId); } return data.results.filter((_, index) => isRecordInCurrentPage( index, currentPage * resultsPerPage, currentPage * resultsPerPage + resultsPerPage - 1, ), ); }; const isRecordInCurrentPage = ( index: number, currentPageStartIndex: number, currentPageEndIndex: number, ) => index >= currentPageStartIndex && index <= currentPageEndIndex; const filteredData = filterData(); const hasRecentHolidayStop = checkForRecentHolidayStop(filteredData); const problemTypes: DeliveryProblemType[] = [ ...productType.delivery.records.availableProblemTypes, ...(hasRecentHolidayStop ? [holidaySuspensionDeliveryProblem] : []), ].sort((a, b) => a.label.localeCompare(b.label)); const formErrorTitle = !step3FormValidationDetails.isValid && step1FormValidationDetails.isValid && step2FormValidationDetails.isValid ? 'Unfinished changes' : 'Some information is missing'; const formErrorMessages = [ step1FormValidationDetails, step2FormValidationDetails, step3FormValidationDetails, ].reduce( (acc: string[], validationDetails) => !validationDetails.isValid && validationDetails.message ? [...acc, validationDetails.message] : acc, [], ); return ( <DeliveryRecordsAddressContext.Provider value={{ address, setAddress, productsAffected, setProductsAffected, enableDeliveryInstructions, }} > {pageStatus !== PageStatus.ReadOnly && pageStatus !== PageStatus.CannotReportProblem && ( <ProgressIndicator steps={[ { title: 'Update', isCurrentStep: true }, { title: 'Review' }, { title: 'Confirmation' }, ]} additionalCSS={css` margin: ${space[5]}px 0 ${space[12]}px; `} /> )} <div css={css` margin: ${space[6]}px 0 ${space[12]}px; `} > <ProductDetailsTable productName={capitalize(productType.friendlyName)} subscriptionId={productDetail.subscription.subscriptionId} isGift={isGift(productDetail.subscription)} /> </div> {data.results.find((record) => !record.problemCaseId) && ( <> <h2 css={css` border-top: 1px solid ${neutral['86']}; ${headlineBold28}; ${until.tablet} { font-size: 1.25rem; line-height: 1.6; } `} > Report delivery problems </h2> <div css={css` margin-bottom: ${pageStatus !== PageStatus.ReportIssueStep2 ? space[12] : space[5]}px; ${textSans17}; `} > <p css={css` ${textSans17}; `} > Have you been experiencing problems with your delivery? Report it online and let us take care of it for you. Depending on the problem you’re having, you’ll either be automatically credited or escalated to customer service. It’s easy to use and only takes a couple of minutes. </p> <p css={css` ${textSans17}; `} > Please remember, you can also{' '} <span css={css` cursor: pointer; color: ${palette.brand[500]}; text-decoration: underline; `} onClick={() => setTopCallCentreNumbersVisibility( !showTopCallCentreNumbers, ) } > contact us </span>{' '} if you wish to speak to us in person. </p> {showTopCallCentreNumbers && ( <CallCentreEmailAndNumbers /> )} {pageStatus === PageStatus.CannotReportProblem && ( <span css={css` position: relative; display: block; margin: ${space[3]}px 0; padding: ${space[3]}px ${space[3]}px ${space[3]}px ${space[3] * 2 + 17}px; background-color: ${neutral[97]}; ${textSans15}; ${from.tablet} { margin: ${space[5]}px 0; } `} > <i css={css` position: absolute; top: ${space[3]}px; left: ${space[3]}px; `} > <InfoIconDark fillColor={palette.brand[500]} /> </i> You don't have any available delivery history to report. Your deliveries may be too far in the past or have already been reported. </span> )} {(pageStatus === PageStatus.ReadOnly || pageStatus === PageStatus.CannotReportProblem) && ( <Button onClick={() => { const filteredDataAtPresent = filterData(true); const canReportProblem = filteredDataAtPresent.length > 0; trackEvent({ eventCategory: 'delivery-problem', eventAction: 'report_delivery_problem_button_click', product: { productType, productDetail, }, eventLabel: productType.urlPart, }); if (canReportProblem) { setSelectedProblemRecords([]); setPageStatus( PageStatus.ReportIssueStep1, ); } else { setPageStatus( PageStatus.CannotReportProblem, ); } }} > Report a problem </Button> )} {(pageStatus === PageStatus.ReportIssueStep1 || pageStatus === PageStatus.ReportIssueStep2) && ( <> <DeliveryRecordProblemForm showNextStepButton={ pageStatus !== PageStatus.ReportIssueStep2 } onResetDeliveryRecordsPage={ resetDeliveryRecordsPage } onFormSubmit={step1FormSubmitListener} inValidationState={step1formValidationState} updateValidationStatusCallback={ step1FormUpdateCallback } updateRadioSelectionCallback={ step1FormRadioOptionCallback } problemTypes={problemTypes} /> </> )} </div> </> )} <h2 css={css` border-top: 1px solid ${neutral['86']}; ${headlineBold28}; opacity: ${pageStatus === PageStatus.ReportIssueStep1 && filteredData.length > 0 ? '0.5' : '1'}; ${pageStatus === PageStatus.ReportIssueStep2 ? ` background-color: ${neutral['97']}; border-left: 1px solid ${neutral['86']}; border-right: 1px solid ${neutral['86']}; margin: 0; padding: 14px 14px 14px; ${textSansBold17}; ` : ''} ${until.tablet} { ${pageStatus === PageStatus.ReportIssueStep2 ? `` : ` font-size: 1.25rem; line-height: 1.6; `} } `} > {pageStatus === PageStatus.ReportIssueStep2 ? 'Step 2. Select the date you have experienced the problem' : 'Deliveries'} </h2> {filteredData.length === 0 && pageStatus !== PageStatus.CannotReportProblem && (data.results.length === 0 ? ( <p css={css` ${textSans17}; `} > You haven't had a delivery for this subscription yet. In the future, details of your deliveries will appear here. </p> ) : ( <> <p css={css` ${textSans17}; `} > You currently have no deliveries that you can report a problem on based on the problem type that you have selected. </p> <p css={css` ${textSans17}; `} > If you are still having problems please{' '} <span css={css` cursor: pointer; color: ${palette.brand[500]}; text-decoration: underline; `} onClick={() => setBottomCallCentreNumbersVisibility( !showBottomCallCentreNumbers, ) } > Contact us </span> </p> </> ))} {filteredData.map( (deliveryRecord: DeliveryRecordApiItem, listIndex) => ( <DeliveryRecordCard key={deliveryRecord.id} deliveryRecord={deliveryRecord} listIndex={listIndex} pageStatus={pageStatus} deliveryProblemMap={data.deliveryProblemMap} addRecordToDeliveryProblem={addRecordToDeliveryProblem} removeRecordFromDeliveryProblem={ removeRecordFromDeliveryProblem } showDeliveryInstructions={ productType.delivery.records .showDeliveryInstructions } recordCurrency={subscriptionCurrency} isChecked={selectedProblemRecords.includes( deliveryRecord.id, )} productName={capitalize( productType.shortFriendlyName || productType.friendlyName, )} /> ), )} {totalPages > 1 && (pageStatus === PageStatus.ReadOnly || pageStatus === PageStatus.CannotReportProblem) && ( <PaginationNav resultsPerPage={resultsPerPage} totalNumberOfResults={data.results.length} currentPage={currentPage} setCurrentPage={setCurrentPage} changeCallBack={scrollToTop} /> )} {pageStatus === PageStatus.ReportIssueStep2 && ( <> <section css={css` border: 1px solid ${neutral['86']}; margin: ${space[5]}px 0 ${space[5]}px; padding: 0; `} > <h1 css={css` margin: 0; padding: ${space[3]}px; background-color: ${neutral['97']}; border-bottom: 1px solid ${neutral['86']}; ${textSansBold17}; ${from.tablet} { padding: ${space[3]}px ${space[5]}px; } `} > Step 3. Check your current delivery address {enableDeliveryInstructions && ' and instructions'} </h1> {productDetail.subscription.deliveryAddress && ( <DeliveryAddressStep productDetail={productDetail} enableDeliveryInstructions={ enableDeliveryInstructions } setAddressValidationState={ setAddressValidationState } /> )} </section> <div css={css` margin-top: ${space[6]}px; `} > {(step1formValidationState || step2formValidationState || step3formValidationState) && formErrorMessages.length > 0 && ( <FormError title={formErrorTitle} messages={formErrorMessages} /> )} <Button onClick={() => { setStep1formValidationState(true); const isStep2Valid = !!selectedProblemRecords.length; setStep2FormValidationDetails({ isValid: isStep2Valid, message: 'Step 2: Please select an affected delivery record.', }); setStep2formValidationState(!isStep2Valid); const isStep3Valid = addressInValidState; setStep3FormValidationDetails({ isValid: isStep3Valid, message: 'Step 3: Please save or discard your delivery address changes.', }); setStep3formValidationState(!isStep3Valid); if ( step1FormValidationDetails.isValid && isStep2Valid && isStep3Valid ) { trackEvent({ eventCategory: 'delivery-problem', eventAction: 'review_report_button_click', product: { productType, productDetail, }, eventLabel: productType.urlPart, }); setPageStatus(PageStatus.ContinueToReview); navigate('review', { state: { productDetail, affectedRecords: data.results.filter((record) => selectedProblemRecords.includes( record.id, ), ), problemType: deliveryProblem, showProblemCredit, }, }); } }} > Review your report </Button> <Button cssOverrides={css` ${textSans17}; background-color: transparent; font-weight: bold; margin-left: 22px; padding: 0; color: ${palette.brand[400]}; :hover { background-color: transparent; } `} onClick={() => { setPageStatus(PageStatus.ReadOnly); }} > Cancel </Button> <Stack space={5}> <p css={css` ${textSans17}; color: ${neutral[46]}; margin: ${space[6]}px 0 0; `} > If your delivery is not shown above, or you’d like to talk to someone,{' '} <span css={css` cursor: pointer; color: ${palette.brand[500]}; text-decoration: underline; `} onClick={() => setBottomCallCentreNumbersVisibility( !showBottomCallCentreNumbers, ) } > contact us </span> . </p> {showBottomCallCentreNumbers && ( <CallCentreEmailAndNumbers /> )} </Stack> </div> </> )} </DeliveryRecordsAddressContext.Provider> ); };