From c77bc2d8bb53a22a9cb6b79b9dcac74107284f24 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:04:01 +0200 Subject: [PATCH 01/43] feat(icons): add arrow-indent icon --- src/public/icons/arrow-indent.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/public/icons/arrow-indent.svg diff --git a/src/public/icons/arrow-indent.svg b/src/public/icons/arrow-indent.svg new file mode 100644 index 000000000..338b3996e --- /dev/null +++ b/src/public/icons/arrow-indent.svg @@ -0,0 +1,8 @@ + + + From 3aa3685c3764dd43b71dda20438285b00db314ae Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:04:16 +0200 Subject: [PATCH 02/43] chore(translations): add translations --- translations/base.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/translations/base.json b/translations/base.json index 1b06f5668..80cfa8c59 100644 --- a/translations/base.json +++ b/translations/base.json @@ -1871,7 +1871,7 @@ "text_624efab67eb2570101d117c6": "Type a name", "text_624efab67eb2570101d117ce": "Customer external ID", "text_624efab67eb2570101d117d6": "Type an external ID", - "text_6250304370f0f700a8fdc27d": "Details", + "text_6250304370f0f700a8fdc27d": "Customer details", "text_6250304370f0f700a8fdc283": "External ID", "text_6250304370f0f700a8fdc28b": "Assign a plan", "text_6250304370f0f700a8fdc28d": "Subscriptions", @@ -2792,5 +2792,11 @@ "text_1733303404277q80b216p5zr": "This integration is not officially developed by Lago. Please note that the Lago team will not be able to provide support if any issues arise.", "text_1733303818769298k0fvsgcz": "Type a redirect URL", "text_17367626793434wkg1rk0114": "Cashfree", - "text_1736764955395763x9k5gqkj": "These integrations are not officially developed by Lago, so please be aware that our team cannot provide support for any issues that may arise." + "text_1736764955395763x9k5gqkj": "These integrations are not officially developed by Lago, so please be aware that our team cannot provide support for any issues that may arise.", + "text_1736950586920yq3xq4gols8": "A coupon is a discount that reduces the amount on future invoices.", + "text_1736968199827r2u2gd7pypg": "A subscription is created when a plan is assigned to a partner. You can assign a plan to a customer at any time", + "text_1736968618645gg26amx8djq": "Frequency", + "text_1736972452609qdjngeuqsz0": "Downgrade", + "text_1736972452609g2v8mzgvi2t": "Scheduled", + "text_1737059551511f5acxkfz7p4": "Retrieve all customer details, including connected external apps." } From fdbec2c965602d3a31fa02b4a4edeccbb7a2e7a8 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:04:36 +0200 Subject: [PATCH 03/43] chore(graphql): generate types --- src/generated/graphql.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index ffad3abac..ec7440b4b 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6934,7 +6934,7 @@ export type GetCustomerSubscriptionForListQueryVariables = Exact<{ }>; -export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, code: string }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }> } | null }; +export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval, code: string }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }> } | null }; export type SubscriptionItemFragment = { __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, name: string, code: string }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }; @@ -15065,6 +15065,8 @@ export const GetCustomerSubscriptionForListDocument = gql` plan { id amountCurrency + name + interval } ...SubscriptionItem } From 76a8ff2e3247e5c36500ac257fd9ce967918405c Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:05:02 +0200 Subject: [PATCH 04/43] feat(TimezoneDate): add className prop --- src/components/TimezoneDate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TimezoneDate.tsx b/src/components/TimezoneDate.tsx index 012c7f666..085f9149c 100644 --- a/src/components/TimezoneDate.tsx +++ b/src/components/TimezoneDate.tsx @@ -15,7 +15,7 @@ interface TimezoneDateProps { mainDateFormat?: string mainTimezone?: keyof typeof MainTimezoneEnum customerTimezone?: TimezoneEnum - mainTypographyProps?: Pick + mainTypographyProps?: Pick className?: string } From 317ff88799461e97d6f1f960e04bb108ff2b0747 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:05:32 +0200 Subject: [PATCH 05/43] feat(Status): add support for downgrade and scheduled statuses --- src/components/designSystem/Status.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/designSystem/Status.tsx b/src/components/designSystem/Status.tsx index 367434a66..21a15459b 100644 --- a/src/components/designSystem/Status.tsx +++ b/src/components/designSystem/Status.tsx @@ -14,12 +14,14 @@ export enum StatusType { default = 'default', danger = 'danger', disabled = 'disabled', + downgrade = 'downgrade', + scheduled = 'scheduled', } type StatusLabelSuccess = 'succeeded' | 'finalized' | 'active' | 'pay' | 'available' | 'refunded' type StatusLabelWarning = 'failed' type StatusLabelOutline = 'draft' -type StatusLabelDefault = 'pending' | 'toPay' | 'n/a' +type StatusLabelDefault = 'downgrade' | 'scheduled' | 'pending' | 'toPay' | 'n/a' type StatusLabelDanger = | 'disputed' | 'disputeLost' @@ -55,6 +57,8 @@ const statusLabelMapping: Record = { consumed: 'text_6376641a2a9c70fff5bddcd1', voided: 'text_6376641a2a9c70fff5bddcd5', overdue: 'text_666c5b12fea4aa1e1b26bf55', + downgrade: 'text_1736972452609qdjngeuqsz0', + scheduled: 'text_1736972452609g2v8mzgvi2t', ['n/a']: '-', // These keys below are displayed in the customer portal // Hence they must be translated in all available languages From b4ce221d8f32219c44c682767e817e9381aac74b Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:08:18 +0200 Subject: [PATCH 06/43] feat(icons): add arrow-indent icon --- src/components/designSystem/Icon/mapping.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/designSystem/Icon/mapping.tsx b/src/components/designSystem/Icon/mapping.tsx index ae5b00ea4..59d308a19 100644 --- a/src/components/designSystem/Icon/mapping.tsx +++ b/src/components/designSystem/Icon/mapping.tsx @@ -1,6 +1,7 @@ import Alphabet from '~/public/icons/alphabet.svg' import Apps from '~/public/icons/apps.svg' import ArrowBottom from '~/public/icons/arrow-bottom.svg' +import ArrowIndent from '~/public/icons/arrow-indent.svg' import ArrowLeftRight from '~/public/icons/arrow-left-right.svg' import ArrowLeft from '~/public/icons/arrow-left.svg' import ArrowRight from '~/public/icons/arrow-right.svg' @@ -128,6 +129,7 @@ export const ALL_ICONS = { 'arrow-left-right': ArrowLeftRight, 'arrow-right': ArrowRight, 'arrow-top': ArrowTop, + 'arrow-indent': ArrowIndent, ascending: Ascending, 'attachment-na': AttachmentNa, bank: Bank, From 268452e184ef85fa48ff82a4fd646504dab9e8e4 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:09:08 +0200 Subject: [PATCH 07/43] feat(layoutsSection): create reusable page title component --- src/components/layouts/Section.tsx | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/components/layouts/Section.tsx diff --git a/src/components/layouts/Section.tsx b/src/components/layouts/Section.tsx new file mode 100644 index 000000000..fd1e85471 --- /dev/null +++ b/src/components/layouts/Section.tsx @@ -0,0 +1,48 @@ +import { Skeleton, Typography } from '@mui/material' + +import { Button } from '~/components/designSystem' +import { tw } from '~/styles/utils' + +export const PageSectionTitle = ({ + className, + title, + subtitle, + action, + loading, +}: { + className?: string + title: string + subtitle?: string + action?: { title: string; onClick: () => void; dataTest?: string } + loading?: boolean +}) => { + return ( +
+ {loading && ( +
+ +
+ )} + + {!loading && ( + <> +
+ + {title} + + + {subtitle && ( + {subtitle} + )} +
+ + {action && ( + + )} + + )} +
+ ) +} From 226f2d18b879012f8bf93eaafa255d61465960a7 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:12:47 +0200 Subject: [PATCH 08/43] feat(CouponCaption): add className prop --- src/components/coupons/CouponCaption.tsx | 187 +++++++++++++---------- 1 file changed, 103 insertions(+), 84 deletions(-) diff --git a/src/components/coupons/CouponCaption.tsx b/src/components/coupons/CouponCaption.tsx index 4df925b3a..97d80a6ee 100644 --- a/src/components/coupons/CouponCaption.tsx +++ b/src/components/coupons/CouponCaption.tsx @@ -44,104 +44,123 @@ export interface CouponMixedType extends CouponItemFragment { interface CouponCaptionProps { coupon: CouponMixedType variant?: TypographyProps['variant'] + className?: string } -export const CouponCaption = memo(({ coupon, variant = 'caption' }: CouponCaptionProps) => { - const { translate } = useInternationalization() +export const CouponCaption = memo( + ({ coupon, variant = 'caption', className }: CouponCaptionProps) => { + const { translate } = useInternationalization() - const getCaption = () => { - const { - amountCurrency, - amountCents, - amountCentsRemaining, - percentageRate, - frequency, - frequencyDuration, - frequencyDurationRemaining, - } = coupon - const couponType = amountCents ? CouponTypeEnum.FixedAmount : CouponTypeEnum.Percentage + const getCaption = () => { + const { + amountCurrency, + amountCents, + amountCentsRemaining, + percentageRate, + frequency, + frequencyDuration, + frequencyDurationRemaining, + } = coupon + const couponType = amountCents ? CouponTypeEnum.FixedAmount : CouponTypeEnum.Percentage - if (couponType === CouponTypeEnum.FixedAmount && frequency === CouponFrequency.Once) { - return translate( - amountCentsRemaining ? 'text_637b4da08cd0118cd0c4486f' : 'text_632d68358f1fedc68eed3e70', - { - amount: intlFormatNumber( - deserializeAmount( - Number(amountCentsRemaining) || Number(amountCents), - amountCurrency || CurrencyEnum.Usd, - ) || 0, - { - currencyDisplay: 'symbol', - currency: amountCurrency || undefined, - }, - ), - }, - ) - } else if (couponType === CouponTypeEnum.Percentage && frequency === CouponFrequency.Once) { - return translate('text_632d68358f1fedc68eed3eb5', { - rate: intlFormatNumber(Number(percentageRate) / 100 || 0, { - style: 'percent', - }), - }) - } else if ( - couponType === CouponTypeEnum.FixedAmount && - frequency === CouponFrequency.Recurring - ) { - return translate( - 'text_632d68358f1fedc68eed3ede', - { + if (couponType === CouponTypeEnum.FixedAmount && frequency === CouponFrequency.Once) { + return translate( + amountCentsRemaining ? 'text_637b4da08cd0118cd0c4486f' : 'text_632d68358f1fedc68eed3e70', + { + amount: intlFormatNumber( + deserializeAmount( + Number(amountCentsRemaining) || Number(amountCents), + amountCurrency || CurrencyEnum.Usd, + ) || 0, + { + currencyDisplay: 'symbol', + currency: amountCurrency || undefined, + }, + ), + }, + ) + } else if (couponType === CouponTypeEnum.Percentage && frequency === CouponFrequency.Once) { + return translate('text_632d68358f1fedc68eed3eb5', { + rate: intlFormatNumber(Number(percentageRate) / 100 || 0, { + style: 'percent', + }), + }) + } else if ( + couponType === CouponTypeEnum.FixedAmount && + frequency === CouponFrequency.Recurring + ) { + return translate( + 'text_632d68358f1fedc68eed3ede', + { + amount: intlFormatNumber( + deserializeAmount( + Number(amountCentsRemaining) || Number(amountCents), + amountCurrency || CurrencyEnum.Usd, + ) || 0, + { + currencyDisplay: 'symbol', + currency: amountCurrency || undefined, + }, + ), + duration: frequencyDurationRemaining || frequencyDuration, + }, + frequencyDurationRemaining || frequencyDuration || 1, + ) + } else if ( + couponType === CouponTypeEnum.Percentage && + frequency === CouponFrequency.Recurring + ) { + return translate( + 'text_632d68358f1fedc68eed3ef9', + { + rate: intlFormatNumber(Number(percentageRate) / 100 || 0, { + style: 'percent', + }), + duration: frequencyDurationRemaining || frequencyDuration, + }, + frequencyDurationRemaining || frequencyDuration || 1, + ) + } else if ( + couponType === CouponTypeEnum.FixedAmount && + frequency === CouponFrequency.Forever + ) { + return translate('text_63c946e8bef768ead2fee35c', { amount: intlFormatNumber( - deserializeAmount( - Number(amountCentsRemaining) || Number(amountCents), - amountCurrency || CurrencyEnum.Usd, - ) || 0, + deserializeAmount(Number(amountCents), amountCurrency || CurrencyEnum.Usd) || 0, { currencyDisplay: 'symbol', currency: amountCurrency || undefined, }, ), - duration: frequencyDurationRemaining || frequencyDuration, - }, - frequencyDurationRemaining || frequencyDuration || 1, - ) - } else if ( - couponType === CouponTypeEnum.Percentage && - frequency === CouponFrequency.Recurring - ) { - return translate( - 'text_632d68358f1fedc68eed3ef9', - { + }) + } else if ( + couponType === CouponTypeEnum.Percentage && + frequency === CouponFrequency.Forever + ) { + return translate('text_63c96b18bfbf40e9ef600e99', { rate: intlFormatNumber(Number(percentageRate) / 100 || 0, { style: 'percent', }), - duration: frequencyDurationRemaining || frequencyDuration, - }, - frequencyDurationRemaining || frequencyDuration || 1, - ) - } else if (couponType === CouponTypeEnum.FixedAmount && frequency === CouponFrequency.Forever) { - return translate('text_63c946e8bef768ead2fee35c', { - amount: intlFormatNumber( - deserializeAmount(Number(amountCents), amountCurrency || CurrencyEnum.Usd) || 0, - { - currencyDisplay: 'symbol', - currency: amountCurrency || undefined, - }, - ), - }) - } else if (couponType === CouponTypeEnum.Percentage && frequency === CouponFrequency.Forever) { - return translate('text_63c96b18bfbf40e9ef600e99', { - rate: intlFormatNumber(Number(percentageRate) / 100 || 0, { - style: 'percent', - }), - }) + }) + } } - } - return ( - - {getCaption()} - - ) -}) + return ( + <> + {!className && ( + + {getCaption()} + + )} + + {className && ( + + {getCaption()} + + )} + + ) + }, +) CouponCaption.displayName = 'CouponCaption' From 95d921a7da005e207e8524ff09ee1427b6fed294 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:14:26 +0200 Subject: [PATCH 09/43] refactor(CustomerPage): unify section heights --- src/components/customers/CustomerCreditNotesList.tsx | 2 +- src/components/customers/CustomerInvoicesTab.tsx | 2 +- src/components/customers/CustomerSettings.tsx | 2 +- src/components/customers/usage/CustomerUsage.tsx | 2 +- src/components/wallets/CustomerWalletList.tsx | 5 +---- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/customers/CustomerCreditNotesList.tsx b/src/components/customers/CustomerCreditNotesList.tsx index 49bf461de..ca1c1956d 100644 --- a/src/components/customers/CustomerCreditNotesList.tsx +++ b/src/components/customers/CustomerCreditNotesList.tsx @@ -53,7 +53,7 @@ export const CustomerCreditNotesList = ({ return (
- + {translate('text_63725b30957fd5b26b308dd7')}
diff --git a/src/components/customers/CustomerInvoicesTab.tsx b/src/components/customers/CustomerInvoicesTab.tsx index e7da4d3b7..638770e25 100644 --- a/src/components/customers/CustomerInvoicesTab.tsx +++ b/src/components/customers/CustomerInvoicesTab.tsx @@ -107,7 +107,7 @@ export const CustomerInvoicesTab = ({ customerId, customerTimezone }: CustomerIn <> {!!invoicesDraft?.length && (
-
+
{translate('text_638f4d756d899445f18a49ee')} diff --git a/src/components/customers/CustomerSettings.tsx b/src/components/customers/CustomerSettings.tsx index aab06ef75..498ddb5b8 100644 --- a/src/components/customers/CustomerSettings.tsx +++ b/src/components/customers/CustomerSettings.tsx @@ -249,7 +249,7 @@ export const CustomerSettings = ({ customerId }: CustomerSettingsProps) => { return ( <> - + {!!loading ? ( diff --git a/src/components/customers/usage/CustomerUsage.tsx b/src/components/customers/usage/CustomerUsage.tsx index 302894c3a..29a669716 100644 --- a/src/components/customers/usage/CustomerUsage.tsx +++ b/src/components/customers/usage/CustomerUsage.tsx @@ -41,7 +41,7 @@ export const CustomerUsage = ({ premiumWarningDialogRef }: CustomerUsageProps) = return (
- + {translate('text_65564e8e4af2340050d431be')}
From f8edf354708b2003e3b35869b128423b780f7cc8 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:16:50 +0200 Subject: [PATCH 10/43] refactor(CustomerCoupons): list -> table --- .../customers/overview/CustomerCoupons.tsx | 124 ++++++++---------- 1 file changed, 53 insertions(+), 71 deletions(-) diff --git a/src/components/customers/overview/CustomerCoupons.tsx b/src/components/customers/overview/CustomerCoupons.tsx index e28ac5ffc..710852118 100644 --- a/src/components/customers/overview/CustomerCoupons.tsx +++ b/src/components/customers/overview/CustomerCoupons.tsx @@ -1,10 +1,10 @@ import { gql } from '@apollo/client' import { memo, useRef } from 'react' import { useParams } from 'react-router-dom' -import styled from 'styled-components' import { CouponCaption, CouponMixedType } from '~/components/coupons/CouponCaption' -import { Avatar, Button, Icon, Tooltip, Typography } from '~/components/designSystem' +import { Button, Icon, Table, Tooltip, Typography } from '~/components/designSystem' +import { PageSectionTitle } from '~/components/layouts/Section' import { WarningDialog, WarningDialogRef } from '~/components/WarningDialog' import { addToast } from '~/core/apolloClient' import { @@ -14,8 +14,6 @@ import { } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' import { usePermissions } from '~/hooks/usePermissions' -import { HEADER_TABLE_HEIGHT, NAV_HEIGHT, theme } from '~/styles' -import { SectionHeader } from '~/styles/customer' import { AddCouponToCustomerDialog, @@ -64,7 +62,7 @@ export const CustomerCoupons = memo(() => { const addCouponDialogRef = useRef(null) const deleteCouponId = useRef(null) const { translate } = useInternationalization() - const { data } = useGetCustomerCouponsQuery({ + const { data, loading } = useGetCustomerCouponsQuery({ variables: { id: customerId as string }, skip: !customerId, }) @@ -84,39 +82,51 @@ export const CustomerCoupons = memo(() => { return ( <> {!!(coupons || [])?.length && ( - - - {translate('text_628b8c693e464200e00e469d')} - - - - - {translate('text_628b8c693e464200e00e46ab')} - - - {(coupons || []).map((appliedCoupon) => ( - - - - - - - {appliedCoupon.coupon?.name} - - - - {hasPermissions(['couponsDetach']) && ( + }, + }} + /> + + ( +
+ + + + {name} + +
+ ), + }, + { + key: 'amountCurrency', + maxSpace: true, + title: translate('text_632d68358f1fedc68eed3e9d'), + content: (coupon) => ( + + ), + }, + ]} + actionColumn={(coupon) => + hasPermissions(['couponsDetach']) && ( { variant="quaternary" icon="trash" onClick={() => { - deleteCouponId.current = appliedCoupon.id + deleteCouponId.current = coupon.id removeDialogRef?.current?.openDialog() }} /> - )} - - ))} - + ) + } + /> + )} + { ) }) -const Container = styled.div` - display: flex; - flex-direction: column; -` - -const ListHeader = styled.div` - height: ${HEADER_TABLE_HEIGHT}px; - display: flex; - align-items: center; - box-shadow: ${theme.shadows[7]}; - > *:not(:last-child) { - margin-right: ${theme.spacing(6)}; - } -` - -const CouponNameSection = styled.div` - margin-right: auto; - display: flex; - align-items: center; - min-width: 0; - height: ${NAV_HEIGHT}px; - box-shadow: ${theme.shadows[7]}; - width: 100%; -` - -const NameBlock = styled.div` - min-width: 0; -` - CustomerCoupons.displayName = 'CustomerCoupons' From 201b02a080f72c38870d04bfee66947cf2d52997 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:17:54 +0200 Subject: [PATCH 11/43] refactor(CustomerSubscriptionsList): list -> table --- .../overview/CustomerSubscriptionsList.tsx | 377 ++++++++++++++---- .../subscriptions/SubscriptionItem.tsx | 149 ------- 2 files changed, 297 insertions(+), 229 deletions(-) delete mode 100644 src/components/customers/subscriptions/SubscriptionItem.tsx diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index 0955b68e8..3a5703c84 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -1,21 +1,39 @@ import { gql } from '@apollo/client' import { useRef } from 'react' -import { generatePath, useNavigate, useParams } from 'react-router-dom' -import styled from 'styled-components' +import { generatePath, NavigateFunction, useNavigate, useParams } from 'react-router-dom' -import { Button, Typography } from '~/components/designSystem' -import { CREATE_SUBSCRIPTION } from '~/core/router' import { + ActionItem, + Icon, + Status, + StatusProps, + StatusType, + Table, + Typography, +} from '~/components/designSystem' +import { PageSectionTitle } from '~/components/layouts/Section' +import { TimezoneDate } from '~/components/TimezoneDate' +import { addToast } from '~/core/apolloClient' +import { getIntervalTranslationKey } from '~/core/constants/form' +import { + CREATE_SUBSCRIPTION, + CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, + UPDATE_SUBSCRIPTION, + UPGRADE_DOWNGRADE_SUBSCRIPTION, +} from '~/core/router' +import { copyToClipboard } from '~/core/utils/copyToClipboard' +import { + Plan, + StatusTypeEnum, + Subscription, SubscriptionItemFragmentDoc, TimezoneEnum, useGetCustomerSubscriptionForListQuery, } from '~/generated/graphql' -import { useInternationalization } from '~/hooks/core/useInternationalization' +import { TranslateFunc, useInternationalization } from '~/hooks/core/useInternationalization' import { usePermissions } from '~/hooks/usePermissions' -import { HEADER_TABLE_HEIGHT, theme } from '~/styles' -import { SectionHeader } from '~/styles/customer' +import { CustomerSubscriptionDetailsTabsOptionsEnum } from '~/pages/SubscriptionDetails' -import { SubscriptionItem, SubscriptionItemSkeleton } from '../subscriptions/SubscriptionItem' import { TerminateCustomerSubscriptionDialog, TerminateCustomerSubscriptionDialogRef, @@ -30,6 +48,8 @@ gql` plan { id amountCurrency + name + interval } ...SubscriptionItem } @@ -43,6 +63,162 @@ interface CustomerSubscriptionsListProps { customerTimezone?: TimezoneEnum } +type AnnotatedSubscription = { + id: string + externalId?: Subscription['externalId'] + name: Subscription['name'] + startedAt: Subscription['startedAt'] + endingAt?: Subscription['endingAt'] + status?: Subscription['status'] + frequency: Plan['interval'] + statusType: { + type: StatusType + label: string + } + isDowngrade?: boolean + customerId: string +} + +const annotateSubscriptions = ( + subscriptions: Subscription[] | null | undefined, +): AnnotatedSubscription[] => { + return (subscriptions || []).reduce((subsAcc, subscription) => { + const { + id, + plan, + status, + nextPlan, + nextPendingStartDate, + externalId, + nextName, + name, + startedAt, + subscriptionAt, + endingAt, + customer, + } = subscription || {} + + const isDowngrading = !!nextPlan + + const _sub = { + id, + externalId, + name: name || plan.name, + status, + startedAt: startedAt || subscriptionAt, + endingAt: endingAt, + frequency: plan.interval, + startDate: startedAt || subscriptionAt, + statusType: { + ...(status === StatusTypeEnum.Pending + ? { + type: StatusType.default, + label: 'pending', + } + : { + type: StatusType.success, + label: 'active', + }), + }, + customerId: customer?.id, + } + + const _subDowngrade = isDowngrading && + nextPlan && { + id: nextPlan.id, + name: nextName || nextPlan.name, + frequency: nextPlan.interval, + startedAt: nextPendingStartDate, + statusType: { + type: StatusType.default, + label: 'pending', + }, + isDowngrade: true, + customerId: customer?.id, + } + + return [...subsAcc, _sub, ...(_subDowngrade ? [_subDowngrade] : [])] + }, []) +} + +const generateActionColumn = ({ + subscription, + hasSubscriptionsUpdatePermission, + customerId, + terminateSubscriptionDialogRef, + translate, + navigate, +}: { + subscription: AnnotatedSubscription + hasSubscriptionsUpdatePermission: boolean + customerId?: string + terminateSubscriptionDialogRef: React.RefObject + translate: TranslateFunc + navigate: NavigateFunction +}) => { + let actions: ActionItem[] = [] + + if (!subscription.isDowngrade && hasSubscriptionsUpdatePermission) { + actions = actions.concat([ + { + startIcon: 'text', + title: translate('text_62d7f6178ec94cd09370e63c'), + onAction: () => + navigate( + generatePath(UPDATE_SUBSCRIPTION, { + customerId: customerId as string, + subscriptionId: subscription.id, + }), + ), + }, + { + startIcon: 'pen', + title: translate('text_62d7f6178ec94cd09370e64a'), + onAction: () => + navigate( + generatePath(UPGRADE_DOWNGRADE_SUBSCRIPTION, { + customerId: customerId as string, + subscriptionId: subscription.id, + }), + ), + }, + ]) + } + + actions = actions.concat({ + startIcon: 'duplicate', + title: translate('text_62d7f6178ec94cd09370e65b'), + onAction: () => { + if (!subscription.externalId) return + + copyToClipboard(subscription.externalId) + + addToast({ + severity: 'info', + translateKey: 'text_62d94cc9ccc5eebcc03160a0', + }) + }, + }) + + if (hasSubscriptionsUpdatePermission) { + actions = actions.concat({ + startIcon: 'trash', + title: + subscription.status === StatusTypeEnum.Pending + ? translate('text_64a6d736c23125004817627f') + : translate('text_62d904b97e690a881f2b867c'), + onAction: () => + terminateSubscriptionDialogRef?.current?.openDialog({ + id: subscription.id, + name: subscription.name, + status: status as StatusTypeEnum, + }), + }) + } + + return actions +} + export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscriptionsListProps) => { const { customerId } = useParams() const navigate = useNavigate() @@ -52,90 +228,131 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip variables: { id: customerId as string }, skip: !customerId, }) - const subscriptions = data?.customer?.subscriptions + const subscriptions = data?.customer?.subscriptions as Subscription[] const hasNoSubscription = !subscriptions || !subscriptions.length const terminateSubscriptionDialogRef = useRef(null) + const annotatedSubscriptions = annotateSubscriptions(subscriptions) + return (
- - {translate('text_6250304370f0f700a8fdc28d')} - - {hasPermissions(['subscriptionsCreate']) && ( - - )} - - {loading ? ( - - {[0, 1, 2].map((_, i) => ( - - ))} - - ) : hasNoSubscription ? ( + { + navigate( + generatePath(CREATE_SUBSCRIPTION, { + customerId: customerId as string, + }), + ) + }, + } + : undefined + } + /> + + {!loading && hasNoSubscription && ( {translate('text_6250304370f0f700a8fdc28f')} - ) : ( + )} + + {!hasNoSubscription && ( <> - - - {translate('text_6253f11816f710014600b9ed')} - - - {translate('text_62d7f6178ec94cd09370e5fb')} - - - {translate('text_6253f11816f710014600b9f1')} - - - - {subscriptions.map((subscription, i) => { - return ( - - ) - })} - +
+ isDowngrade + ? '' + : generatePath(CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, { + customerId: customerId as string, + subscriptionId: id, + tab: CustomerSubscriptionDetailsTabsOptionsEnum.overview, + }) + } + columns={[ + { + key: 'name', + maxSpace: true, + title: translate('text_6253f11816f710014600b9ed'), + content: ({ name, isDowngrade }) => ( + <> +
+ {isDowngrade && } + + + {name} + + + {isDowngrade && } +
+ + ), + }, + { + key: 'frequency', + title: translate('text_1736968618645gg26amx8djq'), + content: ({ frequency }) => ( + {translate(getIntervalTranslationKey[frequency])} + ), + }, + { + key: 'startedAt', + title: translate('text_65201c5a175a4b0238abf29e'), + content: ({ startedAt }) => ( + + ), + }, + { + key: 'endingAt', + title: translate('text_65201c5a175a4b0238abf2a0'), + content: ({ endingAt }) => + endingAt ? ( + + ) : ( + - + ), + }, + { + key: 'statusType.type', + title: translate('text_62d7f6178ec94cd09370e5fb'), + content: ({ statusType }) => , + }, + ]} + actionColumn={(subscription) => + generateActionColumn({ + subscription, + customerId, + navigate, + translate, + terminateSubscriptionDialogRef, + hasSubscriptionsUpdatePermission: hasPermissions(['subscriptionsUpdate']), + }) + } + /> )} + ) } CustomerSubscriptionsList.displayName = 'CustomerSubscriptionsList' - -const ListHeader = styled.div` - height: ${HEADER_TABLE_HEIGHT}px; - display: grid; - align-items: center; - padding: 0 ${theme.spacing(4)}; - grid-template-columns: 1fr 80px 120px 40px; - grid-column-gap: ${theme.spacing(4)}; -` - -const List = styled.div` - > *:not(:last-child) { - margin-bottom: ${theme.spacing(4)}; - } -` - -const LoadingContent = styled.div` - > *:not(:last-child) { - margin-bottom: ${theme.spacing(4)}; - } -` diff --git a/src/components/customers/subscriptions/SubscriptionItem.tsx b/src/components/customers/subscriptions/SubscriptionItem.tsx deleted file mode 100644 index c3bb5ae94..000000000 --- a/src/components/customers/subscriptions/SubscriptionItem.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { gql } from '@apollo/client' -import { PropsWithChildren, RefObject } from 'react' - -import { Skeleton, Typography } from '~/components/designSystem' -import { - StatusTypeEnum, - SubscriptionItemFragment, - SubscriptionLinePlanFragmentDoc, - TimezoneEnum, -} from '~/generated/graphql' -import { useInternationalization } from '~/hooks/core/useInternationalization' -import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' - -import { SubscriptionLine } from './SubscriptionLine' -import { TerminateCustomerSubscriptionDialogRef } from './TerminateCustomerSubscriptionDialog' - -gql` - fragment SubscriptionItem on Subscription { - id - status - startedAt - nextPendingStartDate - name - nextName - externalId - subscriptionAt - endingAt - plan { - ...SubscriptionLinePlan - } - nextPlan { - ...SubscriptionLinePlan - } - nextSubscription { - id - } - } - - ${SubscriptionLinePlanFragmentDoc} -` - -const DateInfos = ({ children }: PropsWithChildren) => ( - - {children} - -) - -interface SubscriptionItemProps { - subscription: SubscriptionItemFragment - customerTimezone?: TimezoneEnum - terminateSubscriptionDialogRef: RefObject | null -} - -export const SubscriptionItem = ({ - subscription, - customerTimezone, - terminateSubscriptionDialogRef, -}: SubscriptionItemProps) => { - const { translate } = useInternationalization() - const { - id, - plan, - status, - nextPlan, - nextPendingStartDate, - externalId, - nextName, - name, - startedAt, - subscriptionAt, - endingAt, - } = subscription || {} - const { formatTimeOrgaTZ } = useOrganizationInfos() - const isDowngrading = !!nextPlan - const isPending = status === StatusTypeEnum.Pending - const hasEndingAtForActive = status === StatusTypeEnum.Active && !!endingAt - - if (!subscription) return null - - return ( -
- {isDowngrading && ( - - )} - - {isDowngrading ? ( - - {translate('text_62681c60582e4f00aa82938a', { - planName: nextPlan?.name, - dateStartNewPlan: !nextPendingStartDate ? '-' : formatTimeOrgaTZ(nextPendingStartDate), - })} - - ) : isPending ? ( - - {translate('text_6335e50b0b089e1d8ed50960', { - planName: plan?.name, - startDate: formatTimeOrgaTZ(subscriptionAt), - })} - - ) : hasEndingAtForActive ? ( - - {translate('text_64ef55a730b88e3d2117b44e', { - planName: plan?.name, - date: formatTimeOrgaTZ(endingAt), - })} - - ) : null} -
- ) -} - -SubscriptionItem.displayName = 'SubscriptionItem' - -export const SubscriptionItemSkeleton = () => { - return ( -
- -
- - -
-
- ) -} From 981d06576319c70dc1d9cd123deade426641735a Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:20:27 +0200 Subject: [PATCH 12/43] refactor(CustomerMainInfos): always show all the customer details --- .../customers/CustomerMainInfos.tsx | 76 ++++--------------- 1 file changed, 13 insertions(+), 63 deletions(-) diff --git a/src/components/customers/CustomerMainInfos.tsx b/src/components/customers/CustomerMainInfos.tsx index 5385f6c54..99dc45a60 100644 --- a/src/components/customers/CustomerMainInfos.tsx +++ b/src/components/customers/CustomerMainInfos.tsx @@ -1,11 +1,12 @@ import { gql } from '@apollo/client' import { Stack } from '@mui/material' -import { FC, PropsWithChildren, useCallback, useRef, useState } from 'react' +import { FC, PropsWithChildren } from 'react' import { Link, LinkProps } from 'react-router-dom' import styled from 'styled-components' import { TRANSLATIONS_MAP_CUSTOMER_TYPE } from '~/components/customers/utils' -import { Avatar, Button, Icon, Skeleton, Typography } from '~/components/designSystem' +import { Avatar, Icon, Skeleton, Typography } from '~/components/designSystem' +import { PageSectionTitle } from '~/components/layouts/Section' import { CountryCodes } from '~/core/constants/countryCodes' import { buildAnrokCustomerUrl, @@ -195,8 +196,6 @@ interface CustomerMainInfosProps { onEdit?: () => unknown } -const SHOW_MORE_THRESHOLD = 6 - const InlineLink: FC> = ({ children, ...props }) => { return ( > = ({ children, ...props }) => export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInfosProps) => { const { translate } = useInternationalization() - const [showMore, setShowMore] = useState(false) - const [shouldSeeMoreButton, setShouldSeeMoreButton] = useState(false) - const infosRef = useRef(null) const { data: paymentProvidersData } = usePaymentProvidersListForCustomerMainInfosQuery({ variables: { limit: 1000 }, @@ -272,15 +268,6 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf (integration) => integration?.id === customer?.salesforceCustomer?.integrationId, ) as SalesforceIntegration - const updateRef = useCallback( - (node: HTMLDivElement) => { - if (customer && node) { - setShouldSeeMoreButton(node.childNodes.length >= SHOW_MORE_THRESHOLD) - } - }, - [customer], - ) - if (loading || !customer) return ( @@ -327,24 +314,16 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf return ( - - {translate('text_6250304370f0f700a8fdc27d')} - - - - { - infosRef.current = node - - if (node) { - updateRef(node) - } + onEdit?.(), }} - data-id="customer-info-list" - $showMore={showMore} - > + /> + + {customerType && (
{translate('text_1726128938631ioz4orixel3')} @@ -707,25 +686,6 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf
))}
- {shouldSeeMoreButton && !showMore && ( - { - const hiddenItems = Array.from( - infosRef.current?.querySelectorAll( - `*:nth-of-type(n + ${SHOW_MORE_THRESHOLD})`, - ) as NodeListOf, - ) - - hiddenItems?.forEach((item) => { - item.style.display = 'block' - }) - - setShowMore(true) - }} - > - {translate('text_6670a2a7ae3562006c4ee3ce')} - - )}
) } @@ -746,15 +706,10 @@ const DetailsBlock = styled.div` } ` -const InfosBlock = styled.div<{ $showMore: boolean }>` +const InfosBlock = styled.div` > *:not(:last-child) { margin-bottom: ${theme.spacing(3)}; } - - // Hide all items after the threshold - > *:nth-child(n + ${SHOW_MORE_THRESHOLD}) { - ${({ $showMore }) => ($showMore ? 'display: block;' : 'display: none;')} - } ` const SectionHeader = styled.div` @@ -763,8 +718,3 @@ const SectionHeader = styled.div` justify-content: space-between; margin-bottom: ${theme.spacing(4)}; ` - -const ShowMoreButton = styled.span` - color: ${theme.palette.primary[600]}; - cursor: pointer; -` From 86f73a381ce818cb31a1827599b7f84dd393d305 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:21:18 +0200 Subject: [PATCH 13/43] refactor(CustomerOverview): re-use PageSectionTitle --- .../customers/overview/CustomerOverview.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/components/customers/overview/CustomerOverview.tsx b/src/components/customers/overview/CustomerOverview.tsx index e3fb31d27..e07819eff 100644 --- a/src/components/customers/overview/CustomerOverview.tsx +++ b/src/components/customers/overview/CustomerOverview.tsx @@ -6,7 +6,8 @@ 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 { Alert, Typography } from '~/components/designSystem' +import { PageSectionTitle } from '~/components/layouts/Section' import { OverviewCard } from '~/components/OverviewCard' import { intlFormatNumber } from '~/core/formats/intlFormatNumber' import { CUSTOMER_REQUEST_OVERDUE_PAYMENT_ROUTE } from '~/core/router' @@ -22,7 +23,6 @@ import { import { useInternationalization } from '~/hooks/core/useInternationalization' import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' import { usePermissions } from '~/hooks/usePermissions' -import { SectionHeader } from '~/styles/customer' gql` query getCustomerOverdueBalances( @@ -158,16 +158,15 @@ export const CustomerOverview: FC = ({ const hasMadePaymentRequestToday = isSameDay(lastPaymentRequestDate, today) return ( - <> +
{(!overdueBalancesError || !grossRevenuesError) && (
- - {translate('text_6670a7222702d70114cc7954')} - - - + }, + }} + /> + {hasOverdueInvoices && !overdueBalancesError && ( = ({ )} {!isLoading && } - +
) } From e509605b6e6bdf74b6142aeebd678342909fee1f Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:21:48 +0200 Subject: [PATCH 14/43] refactor(CustomerDetails): replace customer details drawer with tab --- src/pages/CustomerDetails.tsx | 346 ++++++++++++++-------------------- 1 file changed, 145 insertions(+), 201 deletions(-) diff --git a/src/pages/CustomerDetails.tsx b/src/pages/CustomerDetails.tsx index 85cf8c304..b473364f5 100644 --- a/src/pages/CustomerDetails.tsx +++ b/src/pages/CustomerDetails.tsx @@ -49,7 +49,7 @@ import { import { useInternationalization } from '~/hooks/core/useInternationalization' import { usePermissions } from '~/hooks/usePermissions' import ErrorImage from '~/public/images/maneki/error.svg' -import { MenuPopper, NAV_HEIGHT, PageHeader, theme } from '~/styles' +import { MenuPopper, PageHeader, theme } from '~/styles' gql` fragment CustomerDetails on Customer { @@ -94,6 +94,7 @@ export enum CustomerDetailsTabsOptions { invoices = 'invoices', settings = 'settings', usage = 'usage', + details = 'details', } const CustomerDetails = () => { @@ -158,6 +159,7 @@ const CustomerDetails = () => { )} + - - - - )} - - - - {hasPermissions(['subscriptionsUpdate']) && ( - - )} - - )} - - - ) -} - -SubscriptionLine.displayName = 'SubscriptionLine' - -const Item = styled(ListItemLink)<{ $hasBottomSection?: boolean; $hasAboveSection?: boolean }>` - height: ${NAV_HEIGHT}px; - display: grid; - grid-template-columns: minmax(0, 1fr) 80px 120px auto; - grid-column-gap: ${theme.spacing(4)}; - padding: 0 ${theme.spacing(4)}; - box-shadow: none; - - &:hover, - &:active { - box-shadow: none; - border-radius: ${({ $hasBottomSection, $hasAboveSection }) => - $hasAboveSection ? '0px' : $hasBottomSection ? '12px 12px 0 0' : '12px'}; - } -` - -const NameBlock = styled.div` - min-width: 0; -` - -const ButtonMock = styled.div` - width: 40px; -` - -const LocalPopperOpener = styled(PopperOpener)` - right: ${theme.spacing(4)}; -` From 63f777a3849e9dad285675655b117a0a45985857 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Wed, 22 Jan 2025 14:54:05 +0200 Subject: [PATCH 23/43] refactor(CustomerSubscriptionsList): update graphql --- .../overview/CustomerSubscriptionsList.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index 0a6ef6980..5e7b84f3e 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -44,13 +44,29 @@ gql` id subscriptions(status: [active, pending]) { id + status + startedAt + nextPendingStartDate + name + nextName + externalId + subscriptionAt + endingAt plan { id amountCurrency name interval } - ...SubscriptionItem + nextPlan { + id + name + code + interval + } + nextSubscription { + id + } } } } From 18e215f930705885cba14baf689bb24dd62bf923 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Wed, 22 Jan 2025 14:54:12 +0200 Subject: [PATCH 24/43] chore(graphql): generate types --- src/generated/graphql.tsx | 66 ++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index ec7440b4b..7694d2be1 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -1158,6 +1158,7 @@ export type CreateCreditNoteInput = { /** Create Customer input arguments */ export type CreateCustomerInput = { + accountType?: InputMaybe; addressLine1?: InputMaybe; addressLine2?: InputMaybe; billingConfiguration?: InputMaybe; @@ -1906,6 +1907,7 @@ export type CurrentVersion = { export type Customer = { __typename?: 'Customer'; + accountType: CustomerAccountTypeEnum; /** Number of active subscriptions per customer */ activeSubscriptionsCount: Scalars['Int']['output']; addressLine1?: Maybe; @@ -1988,6 +1990,11 @@ export type CustomerSubscriptionsArgs = { status?: InputMaybe>; }; +export enum CustomerAccountTypeEnum { + Customer = 'customer', + Partner = 'partner' +} + export type CustomerAddress = { __typename?: 'CustomerAddress'; addressLine1?: Maybe; @@ -2888,6 +2895,7 @@ export enum IntegrationTypeEnum { Okta = 'okta', ProgressiveBilling = 'progressive_billing', RevenueAnalytics = 'revenue_analytics', + RevenueShare = 'revenue_share', Salesforce = 'salesforce', Xero = 'xero' } @@ -2958,6 +2966,7 @@ export type Invoice = { prepaidCreditAmountCents: Scalars['BigInt']['output']; progressiveBillingCreditAmountCents: Scalars['BigInt']['output']; refundableAmountCents: Scalars['BigInt']['output']; + selfBilled: Scalars['Boolean']['output']; sequentialId: Scalars['ID']['output']; status: InvoiceStatusTypeEnum; subTotalExcludingTaxesAmountCents: Scalars['BigInt']['output']; @@ -4453,6 +4462,7 @@ export enum PremiumIntegrationTypeEnum { Okta = 'okta', ProgressiveBilling = 'progressive_billing', RevenueAnalytics = 'revenue_analytics', + RevenueShare = 'revenue_share', Salesforce = 'salesforce', Xero = 'xero' } @@ -5868,6 +5878,7 @@ export type UpdateCreditNoteInput = { /** Update Customer input arguments */ export type UpdateCustomerInput = { + accountType?: InputMaybe; addressLine1?: InputMaybe; addressLine2?: InputMaybe; applicableInvoiceCustomSectionIds?: InputMaybe>; @@ -6934,11 +6945,7 @@ export type GetCustomerSubscriptionForListQueryVariables = Exact<{ }>; -export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval, code: string }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }> } | null }; - -export type SubscriptionItemFragment = { __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, name: string, code: string }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }; - -export type SubscriptionLinePlanFragment = { __typename?: 'Plan', id: string, name: string, code: string }; +export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string, interval: PlanInterval } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }> } | null }; export type TerminateCustomerSubscriptionMutationVariables = Exact<{ input: TerminateSubscriptionInput; @@ -9741,35 +9748,6 @@ export const CustomerAppliedCouponsFragmentDoc = gql` } } ${CustomerCouponFragmentDoc}`; -export const SubscriptionLinePlanFragmentDoc = gql` - fragment SubscriptionLinePlan on Plan { - id - name - code -} - `; -export const SubscriptionItemFragmentDoc = gql` - fragment SubscriptionItem on Subscription { - id - status - startedAt - nextPendingStartDate - name - nextName - externalId - subscriptionAt - endingAt - plan { - ...SubscriptionLinePlan - } - nextPlan { - ...SubscriptionLinePlan - } - nextSubscription { - id - } -} - ${SubscriptionLinePlanFragmentDoc}`; export const CustomerUsageForUsageDetailsFragmentDoc = gql` fragment CustomerUsageForUsageDetails on CustomerUsage { fromDatetime @@ -15062,17 +15040,33 @@ export const GetCustomerSubscriptionForListDocument = gql` id subscriptions(status: [active, pending]) { id + status + startedAt + nextPendingStartDate + name + nextName + externalId + subscriptionAt + endingAt plan { id amountCurrency name interval } - ...SubscriptionItem + nextPlan { + id + name + code + interval + } + nextSubscription { + id + } } } } - ${SubscriptionItemFragmentDoc}`; + `; /** * __useGetCustomerSubscriptionForListQuery__ From 0791f3ca70ac1c79251e8f64f8a7411bd239eed2 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Wed, 22 Jan 2025 15:05:08 +0200 Subject: [PATCH 25/43] refactor(CustomerSubscriptionsList): update fragment for next subscription --- .../customers/overview/CustomerSubscriptionsList.tsx | 8 ++++++-- src/generated/graphql.tsx | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index 5e7b84f3e..de660558c 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -66,6 +66,8 @@ gql` } nextSubscription { id + name + externalId } } } @@ -109,6 +111,7 @@ const annotateSubscriptions = ( subscriptionAt, endingAt, customer, + nextSubscription, } = subscription || {} const isDowngrading = !!nextPlan @@ -138,8 +141,9 @@ const annotateSubscriptions = ( const _subDowngrade = isDowngrading && nextPlan && { - id: nextPlan.id, - name: nextName || nextPlan.name, + id: nextSubscription?.id || nextPlan.id, + externalId: nextSubscription?.externalId, + name: nextSubscription?.name || nextName || nextPlan.name, frequency: nextPlan.interval, startedAt: nextPendingStartDate, statusType: { diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 7694d2be1..c9825100d 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6945,7 +6945,7 @@ export type GetCustomerSubscriptionForListQueryVariables = Exact<{ }>; -export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string, interval: PlanInterval } | null, nextSubscription?: { __typename?: 'Subscription', id: string } | null }> } | null }; +export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string, interval: PlanInterval } | null, nextSubscription?: { __typename?: 'Subscription', id: string, name?: string | null, externalId: string } | null }> } | null }; export type TerminateCustomerSubscriptionMutationVariables = Exact<{ input: TerminateSubscriptionInput; @@ -15062,6 +15062,8 @@ export const GetCustomerSubscriptionForListDocument = gql` } nextSubscription { id + name + externalId } } } From 366100c1dafc3b5d8a5689a0cf3f29ae6dc3df15 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 15:58:45 +0200 Subject: [PATCH 26/43] chore(translations): add translations --- translations/base.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/translations/base.json b/translations/base.json index 97f0e832f..ca456e170 100644 --- a/translations/base.json +++ b/translations/base.json @@ -1873,7 +1873,7 @@ "text_6250304370f0f700a8fdc283": "External ID", "text_6250304370f0f700a8fdc28b": "Assign a plan", "text_6250304370f0f700a8fdc28d": "Subscriptions", - "text_6250304370f0f700a8fdc28f": "No plan linked to this customer, add a Plan to this customer to start a Subscription.", + "text_6250304370f0f700a8fdc28f": "No subscriptions are available. Please assign a plan to start one.", "text_6250304370f0f700a8fdc291": "Invoices", "text_6250304370f0f700a8fdc293": "No invoice linked to this customer. An invoice is generated at the end of a billing period for a plan, and immediately for an add-on.", "text_6250304370f0f700a8fdc295": "Customer successfully created", @@ -2792,5 +2792,7 @@ "text_1736968618645gg26amx8djq": "Frequency", "text_1736972452609qdjngeuqsz0": "Downgrade", "text_1736972452609g2v8mzgvi2t": "Scheduled", - "text_1737059551511f5acxkfz7p4": "Retrieve all customer details, including connected external apps." + "text_1737059551511f5acxkfz7p4": "Retrieve all customer details, including connected external apps.", + "text_17376404438209bh9jk7xa2s": "Details" } + From 6af42cfeb94640a39e4012477cd85d4841f7c6de Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 15:59:00 +0200 Subject: [PATCH 27/43] feat(CustomerDetails): update label --- src/pages/CustomerDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/CustomerDetails.tsx b/src/pages/CustomerDetails.tsx index f2eaf1e66..45edbc625 100644 --- a/src/pages/CustomerDetails.tsx +++ b/src/pages/CustomerDetails.tsx @@ -430,7 +430,7 @@ const CustomerDetails = () => { ), }, { - title: translate('text_6250304370f0f700a8fdc27d'), + title: translate('text_17376404438209bh9jk7xa2s'), link: generatePath(CUSTOMER_DETAILS_TAB_ROUTE, { customerId: customerId as string, tab: CustomerDetailsTabsOptions.details, From 011e63e55793710ee33625e71717d40dfb630e1f Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 15:59:30 +0200 Subject: [PATCH 28/43] feat(TimezoneDate): add typographyClassName prop --- src/components/TimezoneDate.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/TimezoneDate.tsx b/src/components/TimezoneDate.tsx index 085f9149c..71605e632 100644 --- a/src/components/TimezoneDate.tsx +++ b/src/components/TimezoneDate.tsx @@ -3,6 +3,7 @@ import { formatDateToTZ, getTimezoneConfig } from '~/core/timezone' import { TimezoneEnum } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' +import { tw } from '~/styles/utils' enum MainTimezoneEnum { utc0 = 'utc0', @@ -17,6 +18,7 @@ interface TimezoneDateProps { customerTimezone?: TimezoneEnum mainTypographyProps?: Pick className?: string + typographyClassName?: string } export const TimezoneDate = ({ @@ -25,6 +27,7 @@ export const TimezoneDate = ({ mainTimezone = MainTimezoneEnum.organization, customerTimezone, mainTypographyProps, + typographyClassName, className, }: TimezoneDateProps) => { const { translate } = useInternationalization() @@ -69,7 +72,7 @@ export const TimezoneDate = ({ placement="top-end" > Date: Thu, 23 Jan 2025 15:59:59 +0200 Subject: [PATCH 29/43] refactor(CustomerSubscriptionsList): scheduled status. column order. date border --- .../overview/CustomerSubscriptionsList.tsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index de660558c..a85bf3fe0 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -32,6 +32,7 @@ import { } from '~/generated/graphql' import { TranslateFunc, useInternationalization } from '~/hooks/core/useInternationalization' import { usePermissions } from '~/hooks/usePermissions' +import { tw } from '~/styles/utils' import { TerminateCustomerSubscriptionDialog, @@ -91,6 +92,7 @@ type AnnotatedSubscription = { label: string } isDowngrade?: boolean + isScheduled?: boolean customerId: string } @@ -137,6 +139,7 @@ const annotateSubscriptions = ( }), }, customerId: customer?.id, + isScheduled: status === StatusTypeEnum.Pending, } const _subDowngrade = isDowngrading && @@ -273,7 +276,9 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip /> {!loading && hasNoSubscription && ( - {translate('text_6250304370f0f700a8fdc28f')} + + {translate('text_6250304370f0f700a8fdc28f')} + )} {!hasNoSubscription && ( @@ -293,13 +298,22 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip }) } columns={[ + { + key: 'statusType.type', + title: translate('text_62d7f6178ec94cd09370e5fb'), + content: ({ statusType }) => , + }, { key: 'name', maxSpace: true, title: translate('text_6253f11816f710014600b9ed'), - content: ({ name, isDowngrade }) => ( + content: ({ name, isDowngrade, isScheduled }) => ( <> -
+
{isDowngrade && } @@ -307,6 +321,8 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip {isDowngrade && } + + {isScheduled && }
), @@ -323,9 +339,7 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip title: translate('text_65201c5a175a4b0238abf29e'), content: ({ startedAt }) => ( @@ -337,9 +351,7 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip content: ({ endingAt }) => endingAt ? ( @@ -347,11 +359,6 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip - ), }, - { - key: 'statusType.type', - title: translate('text_62d7f6178ec94cd09370e5fb'), - content: ({ statusType }) => , - }, ]} actionColumn={(subscription) => generateActionColumn({ From 239ff0f9f3a56078cf7caf3c6c96a5402a5e21e9 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 16:09:05 +0200 Subject: [PATCH 30/43] chore(graphql): generate types --- src/generated/graphql.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index c9825100d..7fd82d31f 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6945,7 +6945,7 @@ export type GetCustomerSubscriptionForListQueryVariables = Exact<{ }>; -export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string, interval: PlanInterval } | null, nextSubscription?: { __typename?: 'Subscription', id: string, name?: string | null, externalId: string } | null }> } | null }; +export type GetCustomerSubscriptionForListQuery = { __typename?: 'Query', customer?: { __typename?: 'Customer', id: string, subscriptions: Array<{ __typename?: 'Subscription', id: string, status?: StatusTypeEnum | null, startedAt?: any | null, nextPendingStartDate?: any | null, name?: string | null, nextName?: string | null, externalId: string, subscriptionAt?: any | null, endingAt?: any | null, plan: { __typename?: 'Plan', id: string, amountCurrency: CurrencyEnum, name: string, interval: PlanInterval }, nextPlan?: { __typename?: 'Plan', id: string, name: string, code: string, interval: PlanInterval } | null, nextSubscription?: { __typename?: 'Subscription', id: string, name?: string | null, externalId: string, status?: StatusTypeEnum | null } | null }> } | null }; export type TerminateCustomerSubscriptionMutationVariables = Exact<{ input: TerminateSubscriptionInput; @@ -15064,6 +15064,7 @@ export const GetCustomerSubscriptionForListDocument = gql` id name externalId + status } } } From a3135d509449baef95ece2b4c64b8ab09e71e7cd Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 16:09:31 +0200 Subject: [PATCH 31/43] fix(CustomerSubscriptionList): enable clicking on downgrade. fix downgrade status --- .../overview/CustomerSubscriptionsList.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index a85bf3fe0..ec4e66bad 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -69,6 +69,7 @@ gql` id name externalId + status } } } @@ -147,6 +148,7 @@ const annotateSubscriptions = ( id: nextSubscription?.id || nextPlan.id, externalId: nextSubscription?.externalId, name: nextSubscription?.name || nextName || nextPlan.name, + status: nextSubscription?.status, frequency: nextPlan.interval, startedAt: nextPendingStartDate, statusType: { @@ -288,14 +290,12 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip data={annotatedSubscriptions || []} containerSize={0} isLoading={loading} - onRowActionLink={({ id, isDowngrade }) => - isDowngrade - ? '' - : generatePath(CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, { - customerId: customerId as string, - subscriptionId: id, - tab: CustomerSubscriptionDetailsTabsOptionsEnum.overview, - }) + onRowActionLink={({ id }) => + generatePath(CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, { + customerId: customerId as string, + subscriptionId: id, + tab: CustomerSubscriptionDetailsTabsOptionsEnum.overview, + }) } columns={[ { From 82d561dae4f29bd7051cfcd39d4e2d3d5c63b214 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 17:27:47 +0200 Subject: [PATCH 32/43] fix: add table paddings --- src/components/customers/overview/CustomerCoupons.tsx | 2 +- src/components/customers/overview/CustomerSubscriptionsList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/customers/overview/CustomerCoupons.tsx b/src/components/customers/overview/CustomerCoupons.tsx index 710852118..7ae31a7d4 100644 --- a/src/components/customers/overview/CustomerCoupons.tsx +++ b/src/components/customers/overview/CustomerCoupons.tsx @@ -97,7 +97,7 @@ export const CustomerCoupons = memo(() => {
generatePath(CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, { From 77228cfc4ec1264b0adad66708b718b4a21921c9 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 17:29:32 +0200 Subject: [PATCH 33/43] fix(CustomerSubscriptionList): pass subscription status to terminate dialog --- src/components/customers/overview/CustomerSubscriptionsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index 800e3ae16..8f6c0d329 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -233,7 +233,7 @@ const generateActionColumn = ({ terminateSubscriptionDialogRef?.current?.openDialog({ id: subscription.id, name: subscription.name, - status: status as StatusTypeEnum, + status: subscription.status as StatusTypeEnum, }), }) } From 3b9f8bf538c181d891168f401bf5e5455f46716d Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 17:40:16 +0200 Subject: [PATCH 34/43] feat: move customer overview to the invoices tab --- src/components/customers/CustomerInvoicesTab.tsx | 13 ++++++++++++- .../customers/overview/CustomerOverview.tsx | 9 --------- src/pages/CustomerDetails.tsx | 15 ++++++++------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/components/customers/CustomerInvoicesTab.tsx b/src/components/customers/CustomerInvoicesTab.tsx index 638770e25..2ca2ec161 100644 --- a/src/components/customers/CustomerInvoicesTab.tsx +++ b/src/components/customers/CustomerInvoicesTab.tsx @@ -1,9 +1,11 @@ import { gql } from '@apollo/client' import { generatePath } from 'react-router-dom' +import { CustomerOverview } from '~/components/customers/overview/CustomerOverview' import { ButtonLink, Skeleton, Typography } from '~/components/designSystem' import { CUSTOMER_DRAFT_INVOICES_LIST_ROUTE } from '~/core/router' import { + CurrencyEnum, InvoiceForInvoiceListFragmentDoc, InvoiceStatusTypeEnum, TimezoneEnum, @@ -44,9 +46,16 @@ gql` interface CustomerInvoicesTabProps { customerId: string customerTimezone?: TimezoneEnum + externalId?: string + userCurrency?: CurrencyEnum } -export const CustomerInvoicesTab = ({ customerId, customerTimezone }: CustomerInvoicesTabProps) => { +export const CustomerInvoicesTab = ({ + customerId, + customerTimezone, + externalId, + userCurrency, +}: CustomerInvoicesTabProps) => { const { translate } = useInternationalization() const { data: dataDraft, @@ -89,6 +98,8 @@ export const CustomerInvoicesTab = ({ customerId, customerTimezone }: CustomerIn return (
+ + {initialLoad ? (
diff --git a/src/components/customers/overview/CustomerOverview.tsx b/src/components/customers/overview/CustomerOverview.tsx index e07819eff..c89b2e759 100644 --- a/src/components/customers/overview/CustomerOverview.tsx +++ b/src/components/customers/overview/CustomerOverview.tsx @@ -4,8 +4,6 @@ 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, Typography } from '~/components/designSystem' import { PageSectionTitle } from '~/components/layouts/Section' import { OverviewCard } from '~/components/OverviewCard' @@ -16,7 +14,6 @@ import { isSameDay } from '~/core/timezone' import { LocaleEnum } from '~/core/translations' import { CurrencyEnum, - TimezoneEnum, useGetCustomerGrossRevenuesLazyQuery, useGetCustomerOverdueBalancesLazyQuery, } from '~/generated/graphql' @@ -71,16 +68,12 @@ gql` interface CustomerOverviewProps { externalCustomerId?: string - customerTimezone?: TimezoneEnum userCurrency?: CurrencyEnum - isLoading?: boolean } export const CustomerOverview: FC = ({ externalCustomerId, - customerTimezone, userCurrency, - isLoading, }) => { const { translate } = useInternationalization() const { organization, formatTimeOrgaTZ } = useOrganizationInfos() @@ -271,8 +264,6 @@ export const CustomerOverview: FC = ({ )} - {!isLoading && } -
) } diff --git a/src/pages/CustomerDetails.tsx b/src/pages/CustomerDetails.tsx index 45edbc625..9de85dc5f 100644 --- a/src/pages/CustomerDetails.tsx +++ b/src/pages/CustomerDetails.tsx @@ -14,7 +14,8 @@ import { DeleteCustomerDialog, DeleteCustomerDialogRef, } from '~/components/customers/DeleteCustomerDialog' -import { CustomerOverview } from '~/components/customers/overview/CustomerOverview' +import { CustomerCoupons } from '~/components/customers/overview/CustomerCoupons' +import { CustomerSubscriptionsList } from '~/components/customers/overview/CustomerSubscriptionsList' import { CustomerUsage } from '~/components/customers/usage/CustomerUsage' import { computeCustomerInitials } from '~/components/customers/utils' import { @@ -367,12 +368,10 @@ const CustomerDetails = () => { }), ], component: ( - +
+ + +
), }, { @@ -407,6 +406,8 @@ const CustomerDetails = () => { }), component: ( From bb59cfefafb5d802ffe8627a9c4b7fb12bf25fce Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:00:39 +0200 Subject: [PATCH 35/43] chore(translations): add translations --- translations/base.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/translations/base.json b/translations/base.json index ca456e170..0f7efc09b 100644 --- a/translations/base.json +++ b/translations/base.json @@ -585,7 +585,7 @@ "text_62d175066d2dbf1d50bc937c": "Wallets", "text_62d175066d2dbf1d50bc9382": "Add a wallet & credits", "text_62d175066d2dbf1d50bc9384": "Wallet", - "text_62d175066d2dbf1d50bc9386": "No wallet linked to this customer. Add a subscription to this customer and assign a wallet with prepaid credits.", + "text_62d175066d2dbf1d50bc9386": "No wallets are available. Please create one to add prepaid credits.", "text_62d175066d2dbf1d50bc93a5": "Add wallet & credits", "text_62d18855b22699e5cf55f873": "Applying credit to this customer generates an invoice. The plan’s usage will be subtracted from the credit bought.", "text_62d18855b22699e5cf55f875": "Wallet name (optional)", @@ -1345,7 +1345,7 @@ "text_6670a6577ecbf200898af647": "Overdue invoices", "text_6670a6577ecbf200898af64a": "across {{count}} invoices", "text_6670a7222702d70114cc7953": "Refresh", - "text_6670a7222702d70114cc7954": "Billing overview", + "text_6670a7222702d70114cc7954": "Invoice overview", "text_6670a7222702d70114cc7955": "{{count}} invoice totaling {{amount}} is overdue.|{{count}} invoices totaling {{amount}} are overdue.", "text_6670a7222702d70114cc7957": "Total invoiced", "text_6670a7222702d70114cc795a": "Total overdue", @@ -2793,6 +2793,11 @@ "text_1736972452609qdjngeuqsz0": "Downgrade", "text_1736972452609g2v8mzgvi2t": "Scheduled", "text_1737059551511f5acxkfz7p4": "Retrieve all customer details, including connected external apps.", - "text_17376404438209bh9jk7xa2s": "Details" + "text_17376404438209bh9jk7xa2s": "Details", + "text_1737647019083bbxjrexen5s": "A wallet allows customers to prepay credits, generating an invoice instantly. Once paid, these credits are deducted from future invoices.", + "text_173764736415670g9n7v9tth": "Find insights linked to this customer.", + "text_1737649151689ldyvwtq9ov1": "Retrieve invoice data associated with this customer.", + "text_1737654864705k68zqvg5u9d": "List of finalized invoices. Please note these are no longer editable.", + "text_1737655039923xyw73dt51ee": "Draft invoices are still editable. Send events on the appropriate invoice date or manually adjust fees to modify them." } From f62ead842be930341cd511fad8c462cc8106a553 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:00:54 +0200 Subject: [PATCH 36/43] feat(PageSectionTitle): add support for a custom action --- src/components/layouts/Section.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/layouts/Section.tsx b/src/components/layouts/Section.tsx index fd1e85471..a55395fdc 100644 --- a/src/components/layouts/Section.tsx +++ b/src/components/layouts/Section.tsx @@ -8,12 +8,14 @@ export const PageSectionTitle = ({ title, subtitle, action, + customAction, loading, }: { className?: string title: string subtitle?: string action?: { title: string; onClick: () => void; dataTest?: string } + customAction?: React.ReactNode loading?: boolean }) => { return ( @@ -41,6 +43,8 @@ export const PageSectionTitle = ({ {action.title} )} + + {customAction ? customAction : null} )}
From 72f10c3c3f67c564ab92871f2069e3078f931d95 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:01:20 +0200 Subject: [PATCH 37/43] refactor(CustomerWalletsList): unify title --- src/components/wallets/CustomerWalletList.tsx | 184 +++++++++--------- 1 file changed, 93 insertions(+), 91 deletions(-) diff --git a/src/components/wallets/CustomerWalletList.tsx b/src/components/wallets/CustomerWalletList.tsx index 8d833bf68..889f7fe13 100644 --- a/src/components/wallets/CustomerWalletList.tsx +++ b/src/components/wallets/CustomerWalletList.tsx @@ -4,6 +4,7 @@ import { generatePath, useNavigate } from 'react-router-dom' import { Button, InfiniteScroll, Popper, Typography } from '~/components/designSystem' import { GenericPlaceholder } from '~/components/GenericPlaceholder' +import { PageSectionTitle } from '~/components/layouts/Section' import { CREATE_WALLET_ROUTE, EDIT_WALLET_ROUTE } from '~/core/router' import { TimezoneEnum, @@ -18,7 +19,6 @@ import { useInternationalization } from '~/hooks/core/useInternationalization' import { usePermissions } from '~/hooks/usePermissions' import ErrorImage from '~/public/images/maneki/error.svg' import { MenuPopper } from '~/styles' -import { SectionHeader } from '~/styles/customer' import { TerminateCustomerWalletDialog, @@ -98,103 +98,103 @@ export const CustomerWalletsList = ({ customerId, customerTimezone }: CustommerW return ( <>
- - {translate('text_62d175066d2dbf1d50bc9384')} - - {hasAnyPermissionsToShowActions && ( + - {!activeWallet && hasPermissions(['walletsCreate']) ? ( - - ) : ( - - {translate('text_62e161ceb87c201025388aa2')} + {hasAnyPermissionsToShowActions && ( + <> + {!activeWallet && hasPermissions(['walletsCreate']) ? ( + - } - > - {({ closePopper }) => ( - - {hasPermissions(['walletsTopUp']) && ( - - )} - - {hasPermissions(['walletsUpdate']) && ( - + } + > + {({ closePopper }) => ( + + {hasPermissions(['walletsTopUp']) && ( + + )} + + {hasPermissions(['walletsUpdate']) && ( + + )} + + {hasPermissions(['walletsTerminate']) && ( + <> + + + + + )} + )} - - {hasPermissions(['walletsTerminate']) && ( - <> - - - - - )} - + )} - + )} - )} - + } + /> {!!loading ? (
@@ -203,7 +203,9 @@ export const CustomerWalletsList = ({ customerId, customerTimezone }: CustommerW ))}
) : !loading && !!hasNoWallet ? ( - {translate('text_62d175066d2dbf1d50bc9386')} + + {translate('text_62d175066d2dbf1d50bc9386')} + ) : ( { From 579fcad418634cfaee0481772fa17196ee5d7902 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:01:40 +0200 Subject: [PATCH 38/43] refactor(CustomerUsage): unify title --- .../customers/usage/CustomerUsage.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/customers/usage/CustomerUsage.tsx b/src/components/customers/usage/CustomerUsage.tsx index 29a669716..4fcb48072 100644 --- a/src/components/customers/usage/CustomerUsage.tsx +++ b/src/components/customers/usage/CustomerUsage.tsx @@ -7,11 +7,11 @@ import MonthSelectorDropdown, { AnalyticsPeriodScopeEnum, TPeriodScopeTranslationLookupValue, } from '~/components/graphs/MonthSelectorDropdown' +import { PageSectionTitle } from '~/components/layouts/Section' import { PremiumWarningDialogRef } from '~/components/PremiumWarningDialog' import { useGetCustomerSubscriptionForUsageQuery } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' -import { SectionHeader } from '~/styles/customer' gql` query getCustomerSubscriptionForUsage($id: ID!) { @@ -41,19 +41,21 @@ export const CustomerUsage = ({ premiumWarningDialogRef }: CustomerUsageProps) = return (
- - {translate('text_65564e8e4af2340050d431be')} - - - + + } + /> Date: Thu, 23 Jan 2025 20:02:14 +0200 Subject: [PATCH 39/43] refactor(CustomerOverview): add subtitle --- src/components/customers/overview/CustomerOverview.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/customers/overview/CustomerOverview.tsx b/src/components/customers/overview/CustomerOverview.tsx index c89b2e759..6f9edb273 100644 --- a/src/components/customers/overview/CustomerOverview.tsx +++ b/src/components/customers/overview/CustomerOverview.tsx @@ -156,6 +156,7 @@ export const CustomerOverview: FC = ({
Date: Thu, 23 Jan 2025 20:02:52 +0200 Subject: [PATCH 40/43] refactor(CustomerInvoicesTab): improve logic clarity. unify design --- .../customers/CustomerInvoicesTab.tsx | 125 +++++++++--------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/src/components/customers/CustomerInvoicesTab.tsx b/src/components/customers/CustomerInvoicesTab.tsx index 2ca2ec161..dbe9adced 100644 --- a/src/components/customers/CustomerInvoicesTab.tsx +++ b/src/components/customers/CustomerInvoicesTab.tsx @@ -3,6 +3,7 @@ import { generatePath } from 'react-router-dom' import { CustomerOverview } from '~/components/customers/overview/CustomerOverview' import { ButtonLink, Skeleton, Typography } from '~/components/designSystem' +import { PageSectionTitle } from '~/components/layouts/Section' import { CUSTOMER_DRAFT_INVOICES_LIST_ROUTE } from '~/core/router' import { CurrencyEnum, @@ -96,11 +97,19 @@ export const CustomerInvoicesTab = ({ const invoicesFinalized = dataFinalized?.customerInvoices.collection const invoicesDraftCount = dataDraft?.customerInvoices.metadata.totalCount || 0 + const showInvoices = !initialLoad + const hasDraftInvoices = !!invoicesDraft?.length + const hasFinalizedInvoices = !!invoicesFinalized?.length + const isSearching = variablesFinalized?.searchTerm + const hasInvoices = hasDraftInvoices || hasFinalizedInvoices + + const showSeeMore = invoicesDraftCount > DRAFT_INVOICES_ITEMS_COUNT + return ( -
+
- {initialLoad ? ( + {initialLoad && (
- ) : !invoicesDraft?.length && - !invoicesFinalized?.length && - !variablesFinalized?.searchTerm ? ( + )} + + {showInvoices && !hasInvoices && !isSearching && ( {translate('text_6250304370f0f700a8fdc293')} - ) : ( - <> - {!!invoicesDraft?.length && ( -
-
- - {translate('text_638f4d756d899445f18a49ee')} - -
- - - {invoicesDraftCount > DRAFT_INVOICES_ITEMS_COUNT && ( -
- - {translate('text_638f4d756d899445f18a4a0e')} - -
- )} + )} + + {showInvoices && hasDraftInvoices && ( +
+ + + + + {showSeeMore && ( +
+ + {translate('text_638f4d756d899445f18a4a0e')} +
)} +
+ )} - {(loadingFinalized || - !!invoicesFinalized?.length || - !!variablesFinalized?.searchTerm) && ( - <> -
- - {translate('text_6250304370f0f700a8fdc291')} - - -
- + - - )} - + } + /> + + +
)}
) From b271f9c977a0b3125aa75011fa5ca82dbd563a71 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:06:49 +0200 Subject: [PATCH 41/43] refactor(Settings): update spacing --- src/components/layouts/Settings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/layouts/Settings.tsx b/src/components/layouts/Settings.tsx index 40ee3ef42..aab52b382 100644 --- a/src/components/layouts/Settings.tsx +++ b/src/components/layouts/Settings.tsx @@ -16,7 +16,7 @@ export const SettingsPageHeaderContainer = ({ children }: PropsWithChildren) => ) export const SettingsListWrapper = ({ children }: PropsWithChildren) => ( -
{children}
+
{children}
) export const SettingsListItem = ({ @@ -24,7 +24,7 @@ export const SettingsListItem = ({ className, }: PropsWithChildren & { className?: string }) => (
{children}
@@ -34,7 +34,7 @@ export const SettingsListItemLoadingSkeleton = ({ count = 1 }: { count?: number Array.from({ length: count }).map((_, index) => (
From 473acbf540766bb9d85b6a52c072ac7055f7e396 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:17:17 +0200 Subject: [PATCH 42/43] chore(translations): add translations --- translations/base.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/translations/base.json b/translations/base.json index 0f7efc09b..eeaae7e78 100644 --- a/translations/base.json +++ b/translations/base.json @@ -1875,7 +1875,7 @@ "text_6250304370f0f700a8fdc28d": "Subscriptions", "text_6250304370f0f700a8fdc28f": "No subscriptions are available. Please assign a plan to start one.", "text_6250304370f0f700a8fdc291": "Invoices", - "text_6250304370f0f700a8fdc293": "No invoice linked to this customer. An invoice is generated at the end of a billing period for a plan, and immediately for an add-on.", + "text_6250304370f0f700a8fdc293": "No invoices available. Create a one-off invoice or assign a plan to generate one.", "text_6250304370f0f700a8fdc295": "Customer successfully created", "text_6250304370f0f700a8fdc270": "Something went wrong", "text_6250304370f0f700a8fdc274": "Please refresh the page or contact us if the error persists.", @@ -2800,4 +2800,3 @@ "text_1737654864705k68zqvg5u9d": "List of finalized invoices. Please note these are no longer editable.", "text_1737655039923xyw73dt51ee": "Draft invoices are still editable. Send events on the appropriate invoice date or manually adjust fees to modify them." } - From 5aa4e99f2db9212e7eed97efb211bf7b3cca3ce0 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:17:57 +0200 Subject: [PATCH 43/43] refactor(CustomerInvoicesTab): do not show overview for 0 invoices. improve typography for 0 invoices --- src/components/customers/CustomerInvoicesTab.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/customers/CustomerInvoicesTab.tsx b/src/components/customers/CustomerInvoicesTab.tsx index dbe9adced..6c90fab5f 100644 --- a/src/components/customers/CustomerInvoicesTab.tsx +++ b/src/components/customers/CustomerInvoicesTab.tsx @@ -107,7 +107,9 @@ export const CustomerInvoicesTab = ({ return (
- + {showInvoices && hasInvoices && ( + + )} {initialLoad && (
@@ -122,7 +124,16 @@ export const CustomerInvoicesTab = ({ )} {showInvoices && !hasInvoices && !isSearching && ( - {translate('text_6250304370f0f700a8fdc293')} +
+ + + + {translate('text_6250304370f0f700a8fdc293')} + +
)} {showInvoices && hasDraftInvoices && (