From 2eb67240c4262d5bf312b65386b971565511e514 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Fri, 4 Oct 2024 15:37:08 +0300 Subject: [PATCH] Add Edit customer information page --- src/components/SearchInput.tsx | 4 +- .../customerPortal/PortalCustomerInfos.tsx | 32 +-- .../customerPortal/PortalInvoicesList.tsx | 144 ++++++++-- .../customerPortal/PortalOverview.tsx | 184 ------------- .../customerPortal/common/SectionTitle.tsx | 10 +- .../CustomerInformationPage.tsx | 250 ++++++++++++++++++ .../customerPortal/wallet/WalletPage.tsx | 2 + .../customerPortal/wallet/WalletSection.tsx | 13 +- src/generated/graphql.tsx | 40 +++ src/pages/customerPortal/CustomerPortal.tsx | 36 ++- 10 files changed, 481 insertions(+), 234 deletions(-) delete mode 100644 src/components/customerPortal/PortalOverview.tsx create mode 100644 src/components/customerPortal/customerInformation/CustomerInformationPage.tsx diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index 76571faf3..f9564c9a6 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -8,15 +8,17 @@ import { Icon } from './designSystem' import { TextInput } from './form' interface SearchInputProps { + className?: string onChange: ReturnType['debouncedSearch'] placeholder?: string } -export const SearchInput = ({ onChange, placeholder }: SearchInputProps) => { +export const SearchInput = ({ className, onChange, placeholder }: SearchInputProps) => { const [localValue, setLocalValue] = useState('') return ( { diff --git a/src/components/customerPortal/PortalCustomerInfos.tsx b/src/components/customerPortal/PortalCustomerInfos.tsx index baaf10489..63d4a61dd 100644 --- a/src/components/customerPortal/PortalCustomerInfos.tsx +++ b/src/components/customerPortal/PortalCustomerInfos.tsx @@ -2,9 +2,12 @@ import { gql } from '@apollo/client' import { memo } from 'react' import styled from 'styled-components' +import SectionContainer from '~/components/customerPortal/common/SectionContainer' +import SectionTitle from '~/components/customerPortal/common/SectionTitle' import { Skeleton, Typography } from '~/components/designSystem' import { CountryCodes } from '~/core/constants/countryCodes' import { useGetPortalCustomerInfosQuery } from '~/generated/graphql' +import { useInternationalization } from '~/hooks/core/useInternationalization' import { NAV_HEIGHT, theme } from '~/styles' gql` @@ -27,16 +30,22 @@ gql` ` interface PortalCustomerInfosProps { - translate: Function + viewEditInformation: () => void } -export const PortalCustomerInfos = memo(({ translate }: PortalCustomerInfosProps) => { +const PortalCustomerInfos = ({ viewEditInformation }: PortalCustomerInfosProps) => { + const { translate } = useInternationalization() + const { data, loading } = useGetPortalCustomerInfosQuery() const customerPortalUser = data?.customerPortalUser return ( -
- {translate('text_6419c64eace749372fc72b07')} + + {loading ? ( @@ -152,11 +161,11 @@ export const PortalCustomerInfos = memo(({ translate }: PortalCustomerInfosProps )} -
+ ) -}) +} -PortalCustomerInfos.displayName = 'PortalCustomerInfos' +export default PortalCustomerInfos const InfoSkeletonContainer = styled.div` display: flex; @@ -171,14 +180,6 @@ const InfoSkeletonLine = styled.div` } ` -const Title = styled(Typography)` - display: flex; - align-items: center; - height: 40px; - box-shadow: ${theme.shadows[7]}; - margin-bottom: ${theme.spacing(6)}; -` - const InfosContainer = styled.section` display: flex; column-gap: ${theme.spacing(8)}; @@ -194,6 +195,7 @@ const InfosContainer = styled.section` const InfoLine = styled.div` display: flex; + flex-direction: column; align-items: flex-start; margin-bottom: ${theme.spacing(2)}; diff --git a/src/components/customerPortal/PortalInvoicesList.tsx b/src/components/customerPortal/PortalInvoicesList.tsx index 004b0fc36..a2a12a393 100644 --- a/src/components/customerPortal/PortalInvoicesList.tsx +++ b/src/components/customerPortal/PortalInvoicesList.tsx @@ -1,7 +1,11 @@ import { gql } from '@apollo/client' +import { Stack } from '@mui/material' import { DateTime } from 'luxon' +import { useEffect } from 'react' import styled, { css } from 'styled-components' +import SectionContainer from '~/components/customerPortal/common/SectionContainer' +import SectionTitle from '~/components/customerPortal/common/SectionTitle' import { Button, InfiniteScroll, @@ -12,6 +16,7 @@ import { Tooltip, Typography, } from '~/components/designSystem' +import { OverviewCard } from '~/components/OverviewCard' import { SearchInput } from '~/components/SearchInput' import { addToast } from '~/core/apolloClient' import { intlFormatNumber } from '~/core/formats/intlFormatNumber' @@ -26,6 +31,9 @@ import { PortalInvoiceListItemFragmentDoc, useCustomerPortalInvoicesLazyQuery, useDownloadCustomerPortalInvoiceMutation, + useGetCustomerPortalInvoicesCollectionLazyQuery, + useGetCustomerPortalOverdueBalancesLazyQuery, + useGetCustomerPortalUserCurrencyQuery, } from '~/generated/graphql' import { useDebouncedSearch } from '~/hooks/useDebouncedSearch' import { NAV_HEIGHT, theme } from '~/styles' @@ -68,11 +76,43 @@ gql` } } + query getCustomerPortalInvoicesCollection($expireCache: Boolean) { + customerPortalInvoiceCollections(expireCache: $expireCache) { + collection { + amountCents + invoicesCount + currency + } + } + } + + query getCustomerPortalOverdueBalances($expireCache: Boolean) { + customerPortalOverdueBalances(expireCache: $expireCache) { + collection { + amountCents + currency + lagoInvoiceIds + } + } + } + + query getCustomerPortalUserCurrency { + customerPortalUser { + currency + } + } + ${PortalInvoiceListItemFragmentDoc} ${InvoiceForFinalizeInvoiceFragmentDoc} ${InvoiceForUpdateInvoicePaymentStatusFragmentDoc} ` +interface CalculatedData { + amount: number + count: number + currency?: CurrencyEnum +} + const mapStatusConfig = ({ paymentStatus, paymentOverdue, @@ -114,6 +154,47 @@ const PortalInvoicesList = ({ translate, documentLocale }: PortalCustomerInvoice }, }) + const { data: userCurrencyData } = useGetCustomerPortalUserCurrencyQuery() + const [getOverdueBalance, { data: overdueData, loading: overdueLoading }] = + useGetCustomerPortalOverdueBalancesLazyQuery() + const [getInvoicesCollection, { data: invoicesData, loading: invoicesLoading }] = + useGetCustomerPortalInvoicesCollectionLazyQuery() + + useEffect(() => { + getOverdueBalance() + getInvoicesCollection() + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const customerCurrency = userCurrencyData?.customerPortalUser?.currency ?? CurrencyEnum.Usd + + const overdue = ( + overdueData?.customerPortalOverdueBalances?.collection || [] + ).reduce( + (acc, item) => { + return { + amount: acc.amount + deserializeAmount(item.amountCents, item.currency), + count: acc.count + item.lagoInvoiceIds.length, + currency: item.currency, + } + }, + { amount: 0, count: 0, currency: customerCurrency }, + ) + + const invoices = ( + invoicesData?.customerPortalInvoiceCollections?.collection || [] + ).reduce( + (acc, item) => { + return { + amount: acc.amount + deserializeAmount(item.amountCents, item.currency ?? customerCurrency), + count: acc.count + Number(item.invoicesCount), + currency: item.currency ?? acc.currency, + } + }, + { amount: 0, count: 0, currency: customerCurrency }, + ) + const [downloadInvoice] = useDownloadCustomerPortalInvoiceMutation({ onCompleted(localData) { const fileUrl = localData?.downloadCustomerPortalInvoice?.fileUrl @@ -145,21 +226,52 @@ const PortalInvoicesList = ({ translate, documentLocale }: PortalCustomerInvoice const hasNoInvoices = !loading && !error && !metadata?.totalCount && !hasSearchTerm return ( -
- - - {translate('text_6419c64eace749372fc72b37')} - - - {!hasNoInvoices && ( - - - - )} - + + + + + + + 0} + /> + + + + {!hasNoInvoices && ( + + )} + {hasNoInvoices ? ( {translate('text_6419c64eace749372fc72b3b')} ) : ( @@ -273,7 +385,7 @@ const PortalInvoicesList = ({ translate, documentLocale }: PortalCustomerInvoice /> )} -
+ ) } diff --git a/src/components/customerPortal/PortalOverview.tsx b/src/components/customerPortal/PortalOverview.tsx deleted file mode 100644 index 7d1af702a..000000000 --- a/src/components/customerPortal/PortalOverview.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { gql } from '@apollo/client' -import { Stack } from '@mui/material' -import { FC, useEffect } from 'react' - -import { Alert, Button, Typography } from '~/components/designSystem' -import { OverviewCard } from '~/components/OverviewCard' -import { intlFormatNumber } from '~/core/formats/intlFormatNumber' -import { deserializeAmount } from '~/core/serializers/serializeAmount' -import { LocaleEnum } from '~/core/translations' -import { - CurrencyEnum, - useGetCustomerPortalInvoicesCollectionLazyQuery, - useGetCustomerPortalOverdueBalancesLazyQuery, - useGetCustomerPortalUserCurrencyQuery, -} from '~/generated/graphql' -import { SectionHeader } from '~/styles/customer' - -gql` - query getCustomerPortalInvoicesCollection($expireCache: Boolean) { - customerPortalInvoiceCollections(expireCache: $expireCache) { - collection { - amountCents - invoicesCount - currency - } - } - } - - query getCustomerPortalOverdueBalances($expireCache: Boolean) { - customerPortalOverdueBalances(expireCache: $expireCache) { - collection { - amountCents - currency - lagoInvoiceIds - } - } - } - - query getCustomerPortalUserCurrency { - customerPortalUser { - currency - } - } -` - -interface PortalOverviewProps { - translate: Function - documentLocale: LocaleEnum -} - -interface CalculatedData { - amount: number - count: number - currency?: CurrencyEnum -} - -export const PortalOverview: FC = ({ translate, documentLocale }) => { - const { data: userCurrencyData } = useGetCustomerPortalUserCurrencyQuery() - const [getOverdueBalance, { data: overdueData, loading: overdueLoading }] = - useGetCustomerPortalOverdueBalancesLazyQuery() - const [getInvoicesCollection, { data: invoicesData, loading: invoicesLoading }] = - useGetCustomerPortalInvoicesCollectionLazyQuery() - - useEffect(() => { - getOverdueBalance() - getInvoicesCollection() - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const customerCurrency = userCurrencyData?.customerPortalUser?.currency ?? CurrencyEnum.Usd - - const overdue = ( - overdueData?.customerPortalOverdueBalances?.collection || [] - ).reduce( - (acc, item) => { - return { - amount: acc.amount + deserializeAmount(item.amountCents, item.currency), - count: acc.count + item.lagoInvoiceIds.length, - currency: item.currency, - } - }, - { amount: 0, count: 0, currency: customerCurrency }, - ) - - const invoices = ( - invoicesData?.customerPortalInvoiceCollections?.collection || [] - ).reduce( - (acc, item) => { - return { - amount: acc.amount + deserializeAmount(item.amountCents, item.currency ?? customerCurrency), - count: acc.count + Number(item.invoicesCount), - currency: item.currency ?? acc.currency, - } - }, - { amount: 0, count: 0, currency: customerCurrency }, - ) - - return ( -
- - {translate('text_6670a7222702d70114cc7954')} - - - - - {!overdueLoading && overdue.count > 0 && ( - - - - - {translate( - 'text_6670a7222702d70114cc7955', - { - count: overdue.count, - amount: intlFormatNumber(overdue.amount, { - currency: overdue.currency, - locale: documentLocale, - currencyDisplay: 'narrowSymbol', - }), - }, - overdue.count, - )} - - - {translate('text_6670a7222702d70114cc7956')} - - - - - )} - - - 0} - /> - - -
- ) -} diff --git a/src/components/customerPortal/common/SectionTitle.tsx b/src/components/customerPortal/common/SectionTitle.tsx index e5cf84803..0d3afbfed 100644 --- a/src/components/customerPortal/common/SectionTitle.tsx +++ b/src/components/customerPortal/common/SectionTitle.tsx @@ -3,14 +3,18 @@ import { tw } from '~/styles/utils' type SectionTitleProps = { className?: string title: string - children?: React.ReactNode + action?: { title: string; onClick: () => void } } -const SectionTitle = ({ className, title, children }: SectionTitleProps) => ( +const SectionTitle = ({ className, title, action }: SectionTitleProps) => (

{title}

- {children} + {action && ( + + {action.title} + + )}
) diff --git a/src/components/customerPortal/customerInformation/CustomerInformationPage.tsx b/src/components/customerPortal/customerInformation/CustomerInformationPage.tsx new file mode 100644 index 000000000..871404a79 --- /dev/null +++ b/src/components/customerPortal/customerInformation/CustomerInformationPage.tsx @@ -0,0 +1,250 @@ +import { gql } from '@apollo/client' +import { useFormik } from 'formik' +import { useState } from 'react' +import { object, string } from 'yup' + +import PageTitle from '~/components/customerPortal/common/PageTitle' +import { Button } from '~/components/designSystem' +import { Checkbox, ComboBoxField, TextInputField } from '~/components/form' +import { addToast } from '~/core/apolloClient' +import { countryDataForCombobox } from '~/core/formats/countryDataForCombobox' +import { + UpdateCustomerInput, + UpdateCustomerPortalCustomerInput, + useGetPortalCustomerInfosQuery, + useUpdatePortalCustomerMutation, +} from '~/generated/graphql' +import { useInternationalization } from '~/hooks/core/useInternationalization' + +type CustomerInformationPageProps = { + goHome: () => void +} + +type EditCustomerBillingFormProps = { + customer?: UpdateCustomerPortalCustomerInput | null +} + +gql` + mutation updatePortalCustomer($input: UpdateCustomerPortalCustomerInput!) { + updateCustomerPortalCustomer(input: $input) { + id + } + } +` + +const EditCustomerBillingForm = ({ customer }: EditCustomerBillingFormProps) => { + const { translate } = useInternationalization() + + const [isShippingEqualBillingAddress, setIsShippingEqualBillingAddress] = useState(false) + + const [ + updatePortalCustomer, + { loading: updatePortalCustomerLoading, error: updatePortalCustomerError }, + ] = useUpdatePortalCustomerMutation({ + onCompleted(res) { + if (res) { + // TODO: Refetch customer? + + addToast({ + severity: 'success', + translateKey: 'TODO: Success', + }) + } + }, + }) + + const formikProps = useFormik>({ + initialValues: { + name: customer?.name ?? '', + legalName: customer?.legalName ?? undefined, + taxIdentificationNumber: customer?.taxIdentificationNumber ?? undefined, + email: customer?.email ?? undefined, + + addressLine1: customer?.addressLine1 ?? undefined, + addressLine2: customer?.addressLine2 ?? undefined, + zipcode: customer?.zipcode ?? undefined, + city: customer?.city ?? undefined, + state: customer?.state ?? undefined, + country: customer?.country ?? undefined, + + shippingAddress: customer?.shippingAddress ?? undefined, + }, + validationSchema: object().shape({ + name: string(), + email: string().email('text_620bc4d4269a55014d493fc3'), + }), + + onSubmit: async (values) => { + updatePortalCustomer({ + variables: { + input: { + ...values, + }, + }, + }) + }, + }) + + if (!customer) { + return null + } + + return ( +
+

+ {translate('TODO: General information')} +

+ + + + + +

+ {translate('TODO:Billing address')} +

+ + + + +
+ + +
+ + + + +

+ {translate('text_667d708c1359b49f5a5a8230')} +

+ + setIsShippingEqualBillingAddress((prev) => !prev)} + /> + + + +
+ + +
+ + + + +
+
+ +
+
+
+ ) +} + +const CustomerInformationPage = ({ goHome }: CustomerInformationPageProps) => { + const { translate } = useInternationalization() + + const { data, loading } = useGetPortalCustomerInfosQuery() + + const customerPortalUser = data?.customerPortalUser + + return ( +
+ + + {loading &&
Loading..
} + + {!loading && } +
+ ) +} + +export default CustomerInformationPage diff --git a/src/components/customerPortal/wallet/WalletPage.tsx b/src/components/customerPortal/wallet/WalletPage.tsx index 0e36c2d1e..d55554d88 100644 --- a/src/components/customerPortal/wallet/WalletPage.tsx +++ b/src/components/customerPortal/wallet/WalletPage.tsx @@ -42,6 +42,8 @@ const WalletPage = ({ goHome }: WalletPageProps) => { useTopUpPortalWalletMutation({ onCompleted(res) { if (res) { + formikProps.resetForm() + addToast({ severity: 'success', translateKey: 'TODO: Success', diff --git a/src/components/customerPortal/wallet/WalletSection.tsx b/src/components/customerPortal/wallet/WalletSection.tsx index 430fa0be3..293e9db0e 100644 --- a/src/components/customerPortal/wallet/WalletSection.tsx +++ b/src/components/customerPortal/wallet/WalletSection.tsx @@ -71,18 +71,19 @@ const WalletSection = ({ viewWallet }: WalletSectionProps) => { if (customerLoadingError) { return (
- Error + + Error
) } return ( - - - {translate('TODO: Top up wallet')} - - + {customerWalletLoading && } diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 2efe34e56..702a25664 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -5974,6 +5974,13 @@ export type GetCustomerPortalUserCurrencyQueryVariables = Exact<{ [key: string]: export type GetCustomerPortalUserCurrencyQuery = { __typename?: 'Query', customerPortalUser?: { __typename?: 'CustomerPortalCustomer', currency?: CurrencyEnum | null } | null }; +export type UpdatePortalCustomerMutationVariables = Exact<{ + input: UpdateCustomerPortalCustomerInput; +}>; + + +export type UpdatePortalCustomerMutation = { __typename?: 'Mutation', updateCustomerPortalCustomer?: { __typename?: 'CustomerPortalCustomer', id: string } | null }; + export type SubscriptionForPortalUsageFragment = { __typename?: 'Subscription', id: string, currentBillingPeriodEndingAt?: any | null, plan: { __typename?: 'Plan', id: string, name: string, code: string, amountCents: any, amountCurrency: CurrencyEnum }, customer: { __typename?: 'Customer', id: string, currency?: CurrencyEnum | null, applicableTimezone: TimezoneEnum }, lifetimeUsage?: { __typename?: 'SubscriptionLifetimeUsage', lastThresholdAmountCents?: any | null, nextThresholdAmountCents?: any | null, totalUsageAmountCents: any, totalUsageFromDatetime: any, totalUsageToDatetime: any } | null }; export type GetSubscriptionForPortalQueryVariables = Exact<{ @@ -11919,6 +11926,39 @@ export type GetCustomerPortalUserCurrencyQueryHookResult = ReturnType; export type GetCustomerPortalUserCurrencySuspenseQueryHookResult = ReturnType; export type GetCustomerPortalUserCurrencyQueryResult = Apollo.QueryResult; +export const UpdatePortalCustomerDocument = gql` + mutation updatePortalCustomer($input: UpdateCustomerPortalCustomerInput!) { + updateCustomerPortalCustomer(input: $input) { + id + } +} + `; +export type UpdatePortalCustomerMutationFn = Apollo.MutationFunction; + +/** + * __useUpdatePortalCustomerMutation__ + * + * To run a mutation, you first call `useUpdatePortalCustomerMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdatePortalCustomerMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updatePortalCustomerMutation, { data, loading, error }] = useUpdatePortalCustomerMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUpdatePortalCustomerMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdatePortalCustomerDocument, options); + } +export type UpdatePortalCustomerMutationHookResult = ReturnType; +export type UpdatePortalCustomerMutationResult = Apollo.MutationResult; +export type UpdatePortalCustomerMutationOptions = Apollo.BaseMutationOptions; export const GetSubscriptionForPortalDocument = gql` query getSubscriptionForPortal($subscriptionId: ID!) { customerPortalSubscription(id: $subscriptionId) { diff --git a/src/pages/customerPortal/CustomerPortal.tsx b/src/pages/customerPortal/CustomerPortal.tsx index d2b2ce5ac..8b01478e1 100644 --- a/src/pages/customerPortal/CustomerPortal.tsx +++ b/src/pages/customerPortal/CustomerPortal.tsx @@ -3,15 +3,18 @@ TODO: Error pages (Usage, Wallet) Wallet consumed credits (take into account premium ongoing - see notion) Wallet info icon next to balance +Refresh button for invoices +Translations */ import { gql } from '@apollo/client' import { generatePath, useNavigate, useParams } from 'react-router-dom' +import styled from 'styled-components' import CustomerPortalLoading from '~/components/customerPortal/common/CustomerPortalLoading' import CustomerPortalSidebar from '~/components/customerPortal/common/CustomerPortalSidebar' -import { PortalCustomerInfos } from '~/components/customerPortal/PortalCustomerInfos' +import CustomerInformationPage from '~/components/customerPortal/customerInformation/CustomerInformationPage' +import PortalCustomerInfos from '~/components/customerPortal/PortalCustomerInfos' import PortalInvoicesList from '~/components/customerPortal/PortalInvoicesList' -import { PortalOverview } from '~/components/customerPortal/PortalOverview' import UsagePage from '~/components/customerPortal/usage/UsagePage' import UsageSection from '~/components/customerPortal/usage/UsageSection' import WalletPage from '~/components/customerPortal/wallet/WalletPage' @@ -23,6 +26,7 @@ import { } from '~/core/router/CustomerPortalRoutes' import { LocaleEnum } from '~/core/translations' import { useGetPortalOrgaInfosQuery } from '~/generated/graphql' +import Logo from '~/public/images/logo/lago-logo-grey.svg' gql` query getPortalOrgaInfos { @@ -75,18 +79,18 @@ const CustomerPortal = ({ translate, documentLocale }: CutsomerPortalProps) => { navigate(generatePath(CUSTOMER_PORTAL_ROUTE, { token: token as string })) } - const viewSubscription = (id: string) => { + const viewSubscription = (id: string) => changePage({ newPage: 'usage', itemId: id, }) - } - const viewWallet = () => { + const viewWallet = () => changePage({ newPage: 'wallet', }) - } + + const viewEditInformation = () => changePage({ newPage: 'customer-edit-information' }) return (
@@ -95,24 +99,38 @@ const CustomerPortal = ({ translate, documentLocale }: CutsomerPortalProps) => { organizationLogoUrl={data?.customerPortalOrganization?.logoUrl} /> -
+
{loading && } {!loading && !page && ( <> - - + + +
+
+ {translate('text_6419c64eace749372fc72b03')} +
+ + +
)} {!loading && page === 'usage' && } {!loading && page === 'wallet' && } + {!loading && page === 'customer-edit-information' && ( + + )}
) } export default CustomerPortal + +const StyledLogo = styled(Logo)` + width: 40px; +`