Skip to content

Commit

Permalink
feat: request payment page (#1651)
Browse files Browse the repository at this point in the history
* feat: create new route

* feat: static UI

* feat: connect BE and translations

* feat: connect with BE

* fix: update customer page queries with integration check and wordings

* fix: design qa

* fix: update validate email function

* fix(lago-289): contact us action

* fix(lago-288): handle error for invoice not overdue

* fix(qa): update things related to product reviews

* feat(permissions): update anayltics permissions

* fix(analytics): remove 12 months filter for customer overview page

* fix(lago-319): update wording

* fix: use env prod for rails codegen

* fix: revert env change
  • Loading branch information
keellyp committed Sep 12, 2024
1 parent 1dd5681 commit 5d7eb44
Show file tree
Hide file tree
Showing 22 changed files with 1,414 additions and 145 deletions.
8 changes: 6 additions & 2 deletions src/components/OverviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ export const OverviewCard: FC<OverviewCardProps> = ({
<CardHeader>
<Typography variant="captionHl">{title}</Typography>
{tooltipContent && (
<Tooltip placement="top-start" title={tooltipContent}>
<StyledTooltip placement="top-start" title={tooltipContent}>
<Icon name="info-circle" />
</Tooltip>
</StyledTooltip>
)}
</CardHeader>

Expand Down Expand Up @@ -69,3 +69,7 @@ const SkeletonWrapper = styled.div`
margin-bottom: ${theme.spacing(8)};
}
`

const StyledTooltip = styled(Tooltip)`
height: 16px;
`
190 changes: 115 additions & 75 deletions src/components/customers/overview/CustomerOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { gql } from '@apollo/client'
import { Skeleton, Stack } from '@mui/material'
import { FC, useEffect } from 'react'
import { DateTime } from 'luxon'
import { FC, useEffect, useMemo } from 'react'
import { generatePath, useNavigate, useParams } from 'react-router-dom'

import { CustomerCoupons } from '~/components/customers/overview/CustomerCoupons'
import { CustomerSubscriptionsList } from '~/components/customers/overview/CustomerSubscriptionsList'
import { Alert, Button, Typography } from '~/components/designSystem'
import { OverviewCard } from '~/components/OverviewCard'
import { intlFormatNumber } from '~/core/formats/intlFormatNumber'
import { CUSTOMER_REQUEST_OVERDUE_PAYMENT_ROUTE } from '~/core/router'
import { deserializeAmount } from '~/core/serializers/serializeAmount'
import { isSameDay } from '~/core/timezone'
import { LocaleEnum } from '~/core/translations'
import {
CurrencyEnum,
TimezoneEnum,
Expand All @@ -16,44 +21,49 @@ import {
} from '~/generated/graphql'
import { useInternationalization } from '~/hooks/core/useInternationalization'
import { useOrganizationInfos } from '~/hooks/useOrganizationInfos'
import { usePermissions } from '~/hooks/usePermissions'
import { SectionHeader } from '~/styles/customer'

gql`
query getCustomerGrossRevenues(
query getCustomerOverdueBalances(
$externalCustomerId: String!
$currency: CurrencyEnum
$expireCache: Boolean
) {
grossRevenues(
paymentRequests {
collection {
createdAt
}
}
overdueBalances(
externalCustomerId: $externalCustomerId
currency: $currency
expireCache: $expireCache
) {
collection {
amountCents
currency
invoicesCount
month
lagoInvoiceIds
}
}
}
query getCustomerOverdueBalances(
query getCustomerGrossRevenues(
$externalCustomerId: String!
$currency: CurrencyEnum
$months: Int!
$expireCache: Boolean
) {
overdueBalances(
grossRevenues(
externalCustomerId: $externalCustomerId
currency: $currency
months: $months
expireCache: $expireCache
) {
collection {
amountCents
currency
lagoInvoiceIds
invoicesCount
month
}
}
}
Expand All @@ -73,37 +83,45 @@ export const CustomerOverview: FC<CustomerOverviewProps> = ({
isLoading,
}) => {
const { translate } = useInternationalization()
const { organization } = useOrganizationInfos()
const { organization, formatTimeOrgaTZ } = useOrganizationInfos()
const { customerId } = useParams()
const navigate = useNavigate()
const { hasPermissions } = usePermissions()

const currency = userCurrency ?? organization?.defaultCurrency ?? CurrencyEnum.Usd

const [getGrossRevenues, { data: grossData, error: grossError, loading: grossLoading }] =
useGetCustomerGrossRevenuesLazyQuery({
variables: {
externalCustomerId: externalCustomerId || '',
currency,
},
})
const [getOverdueBalances, { data: overdueData, error: overdueError, loading: overdueLoading }] =
useGetCustomerOverdueBalancesLazyQuery({
variables: {
externalCustomerId: externalCustomerId || '',
currency,
months: 12,
},
})
const [
getCustomerOverdueBalances,
{ data: overdueBalancesData, loading: overdueBalancesLoading, error: overdueBalancesError },
] = useGetCustomerOverdueBalancesLazyQuery({
variables: {
externalCustomerId: externalCustomerId || '',
currency,
},
})
const [
getCustomerGrossRevenues,
{ data: grossRevenuesData, loading: grossRevenuesLoading, error: grossRevenuesError },
] = useGetCustomerGrossRevenuesLazyQuery({
variables: {
externalCustomerId: externalCustomerId || '',
currency,
},
})

useEffect(() => {
if (!externalCustomerId) return

getGrossRevenues()
getOverdueBalances()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [externalCustomerId])
if (hasPermissions(['analyticsOverdueBalancesView'])) {
getCustomerOverdueBalances()
}

const hasAnyError = grossError || overdueError
if (hasPermissions(['analyticsView'])) {
getCustomerGrossRevenues()
}
}, [externalCustomerId])

const grossRevenues = (grossData?.grossRevenues.collection || []).reduce(
const grossRevenues = (grossRevenuesData?.grossRevenues.collection || []).reduce(
(acc, revenue) => {
return {
amountCents: acc.amountCents + deserializeAmount(revenue.amountCents, currency),
Expand All @@ -113,7 +131,7 @@ export const CustomerOverview: FC<CustomerOverviewProps> = ({
{ amountCents: 0, invoicesCount: 0 },
)

const overdueBalances = overdueData?.overdueBalances.collection || []
const overdueBalances = overdueBalancesData?.overdueBalances.collection || []
const overdueFormattedData = overdueBalances.reduce<{
amountCents: number
invoiceCount: number
Expand All @@ -131,9 +149,16 @@ export const CustomerOverview: FC<CustomerOverviewProps> = ({
)
const hasOverdueInvoices = overdueFormattedData.invoiceCount > 0

const today = useMemo(() => DateTime.now().toUTC(), [])
const lastPaymentRequestDate = useMemo(
() => DateTime.fromISO(overdueBalancesData?.paymentRequests.collection[0]?.createdAt).toUTC(),
[overdueBalancesData?.paymentRequests],
)
const hasMadePaymentRequestToday = isSameDay(lastPaymentRequestDate, today)

return (
<>
{!hasAnyError && (
{(!overdueBalancesError || !grossRevenuesError) && (
<section>
<SectionHeader variant="subhead" $hideBottomShadow>
{translate('text_6670a7222702d70114cc7954')}
Expand All @@ -142,62 +167,77 @@ export const CustomerOverview: FC<CustomerOverviewProps> = ({
data-test="refresh-overview"
variant="quaternary"
onClick={() => {
getGrossRevenues({
getCustomerOverdueBalances({
variables: {
expireCache: true,
externalCustomerId: externalCustomerId || '',
currency,
},
})
getOverdueBalances({
variables: {
expireCache: true,
externalCustomerId: externalCustomerId || '',
currency,
months: 12,
},
})
}}
>
{translate('text_6670a7222702d70114cc7953')}
</Button>
</SectionHeader>
<Stack gap={4}>
{hasOverdueInvoices && !overdueError && (
<Alert type="warning">
<Stack flexDirection="row" gap={4} alignItems="center">
{overdueLoading ? (
<Stack flexDirection="column" gap={1}>
<Skeleton variant="text" width={150} />
<Skeleton variant="text" width={80} />
</Stack>
) : (
<Stack flexDirection="column" gap={1}>
<Typography variant="bodyHl" color="textSecondary">
{translate(
'text_6670a7222702d70114cc7955',
{
count: overdueFormattedData.invoiceCount,
amount: intlFormatNumber(overdueFormattedData.amountCents, {
currencyDisplay: 'symbol',
currency,
{hasOverdueInvoices && !overdueBalancesError && (
<Alert
type="warning"
ButtonProps={
!overdueBalancesLoading
? {
label: translate('text_66b258f62100490d0eb5caa2'),
onClick: () =>
navigate(
generatePath(CUSTOMER_REQUEST_OVERDUE_PAYMENT_ROUTE, {
customerId: customerId ?? '',
}),
},
overdueFormattedData.invoiceCount,
)}
</Typography>
<Typography variant="caption">
{translate('text_6670a2a7ae3562006c4ee3db')}
</Typography>
</Stack>
)}
</Stack>
),
}
: undefined
}
>
{overdueBalancesLoading ? (
<Stack flexDirection="column" gap={1}>
<Skeleton variant="text" width={150} />
<Skeleton variant="text" width={80} />
</Stack>
) : (
<Stack flexDirection="column" gap={1}>
<Typography variant="bodyHl" color="textSecondary">
{translate(
'text_6670a7222702d70114cc7955',
{
count: overdueFormattedData.invoiceCount,
amount: intlFormatNumber(overdueFormattedData.amountCents, {
currencyDisplay: 'symbol',
currency,
}),
},
overdueFormattedData.invoiceCount,
)}
</Typography>
<Typography variant="caption">
{hasMadePaymentRequestToday
? translate('text_66b4f00bd67ccc185ea75c70', {
relativeDay: lastPaymentRequestDate.toRelativeCalendar({
locale: LocaleEnum.en,
}),
time: formatTimeOrgaTZ(
overdueBalancesData?.paymentRequests.collection[0]?.createdAt,
'HH:mm:ss',
),
})
: translate('text_6670a2a7ae3562006c4ee3db')}
</Typography>
</Stack>
)}
</Alert>
)}
<Stack flexDirection="row" gap={4}>
{!grossError && (
{hasPermissions(['analyticsView']) && !grossRevenuesError && (
<OverviewCard
isLoading={grossLoading}
isLoading={grossRevenuesLoading}
title={translate('text_6553885df387fd0097fd7385')}
tooltipContent={translate('text_65564e8e4af2340050d431bf')}
content={intlFormatNumber(grossRevenues.amountCents, {
Expand All @@ -211,9 +251,9 @@ export const CustomerOverview: FC<CustomerOverviewProps> = ({
)}
/>
)}
{!overdueError && (
{hasPermissions(['analyticsOverdueBalancesView']) && !overdueBalancesError && (
<OverviewCard
isLoading={overdueLoading}
isLoading={overdueBalancesLoading}
title={translate('text_6670a7222702d70114cc795a')}
tooltipContent={translate('text_6670a2a7ae3562006c4ee3e7')}
content={intlFormatNumber(overdueFormattedData.amountCents, {
Expand Down
6 changes: 3 additions & 3 deletions src/components/graphs/Gross.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { AreaGrossRevenuesChartFakeData } from '../designSystem/graphs/fixtures'
import { GenericPlaceholder } from '../GenericPlaceholder'

gql`
query getGrossRevenues($currency: CurrencyEnum!, $externalCustomerId: String) {
grossRevenues(currency: $currency, externalCustomerId: $externalCustomerId) {
query getGrossRevenues($currency: CurrencyEnum!, $externalCustomerId: String, $months: Int) {
grossRevenues(currency: $currency, externalCustomerId: $externalCustomerId, months: $months) {
collection {
amountCents
currency
Expand Down Expand Up @@ -73,7 +73,7 @@ const Gross = ({
}: TGraphProps & { externalCustomerId?: string }) => {
const { translate } = useInternationalization()
const { data, loading, error } = useGetGrossRevenuesQuery({
variables: { currency: currency, externalCustomerId: externalCustomerId },
variables: { currency: currency, externalCustomerId: externalCustomerId, months: 12 },
skip: demoMode || blur || !currency,
})

Expand Down
1 change: 1 addition & 0 deletions src/core/apolloClient/graphqlResolvers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const typeDefs = gql`
does_not_match_item_amounts
payment_processor_is_currently_handling_payment
plan_overlapping
invoices_not_overdue
# Object not found
plan_not_found
Expand Down
14 changes: 14 additions & 0 deletions src/core/router/CustomerRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ const CustomerInvoiceDetails = lazyLoad(
import(/* webpackChunkName: 'customer-invoice-details' */ '~/layouts/CustomerInvoiceDetails'),
)

const CustomerRequestOverduePayment = lazyLoad(
() =>
import(
/* webpackChunkName: 'customer-request-overdue-payment' */ '~/pages/CustomerRequestOverduePayment/index'
),
)

// Credit note related
const CreateCreditNote = lazyLoad(
() => import(/* webpackChunkName: 'create-credit-note' */ '~/pages/CreateCreditNote'),
Expand All @@ -33,6 +40,7 @@ export const CUSTOMER_DETAILS_ROUTE = '/customer/:customerId'
export const CUSTOMER_DETAILS_TAB_ROUTE = `${CUSTOMER_DETAILS_ROUTE}/:tab`
export const CUSTOMER_DRAFT_INVOICES_LIST_ROUTE = `${CUSTOMER_DETAILS_ROUTE}/draft-invoices`
export const CUSTOMER_INVOICE_DETAILS_ROUTE = `${CUSTOMER_DETAILS_ROUTE}/invoice/:invoiceId/:tab`
export const CUSTOMER_REQUEST_OVERDUE_PAYMENT_ROUTE = `${CUSTOMER_DETAILS_ROUTE}/request-overdue-payment`

// Credit note related
export const CUSTOMER_INVOICE_CREDIT_NOTE_DETAILS_ROUTE = `${CUSTOMER_DETAILS_ROUTE}/invoice/:invoiceId/credit-notes/:creditNoteId`
Expand Down Expand Up @@ -79,4 +87,10 @@ export const customerObjectCreationRoutes: CustomRouteObject[] = [
element: <CreateCreditNote />,
permissions: ['creditNotesCreate'],
},
{
path: CUSTOMER_REQUEST_OVERDUE_PAYMENT_ROUTE,
private: true,
element: <CustomerRequestOverduePayment />,
permissions: ['analyticsOverdueBalancesView'],
},
]
4 changes: 4 additions & 0 deletions src/core/timezone/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ export const formatDateToTZ = (
zone: getTimezoneConfig(timezone).name,
}).toFormat(format || 'LLL. dd, yyyy')
}

export const isSameDay = (a: DateTime, b: DateTime): boolean => {
return a.hasSame(b, 'day') && a.hasSame(b, 'month') && a.hasSame(b, 'year')
}
Loading

0 comments on commit 5d7eb44

Please sign in to comment.