From bf6d50fe13491a6553815c8e012e8dd454c88f00 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:04:01 +0200 Subject: [PATCH 01/64] 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 fc75f479faf3bc6f626a9c366a1043bae19571e3 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:04:16 +0200 Subject: [PATCH 02/64] 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 527dbb9f21713c72fa10de5a27dacca8d8b5a31d Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:04:36 +0200 Subject: [PATCH 03/64] 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 2dca2ceda..8d2a8381c 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6951,7 +6951,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 }; @@ -15082,6 +15082,8 @@ export const GetCustomerSubscriptionForListDocument = gql` plan { id amountCurrency + name + interval } ...SubscriptionItem } From 1c3f0e59601273a611d0c16cf5ef0447a34a33c3 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:05:02 +0200 Subject: [PATCH 04/64] 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 7878a33fbdbbcf54c5afa0baa7b86517ae457805 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:05:32 +0200 Subject: [PATCH 05/64] 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 61ab021747ae4e503821ee29d7dee4526eb54d55 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:08:18 +0200 Subject: [PATCH 06/64] 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 2130fcdd084207fce6d4d857c00b451124e58162 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:09:08 +0200 Subject: [PATCH 07/64] 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 3eecbd6afd13872f837fe1500c6faf4c9f1a0a6f Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:12:47 +0200 Subject: [PATCH 08/64] 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 a399066388b5132219d059a37b224d7d4ea085e5 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:14:26 +0200 Subject: [PATCH 09/64] 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 a2178fa1b..9e169713b 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 351c17db1f421051043385ad6f395f76a480bbca Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:16:50 +0200 Subject: [PATCH 10/64] 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 74ef626e4873746d51a6fba6398a0ac8c00bac2e Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:17:54 +0200 Subject: [PATCH 11/64] 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 614a6d03650bbb81e873ce1d27940a1f94697832 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:20:27 +0200 Subject: [PATCH 12/64] 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 2113d66c98d2bfbb53e57f696d3866a8ec51a61a Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:21:18 +0200 Subject: [PATCH 13/64] 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 7e8f462482ca497ba91e1d90fb0f9c500fd12fbd Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 21 Jan 2025 10:21:48 +0200 Subject: [PATCH 14/64] 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 6c55225bc430808f54d8a52e02bd211ca798f583 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Wed, 22 Jan 2025 14:54:05 +0200 Subject: [PATCH 23/64] 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 9aa0a018542eaad3615d5a4e376a44c91952439b Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Wed, 22 Jan 2025 14:54:12 +0200 Subject: [PATCH 24/64] chore(graphql): generate types --- src/generated/graphql.tsx | 55 ++++++++++++++------------------------- 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 8d2a8381c..086904783 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6951,11 +6951,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; @@ -9758,35 +9754,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 @@ -15079,17 +15046,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 ab41ac72cf79094c85e3bcbde6da2bb1b28c7a96 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Wed, 22 Jan 2025 15:05:08 +0200 Subject: [PATCH 25/64] 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 086904783..9935363e5 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6951,7 +6951,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; @@ -15068,6 +15068,8 @@ export const GetCustomerSubscriptionForListDocument = gql` } nextSubscription { id + name + externalId } } } From 3ab4f5fea18eccc09a60aff54e5b9fd4a3282566 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 15:58:45 +0200 Subject: [PATCH 26/64] 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 129dfae6f17d494a25e26a89c1e43aa24c427531 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 15:59:00 +0200 Subject: [PATCH 27/64] 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 0f4adfff2586b1d89b7e8b6c5ed9057cdb28d1e1 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 15:59:30 +0200 Subject: [PATCH 28/64] 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/64] 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 0877a9352ce0bf023979c75a465177794debe6de Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 16:09:05 +0200 Subject: [PATCH 30/64] 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 9935363e5..df2e4fb95 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -6951,7 +6951,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; @@ -15070,6 +15070,7 @@ export const GetCustomerSubscriptionForListDocument = gql` id name externalId + status } } } From 9ddf7a9737ad122ff205dc9f6f5b59d76d694cd8 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 16:09:31 +0200 Subject: [PATCH 31/64] 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 237524f951a16415563bed544e33bda766798a60 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 17:27:47 +0200 Subject: [PATCH 32/64] 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 50afb38511b70a89bc4ec8594718d32b2966a8e0 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 17:29:32 +0200 Subject: [PATCH 33/64] 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 df2d48759888dffce8fb2cc1379c66c4ce52cc4e Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 17:40:16 +0200 Subject: [PATCH 34/64] 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 e57f62f3fc095fc6c1781529179e4e06e86698c8 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:00:39 +0200 Subject: [PATCH 35/64] 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 60c16e93ae90d989c309c67e5b8556fcdd144e88 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:00:54 +0200 Subject: [PATCH 36/64] 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 e3ec3b13f3964c713323f0b66839a952642aee39 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:01:20 +0200 Subject: [PATCH 37/64] 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 0386de6aeb0294881a818c5d7cf504160b932624 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:01:40 +0200 Subject: [PATCH 38/64] 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/64] 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/64] 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 6b0508805bad446400b6f2d12ee2d8b236529cc0 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:06:49 +0200 Subject: [PATCH 41/64] 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 b93b176bd9ea1b9dbfee913b843879e547a2d8b4 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:17:17 +0200 Subject: [PATCH 42/64] 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 476b58f225cacc444a136eca41356d32187f09a7 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Thu, 23 Jan 2025 20:17:57 +0200 Subject: [PATCH 43/64] 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 && ( From 4467bd2b69a012329af5a4f3846cbc2e46ff2a92 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:21:49 +0200 Subject: [PATCH 44/64] feat(tailwind): add first-child helper --- tailwind.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tailwind.config.ts b/tailwind.config.ts index a11293b7e..016a6eb02 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -275,6 +275,7 @@ const config = { addVariant('not-last-child', '&>*:not(:last-child)') // Not last element addVariant('not-last', '&:not(:last-child)') + addVariant('first-child', '&>*:first-child') /** * Components From 7d9ba54c3e95307b654ca829bc41edb42faa5db2 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:21:57 +0200 Subject: [PATCH 45/64] chore(translations): add translations --- translations/base.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/translations/base.json b/translations/base.json index eeaae7e78..439c80060 100644 --- a/translations/base.json +++ b/translations/base.json @@ -2798,5 +2798,10 @@ "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." + "text_1737655039923xyw73dt51ee": "Draft invoices are still editable. Send events on the appropriate invoice date or manually adjust fees to modify them.", + "text_1737892224509yezgypqk5vp": "Partner information", + "text_17378922245103cc9xrd1tjj": "Billing information", + "text_1737892224510vc53d10q4h5": "Metadata", + "text_1737892224510jnd7cbdp2yg": "External connections" } + From d85807aba26b094317a62d6e3d25ac1ec4391365 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:22:29 +0200 Subject: [PATCH 46/64] refactor(CustomerMainInfos): separate into sections --- .../customers/CustomerMainInfos.tsx | 789 ++++++++++-------- 1 file changed, 421 insertions(+), 368 deletions(-) diff --git a/src/components/customers/CustomerMainInfos.tsx b/src/components/customers/CustomerMainInfos.tsx index 99dc45a60..1f9d66955 100644 --- a/src/components/customers/CustomerMainInfos.tsx +++ b/src/components/customers/CustomerMainInfos.tsx @@ -2,7 +2,6 @@ import { gql } from '@apollo/client' import { Stack } from '@mui/material' 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, Icon, Skeleton, Typography } from '~/components/designSystem' @@ -41,7 +40,6 @@ import Netsuite from '~/public/images/netsuite.svg' import Salesforce from '~/public/images/salesforce.svg' import Stripe from '~/public/images/stripe.svg' import Xero from '~/public/images/xero.svg' -import { theme } from '~/styles' const PaymentProviderMethodTranslationsLookup = { [ProviderPaymentMethodsEnum.BacsDebit]: 'text_65e1f90471bc198c0c934d92', @@ -196,6 +194,18 @@ interface CustomerMainInfosProps { onEdit?: () => unknown } +const InfoSection = ({ title, children }: { title: string; children: React.ReactNode }) => ( +
+ {title} + + {children} +
+) + +const InfoBlock = ({ children }: { children: React.ReactNode }) => ( +
{children}
+) + const InlineLink: FC> = ({ children, ...props }) => { return ( - +
+
- +
@@ -282,7 +292,7 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf
- +
) const { @@ -313,7 +323,7 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf } = customer return ( - +
- - {customerType && ( -
- {translate('text_1726128938631ioz4orixel3')} - - {translate(TRANSLATIONS_MAP_CUSTOMER_TYPE[customerType])} - -
- )} - {name && ( -
- {translate('text_626162c62f790600f850b76a')} - - {name} - -
- )} - {(firstname || lastname) && ( -
- {translate('text_17261289386311s35rvzyxbz')} - - {firstname} {lastname} - -
- )} -
- {translate('text_6250304370f0f700a8fdc283')} - {externalId} -
- {timezone && ( -
- {translate('text_6390a767b79591bc70ba39f7')} - - {translate('text_638f743fa9a2a9545ee6409a', { - zone: translate(timezone || TimezoneEnum.TzUtc), - offset: getTimezoneConfig(timezone).offset, - })} - -
- )} - {externalSalesforceId && ( -
- {translate('text_651fd42936a03200c126c683')} - {externalSalesforceId} -
- )} - {currency && ( -
- {translate('text_632b4acf0c41206cbcb8c324')} - {currency} -
- )} - {legalName && ( -
- {translate('text_626c0c301a16a600ea061471')} - {legalName} -
- )} - {legalNumber && ( -
- {translate('text_626c0c301a16a600ea061475')} - {legalNumber} -
- )} - {taxIdentificationNumber && ( -
- {translate('text_648053ee819b60364c675d05')} - {taxIdentificationNumber} -
- )} - {email && ( -
- {translate('text_626c0c301a16a600ea061479')} - {email.split(',').join(', ')} -
- )} - {url && ( -
- {translate('text_641b164cff8497006bcbd2b3')} - {url} -
- )} - {phone && ( -
- {translate('text_626c0c301a16a600ea06147d')} - {phone} -
- )} - {(addressLine1 || addressLine2 || state || country || city || zipcode) && ( -
- {translate('text_626c0c301a16a600ea06148d')} - {addressLine1} - {addressLine2} - - {zipcode} {city} {state} - - {country && {CountryCodes[country]}} -
- )} - {shippingAddress && - (shippingAddress.addressLine1 || - shippingAddress.addressLine2 || - shippingAddress.state || - shippingAddress.country || - shippingAddress.city || - shippingAddress.zipcode) && ( -
+
+ + {customerType && ( + + + {translate('text_1726128938631ioz4orixel3')} + + + {translate(TRANSLATIONS_MAP_CUSTOMER_TYPE[customerType])} + + + )} + {name && ( + + + {translate('text_626162c62f790600f850b76a')} + + + {name} + + + )} + {(firstname || lastname) && ( + + + {translate('text_17261289386311s35rvzyxbz')} + + + {firstname} {lastname} + + + )} + + {translate('text_6250304370f0f700a8fdc283')} + {externalId} + + {timezone && ( + - {translate('text_667d708c1359b49f5a5a822a')} + {translate('text_6390a767b79591bc70ba39f7')} - {shippingAddress.addressLine1} - {shippingAddress.addressLine2} - {shippingAddress.zipcode} {shippingAddress.city} {shippingAddress.state} + {translate('text_638f743fa9a2a9545ee6409a', { + zone: translate(timezone || TimezoneEnum.TzUtc), + offset: getTimezoneConfig(timezone).offset, + })} + + + )} + {externalSalesforceId && ( + + + {translate('text_651fd42936a03200c126c683')} + + {externalSalesforceId} + + )} + + + + {currency && ( + + + {translate('text_632b4acf0c41206cbcb8c324')} + + {currency} + + )} + {legalName && ( + + + {translate('text_626c0c301a16a600ea061471')} + + {legalName} + + )} + {legalNumber && ( + + + {translate('text_626c0c301a16a600ea061475')} + + {legalNumber} + + )} + {taxIdentificationNumber && ( + + + {translate('text_648053ee819b60364c675d05')} - {shippingAddress.country && ( + {taxIdentificationNumber} + + )} + {email && ( + + + {translate('text_626c0c301a16a600ea061479')} + + {email.split(',').join(', ')} + + )} + {url && ( + + + {translate('text_641b164cff8497006bcbd2b3')} + + {url} + + )} + {phone && ( + + + {translate('text_626c0c301a16a600ea06147d')} + + {phone} + + )} + {(addressLine1 || addressLine2 || state || country || city || zipcode) && ( + + + {translate('text_626c0c301a16a600ea06148d')} + +
+ {addressLine1} + {addressLine2} - {CountryCodes[shippingAddress.country]} + {zipcode} {city} {state} - )} -
+ {country && {CountryCodes[country]}} +
+ )} - {!!paymentProvider && !!linkedProvider?.name && ( -
- {translate('text_62b1edddbf5f461ab9712795')} - - - {paymentProvider === ProviderTypeEnum?.Stripe ? ( - - ) : paymentProvider === ProviderTypeEnum?.Gocardless ? ( - - ) : paymentProvider === ProviderTypeEnum?.Adyen ? ( - - ) : paymentProvider === ProviderTypeEnum?.Cashfree ? ( - - ) : null} - - {linkedProvider?.name} - - {!!providerCustomer && !!providerCustomer?.providerCustomerId && ( - <> - {paymentProvider === ProviderTypeEnum?.Stripe ? ( - - - {providerCustomer?.providerCustomerId} - - - ) : ( + {shippingAddress && + (shippingAddress.addressLine1 || + shippingAddress.addressLine2 || + shippingAddress.state || + shippingAddress.country || + shippingAddress.city || + shippingAddress.zipcode) && ( + + + {translate('text_667d708c1359b49f5a5a822a')} + + +
+ {shippingAddress.addressLine1} + {shippingAddress.addressLine2} - {providerCustomer?.providerCustomerId} + {shippingAddress.zipcode} {shippingAddress.city} {shippingAddress.state} - )} - - )} - {paymentProvider === ProviderTypeEnum?.Stripe && - !!providerCustomer?.providerPaymentMethods?.length && ( - <> - {providerCustomer?.providerPaymentMethods?.map((method) => ( - - {translate(PaymentProviderMethodTranslationsLookup[method])} + {shippingAddress.country && ( + + {CountryCodes[shippingAddress.country]} - ))} - - )} -
- )} - - {(!!customer?.netsuiteCustomer || !!connectedNetsuiteIntegration?.id) && ( -
- {translate('text_66423cad72bbad009f2f568f')} - {integrationsLoading ? ( - - - - - ) : !!connectedNetsuiteIntegration && customer?.netsuiteCustomer?.externalCustomerId ? ( - - - - - - {connectedNetsuiteIntegration?.name} - - - - {customer?.netsuiteCustomer?.externalCustomerId} - - - - ) : null} -
- )} - - {(!!customer?.xeroCustomer || !!connectedXeroIntegration?.id) && ( -
- {translate('text_66423cad72bbad009f2f568f')} - {integrationsLoading ? ( - - - - - ) : !!connectedXeroIntegration && customer?.xeroCustomer?.externalCustomerId ? ( - - - - - - {connectedXeroIntegration?.name} - - - - {customer?.xeroCustomer?.externalCustomerId} - - - - ) : null} -
- )} - - {!!connectedAnrokIntegration && ( -
- {translate('text_6668821d94e4da4dfd8b3840')} - {integrationsLoading ? ( - - - - - ) : !!connectedAnrokIntegration && customer?.anrokCustomer?.integrationId ? ( - +
+
+ )} + + + + {!!metadata?.length && + metadata.map((meta) => ( + + + {meta.key} + + + {meta.value} + + + ))} + + + + {!!paymentProvider && !!linkedProvider?.name && ( + + + {translate('text_62b1edddbf5f461ab9712795')} + +
- + {paymentProvider === ProviderTypeEnum?.Stripe ? ( + + ) : paymentProvider === ProviderTypeEnum?.Gocardless ? ( + + ) : paymentProvider === ProviderTypeEnum?.Adyen ? ( + + ) : paymentProvider === ProviderTypeEnum?.Cashfree ? ( + + ) : null} - {connectedAnrokIntegration?.name} + {linkedProvider?.name} - {!!connectedAnrokIntegration.externalAccountId && - customer?.anrokCustomer?.externalCustomerId && ( - - - {customer?.anrokCustomer?.externalCustomerId} + {!!providerCustomer && !!providerCustomer?.providerCustomerId && ( + <> + {paymentProvider === ProviderTypeEnum?.Stripe ? ( + + + {providerCustomer?.providerCustomerId} + + + ) : ( + + {providerCustomer?.providerCustomerId} - - )} - - ) : null} -
- )} - - {!!connectedHubspotIntegration && ( -
- {translate('text_1728658962985xpfdvl5ru8a')} - {integrationsLoading ? ( - - - - - ) : !!connectedHubspotIntegration && - customer?.hubspotCustomer?.integrationId && - customer?.hubspotCustomer.targetedObject ? ( - - - - - - {connectedHubspotIntegration?.name} - - - {translate( - getTargetedObjectTranslationKey[customer?.hubspotCustomer.targetedObject], + )} + + )} + {paymentProvider === ProviderTypeEnum?.Stripe && + !!providerCustomer?.providerPaymentMethods?.length && ( + <> + {providerCustomer?.providerPaymentMethods?.map((method) => ( + + {translate(PaymentProviderMethodTranslationsLookup[method])} + + ))} + )} - - {!!connectedHubspotIntegration.portalId && - customer?.hubspotCustomer?.externalCustomerId && - !!customer?.hubspotCustomer.targetedObject && ( +
+
+ )} + + {(!!customer?.netsuiteCustomer || !!connectedNetsuiteIntegration?.id) && ( + + + {translate('text_66423cad72bbad009f2f568f')} + + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedNetsuiteIntegration && + customer?.netsuiteCustomer?.externalCustomerId ? ( + + + + + + {connectedNetsuiteIntegration?.name} + - - {customer?.hubspotCustomer?.externalCustomerId} + + {customer?.netsuiteCustomer?.externalCustomerId} - )} - - ) : null} -
- )} - - {!!connectedSalesforceIntegration && ( -
- {translate('text_1728658962985xpfdvl5ru8a')} - {integrationsLoading ? ( - - - - - ) : !!connectedSalesforceIntegration && customer?.salesforceCustomer?.integrationId ? ( - - - - - - {connectedSalesforceIntegration?.name} - - {!!connectedSalesforceIntegration.instanceId && - customer?.salesforceCustomer?.externalCustomerId && ( + + ) : null} +
+
+ )} + + {(!!customer?.xeroCustomer || !!connectedXeroIntegration?.id) && ( + + + {translate('text_66423cad72bbad009f2f568f')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedXeroIntegration && customer?.xeroCustomer?.externalCustomerId ? ( + + + + + + {connectedXeroIntegration?.name} + - {customer?.salesforceCustomer?.externalCustomerId} + {customer?.xeroCustomer?.externalCustomerId} - )} - - ) : null} -
- )} - - {!!metadata?.length && - metadata.map((meta) => ( -
- - {meta.key} - - - {meta.value} - -
- ))} - - - ) -} - -const LoadingDetails = styled.div` - > *:first-child { - margin-bottom: ${theme.spacing(8)}; - } + + ) : null} +
+ + )} - > *:not(:first-child) { - margin-bottom: ${theme.spacing(7)}; - } -` + {!!connectedAnrokIntegration && ( + + + {translate('text_6668821d94e4da4dfd8b3840')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedAnrokIntegration && customer?.anrokCustomer?.integrationId ? ( + + + + + + {connectedAnrokIntegration?.name} + + {!!connectedAnrokIntegration.externalAccountId && + customer?.anrokCustomer?.externalCustomerId && ( + + + {customer?.anrokCustomer?.externalCustomerId} + + + )} + + ) : null} +
+
+ )} -const DetailsBlock = styled.div` - > *:not(:first-child) { - margin-bottom: ${theme.spacing(3)}; - } -` + {!!connectedHubspotIntegration && ( + + + {translate('text_1728658962985xpfdvl5ru8a')} + -const InfosBlock = styled.div` - > *:not(:last-child) { - margin-bottom: ${theme.spacing(3)}; - } -` +
+ {integrationsLoading ? ( + + + + + ) : !!connectedHubspotIntegration && + customer?.hubspotCustomer?.integrationId && + customer?.hubspotCustomer.targetedObject ? ( + + + + + + {connectedHubspotIntegration?.name} + + + {translate( + getTargetedObjectTranslationKey[customer?.hubspotCustomer.targetedObject], + )} + + {!!connectedHubspotIntegration.portalId && + customer?.hubspotCustomer?.externalCustomerId && + !!customer?.hubspotCustomer.targetedObject && ( + + + {customer?.hubspotCustomer?.externalCustomerId} + + + )} + + ) : null} +
+
+ )} -const SectionHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: ${theme.spacing(4)}; -` + {!!connectedSalesforceIntegration && ( + + + {translate('text_1728658962985xpfdvl5ru8a')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedSalesforceIntegration && + customer?.salesforceCustomer?.integrationId ? ( + + + + + + + {connectedSalesforceIntegration?.name} + + + {!!connectedSalesforceIntegration.instanceId && + customer?.salesforceCustomer?.externalCustomerId && ( + + + {customer?.salesforceCustomer?.externalCustomerId}{' '} + + + + )} + + ) : null} +
+
+ )} + +
+
+ ) +} From 2b188daba11f5b47ad0e60835ece1d0605d1f004 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:40:21 +0200 Subject: [PATCH 47/64] refactor(CustomerMainInfos): render metadata and connections conditionally --- .../customers/CustomerMainInfos.tsx | 518 +++++++++--------- 1 file changed, 270 insertions(+), 248 deletions(-) diff --git a/src/components/customers/CustomerMainInfos.tsx b/src/components/customers/CustomerMainInfos.tsx index 1f9d66955..791e8852d 100644 --- a/src/components/customers/CustomerMainInfos.tsx +++ b/src/components/customers/CustomerMainInfos.tsx @@ -322,6 +322,16 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf metadata, } = customer + const hasExternalIntegration = + connectedSalesforceIntegration || + connectedHubspotIntegration || + connectedAnrokIntegration || + !!customer?.xeroCustomer || + !!connectedXeroIntegration?.id || + !!customer?.netsuiteCustomer || + !!connectedNetsuiteIntegration?.id || + (paymentProvider && !!linkedProvider?.name) + return (
- - {!!metadata?.length && - metadata.map((meta) => ( + {!!metadata?.length && ( + + {metadata.map((meta) => ( {meta.key} @@ -504,269 +514,281 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf ))} - + + )} - - {!!paymentProvider && !!linkedProvider?.name && ( - - - {translate('text_62b1edddbf5f461ab9712795')} - -
- - - {paymentProvider === ProviderTypeEnum?.Stripe ? ( - - ) : paymentProvider === ProviderTypeEnum?.Gocardless ? ( - - ) : paymentProvider === ProviderTypeEnum?.Adyen ? ( - - ) : paymentProvider === ProviderTypeEnum?.Cashfree ? ( - - ) : null} - - {linkedProvider?.name} - - {!!providerCustomer && !!providerCustomer?.providerCustomerId && ( - <> - {paymentProvider === ProviderTypeEnum?.Stripe ? ( + {hasExternalIntegration && ( + + {!!paymentProvider && !!linkedProvider?.name && ( + + + {translate('text_62b1edddbf5f461ab9712795')} + +
+ + + {paymentProvider === ProviderTypeEnum?.Stripe ? ( + + ) : paymentProvider === ProviderTypeEnum?.Gocardless ? ( + + ) : paymentProvider === ProviderTypeEnum?.Adyen ? ( + + ) : paymentProvider === ProviderTypeEnum?.Cashfree ? ( + + ) : null} + + {linkedProvider?.name} + + {!!providerCustomer && !!providerCustomer?.providerCustomerId && ( + <> + {paymentProvider === ProviderTypeEnum?.Stripe ? ( + + + {providerCustomer?.providerCustomerId} + + + ) : ( + + {providerCustomer?.providerCustomerId} + + )} + + )} + {paymentProvider === ProviderTypeEnum?.Stripe && + !!providerCustomer?.providerPaymentMethods?.length && ( + <> + {providerCustomer?.providerPaymentMethods?.map((method) => ( + + {translate(PaymentProviderMethodTranslationsLookup[method])} + + ))} + + )} +
+
+ )} + + {(!!customer?.netsuiteCustomer || !!connectedNetsuiteIntegration?.id) && ( + + + {translate('text_66423cad72bbad009f2f568f')} + + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedNetsuiteIntegration && + customer?.netsuiteCustomer?.externalCustomerId ? ( + + + + + + + {connectedNetsuiteIntegration?.name} + + - {providerCustomer?.providerCustomerId} + {customer?.netsuiteCustomer?.externalCustomerId} - ) : ( - - {providerCustomer?.providerCustomerId} - - )} - - )} - {paymentProvider === ProviderTypeEnum?.Stripe && - !!providerCustomer?.providerPaymentMethods?.length && ( - <> - {providerCustomer?.providerPaymentMethods?.map((method) => ( - - {translate(PaymentProviderMethodTranslationsLookup[method])} - - ))} - - )} -
-
- )} - - {(!!customer?.netsuiteCustomer || !!connectedNetsuiteIntegration?.id) && ( - - - {translate('text_66423cad72bbad009f2f568f')} - - -
- {integrationsLoading ? ( - - - - - ) : !!connectedNetsuiteIntegration && - customer?.netsuiteCustomer?.externalCustomerId ? ( - - - - - - {connectedNetsuiteIntegration?.name} - - - {customer?.netsuiteCustomer?.externalCustomerId} - - - - ) : null} -
-
- )} + ) : null} +
+
+ )} - {(!!customer?.xeroCustomer || !!connectedXeroIntegration?.id) && ( - - - {translate('text_66423cad72bbad009f2f568f')} - -
- {integrationsLoading ? ( - - - - - ) : !!connectedXeroIntegration && customer?.xeroCustomer?.externalCustomerId ? ( - - - - - - {connectedXeroIntegration?.name} + {(!!customer?.xeroCustomer || !!connectedXeroIntegration?.id) && ( + + + {translate('text_66423cad72bbad009f2f568f')} + +
+ {integrationsLoading ? ( + + + - - - {customer?.xeroCustomer?.externalCustomerId} - - - - ) : null} -
-
- )} + ) : !!connectedXeroIntegration && customer?.xeroCustomer?.externalCustomerId ? ( + + + + + + {connectedXeroIntegration?.name} + + + + {customer?.xeroCustomer?.externalCustomerId} + + + + ) : null} +
+
+ )} - {!!connectedAnrokIntegration && ( - - - {translate('text_6668821d94e4da4dfd8b3840')} - -
- {integrationsLoading ? ( - - - - - ) : !!connectedAnrokIntegration && customer?.anrokCustomer?.integrationId ? ( - - - - - - {connectedAnrokIntegration?.name} + {!!connectedAnrokIntegration && ( + + + {translate('text_6668821d94e4da4dfd8b3840')} + +
+ {integrationsLoading ? ( + + + - {!!connectedAnrokIntegration.externalAccountId && - customer?.anrokCustomer?.externalCustomerId && ( - - - {customer?.anrokCustomer?.externalCustomerId} - - - )} - - ) : null} -
-
- )} + ) : !!connectedAnrokIntegration && customer?.anrokCustomer?.integrationId ? ( + + + + + + {connectedAnrokIntegration?.name} + + {!!connectedAnrokIntegration.externalAccountId && + customer?.anrokCustomer?.externalCustomerId && ( + + + {customer?.anrokCustomer?.externalCustomerId} + + + )} + + ) : null} +
+
+ )} - {!!connectedHubspotIntegration && ( - - - {translate('text_1728658962985xpfdvl5ru8a')} - + {!!connectedHubspotIntegration && ( + + + {translate('text_1728658962985xpfdvl5ru8a')} + -
- {integrationsLoading ? ( - - - - - ) : !!connectedHubspotIntegration && - customer?.hubspotCustomer?.integrationId && - customer?.hubspotCustomer.targetedObject ? ( - - - - - - {connectedHubspotIntegration?.name} +
+ {integrationsLoading ? ( + + + - - {translate( - getTargetedObjectTranslationKey[customer?.hubspotCustomer.targetedObject], - )} - - {!!connectedHubspotIntegration.portalId && - customer?.hubspotCustomer?.externalCustomerId && - !!customer?.hubspotCustomer.targetedObject && ( - - - {customer?.hubspotCustomer?.externalCustomerId} - - - )} - - ) : null} -
- - )} - - {!!connectedSalesforceIntegration && ( - - - {translate('text_1728658962985xpfdvl5ru8a')} - -
- {integrationsLoading ? ( - - - - - ) : !!connectedSalesforceIntegration && - customer?.salesforceCustomer?.integrationId ? ( - - - - - - - {connectedSalesforceIntegration?.name} + ) : !!connectedHubspotIntegration && + customer?.hubspotCustomer?.integrationId && + customer?.hubspotCustomer.targetedObject ? ( + + + + + + {connectedHubspotIntegration?.name} + + + {translate( + getTargetedObjectTranslationKey[customer?.hubspotCustomer.targetedObject], + )} + {!!connectedHubspotIntegration.portalId && + customer?.hubspotCustomer?.externalCustomerId && + !!customer?.hubspotCustomer.targetedObject && ( + + + {customer?.hubspotCustomer?.externalCustomerId}{' '} + + + + )} - {!!connectedSalesforceIntegration.instanceId && - customer?.salesforceCustomer?.externalCustomerId && ( - - - {customer?.salesforceCustomer?.externalCustomerId}{' '} - - - - )} - - ) : null} -
-
- )} - + ) : null} +
+
+ )} + + {!!connectedSalesforceIntegration && ( + + + {translate('text_1728658962985xpfdvl5ru8a')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedSalesforceIntegration && + customer?.salesforceCustomer?.integrationId ? ( + + + + + + + {connectedSalesforceIntegration?.name} + + + {!!connectedSalesforceIntegration.instanceId && + customer?.salesforceCustomer?.externalCustomerId && ( + + + {customer?.salesforceCustomer?.externalCustomerId}{' '} + + + + )} + + ) : null} +
+
+ )} +
+ )}
) From 3c6d145b717b90d954c08ac3b4cd7db319e877e7 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:43:45 +0200 Subject: [PATCH 48/64] refactor(CustomerMainInfos): do not render border for last element --- src/components/customers/CustomerMainInfos.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/customers/CustomerMainInfos.tsx b/src/components/customers/CustomerMainInfos.tsx index 791e8852d..fc85a74e4 100644 --- a/src/components/customers/CustomerMainInfos.tsx +++ b/src/components/customers/CustomerMainInfos.tsx @@ -195,7 +195,7 @@ interface CustomerMainInfosProps { } const InfoSection = ({ title, children }: { title: string; children: React.ReactNode }) => ( -
+
{title} {children} From a682fe578b621968233088d866e24059cf43ad1a Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:59:28 +0200 Subject: [PATCH 49/64] chore(translations): add translations --- translations/base.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/translations/base.json b/translations/base.json index 439c80060..b4a7aba38 100644 --- a/translations/base.json +++ b/translations/base.json @@ -2802,6 +2802,8 @@ "text_1737892224509yezgypqk5vp": "Partner information", "text_17378922245103cc9xrd1tjj": "Billing information", "text_1737892224510vc53d10q4h5": "Metadata", - "text_1737892224510jnd7cbdp2yg": "External connections" + "text_1737892224510jnd7cbdp2yg": "External connections", + "text_1737895765672pwk47419syk": "Total remaining credits from credited notes. This amount will affect future invoices.", + "text_1737895837105yr0kl7kkyuz": "List of credited notes. These amounts may impact future invoices; consider voiding the credits if needed." } From f4dba152f900a9e8e83680eaebb229971dcd0a54 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 14:59:53 +0200 Subject: [PATCH 50/64] feat(CreditNotesTable): add filtersContainerClassName prop --- .../creditNote/CreditNotesTable.tsx | 344 +++++++++--------- 1 file changed, 170 insertions(+), 174 deletions(-) diff --git a/src/components/creditNote/CreditNotesTable.tsx b/src/components/creditNote/CreditNotesTable.tsx index 2bb6776ab..2909216ce 100644 --- a/src/components/creditNote/CreditNotesTable.tsx +++ b/src/components/creditNote/CreditNotesTable.tsx @@ -29,6 +29,7 @@ import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' import { usePermissions } from '~/hooks/usePermissions' import EmptyImage from '~/public/images/maneki/empty.svg' import { BaseListItem, NAV_HEIGHT, theme } from '~/styles' +import { tw } from '~/styles/utils' import { VoidCreditNoteDialog, @@ -110,6 +111,7 @@ type TCreditNoteTableProps = { customerTimezone?: TimezoneEnum tableContainerSize?: ResponsiveStyleValue showFilters?: boolean + filtersContainerClassName?: string } const CreditNoteTableItemSkeleton = () => { @@ -135,6 +137,7 @@ const CreditNotesTable = ({ error, tableContainerSize, showFilters = true, + filtersContainerClassName, }: TCreditNoteTableProps) => { const { translate } = useInternationalization() const navigate = useNavigate() @@ -177,7 +180,12 @@ const CreditNotesTable = ({ return ( <> {showFilters && ( -
+
)} - -
- {isLoading && !!variables?.searchTerm ? ( - <> - {[1, 2, 3, 4].map((i) => ( - - ))} - - ) : !isLoading && !!variables?.searchTerm && !creditNotes?.length ? ( - } - /> - ) : ( - { - const { currentPage = 0, totalPages = 0 } = metadata || {} +
+ {isLoading && !!variables?.searchTerm ? ( + <> + {[1, 2, 3, 4].map((i) => ( + + ))} + + ) : !isLoading && !!variables?.searchTerm && !creditNotes?.length ? ( + } + /> + ) : ( + { + const { currentPage = 0, totalPages = 0 } = metadata || {} - currentPage < totalPages && - !isLoading && - fetchMore({ - variables: { page: currentPage + 1 }, - }) - }} - > -
- translate( - creditNote.canBeVoided && hasPermissions(['creditNotesVoid']) - ? 'text_63728c6434e1344aea76347d' - : 'text_63728c6434e1344aea76347f', - ) + currentPage < totalPages && + !isLoading && + fetchMore({ + variables: { page: currentPage + 1 }, + }) + }} + > +
{ - let actions: ActionItem[] = [] - - const canDownload = hasPermissions(['creditNotesView']) - const canVoid = creditNote.canBeVoided && hasPermissions(['creditNotesVoid']) - - if (canDownload) { - actions = [ - ...actions, - { - title: translate('text_636d12ce54c41fccdf0ef72d'), - disabled: loadingCreditNoteDownload, - onAction: async ({ id }: { id: string }) => { - await downloadCreditNote({ - variables: { input: { id } }, - }) - }, - }, - ] - } + } + isLoading={isLoading} + hasError={!!error} + placeholder={{ + emptyState: { + title: translate('text_6663014df0a6be0098264dd9'), + subtitle: translate('text_6663014df0a6be0098264dda'), + }, + }} + actionColumnTooltip={(creditNote) => + translate( + creditNote.canBeVoided && hasPermissions(['creditNotesVoid']) + ? 'text_63728c6434e1344aea76347d' + : 'text_63728c6434e1344aea76347f', + ) + } + actionColumn={(creditNote) => { + let actions: ActionItem[] = [] - if (canVoid) { - actions = [ - ...actions, - { - title: translate('text_636d12ce54c41fccdf0ef72f'), - onAction: async ({ id, totalAmountCents, currency }) => { - voidCreditNoteDialogRef.current?.openDialog({ - id, - totalAmountCents, - currency, - }) - }, - }, - ] - } + const canDownload = hasPermissions(['creditNotesView']) + const canVoid = creditNote.canBeVoided && hasPermissions(['creditNotesVoid']) + if (canDownload) { actions = [ ...actions, { - title: translate('text_636d12ce54c41fccdf0ef731'), + title: translate('text_636d12ce54c41fccdf0ef72d'), + disabled: loadingCreditNoteDownload, onAction: async ({ id }: { id: string }) => { - copyToClipboard(id) - - addToast({ - severity: 'info', - translateKey: 'text_63720bd734e1344aea75b82d', + await downloadCreditNote({ + variables: { input: { id } }, }) }, }, ] - - return actions - }} - onRowActionLink={(creditNote) => - generatePath(CUSTOMER_INVOICE_CREDIT_NOTE_DETAILS_ROUTE, { - customerId: creditNote?.invoice?.customer?.id as string, - invoiceId: creditNote?.invoice?.id as string, - creditNoteId: creditNote?.id as string, - }) } - columns={[ - { - key: 'totalAmountCents', - title: translate('text_1727078012568v9460bmnh8a'), - content: (creditNote) => , - }, - { - key: 'number', - title: translate('text_64188b3d9735d5007d71227f'), - minWidth: 160, - content: ({ number }) => ( - - {number} - - ), - }, - { - key: 'totalAmountCents', - title: translate('text_62544c1db13ca10187214d85'), - content: ({ totalAmountCents, currency }) => ( - - {intlFormatNumber(deserializeAmount(totalAmountCents || 0, currency), { - currencyDisplay: 'symbol', + + if (canVoid) { + actions = [ + ...actions, + { + title: translate('text_636d12ce54c41fccdf0ef72f'), + onAction: async ({ id, totalAmountCents, currency }) => { + voidCreditNoteDialogRef.current?.openDialog({ + id, + totalAmountCents, currency, - })} - - ), - maxSpace: !showCustomerName, - textAlign: 'right', - }, - ...(showCustomerName - ? [ - { - key: 'invoice.customer.displayName', - title: translate('text_63ac86d797f728a87b2f9fb3'), - content: (creditNote: CreditNoteTableItemFragment) => ( - - {creditNote.invoice?.customer.displayName} - - ), - maxSpace: true, - tdCellClassName: 'hidden md:table-cell', - } as TableColumn, - ] - : []), + }) + }, + }, + ] + } + + actions = [ + ...actions, { - key: 'createdAt', - title: translate('text_62544c1db13ca10187214d7f'), - content: ({ createdAt }) => ( - - {customerTimezone - ? formatDateToTZ(createdAt, customerTimezone) - : formatTimeOrgaTZ(createdAt)} - - ), + title: translate('text_636d12ce54c41fccdf0ef731'), + onAction: async ({ id }: { id: string }) => { + copyToClipboard(id) + + addToast({ + severity: 'info', + translateKey: 'text_63720bd734e1344aea75b82d', + }) + }, }, - ]} - /> - - )} - + ] - - + return actions + }} + onRowActionLink={(creditNote) => + generatePath(CUSTOMER_INVOICE_CREDIT_NOTE_DETAILS_ROUTE, { + customerId: creditNote?.invoice?.customer?.id as string, + invoiceId: creditNote?.invoice?.id as string, + creditNoteId: creditNote?.id as string, + }) + } + columns={[ + { + key: 'totalAmountCents', + title: translate('text_1727078012568v9460bmnh8a'), + content: (creditNote) => , + }, + { + key: 'number', + title: translate('text_64188b3d9735d5007d71227f'), + minWidth: 160, + content: ({ number }) => ( + + {number} + + ), + }, + { + key: 'totalAmountCents', + title: translate('text_62544c1db13ca10187214d85'), + content: ({ totalAmountCents, currency }) => ( + + {intlFormatNumber(deserializeAmount(totalAmountCents || 0, currency), { + currencyDisplay: 'symbol', + currency, + })} + + ), + maxSpace: !showCustomerName, + textAlign: 'right', + }, + ...(showCustomerName + ? [ + { + key: 'invoice.customer.displayName', + title: translate('text_63ac86d797f728a87b2f9fb3'), + content: (creditNote: CreditNoteTableItemFragment) => ( + + {creditNote.invoice?.customer.displayName} + + ), + maxSpace: true, + tdCellClassName: 'hidden md:table-cell', + } as TableColumn, + ] + : []), + { + key: 'createdAt', + title: translate('text_62544c1db13ca10187214d7f'), + content: ({ createdAt }) => ( + + {customerTimezone + ? formatDateToTZ(createdAt, customerTimezone) + : formatTimeOrgaTZ(createdAt)} + + ), + }, + ]} + /> + + )} + + + ) } export default CreditNotesTable -const ScrollContainer = styled.div` - overflow: auto; - height: calc(100vh - ${NAV_HEIGHT * 2}px); -` - const CreditNotesTableItemGridTemplate = () => css` display: grid; grid-template-columns: minmax(200px, auto) minmax(160px, auto) 1fr 1fr 112px 40px; From 3a5147d8505e1ea150b60f63d8396c2a2d636036 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 15:00:23 +0200 Subject: [PATCH 51/64] fix(CustomerCreditNotesList): remove legacy ScrollContainer --- .../customers/CustomerCreditNotesList.tsx | 126 ++++++++++-------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/components/customers/CustomerCreditNotesList.tsx b/src/components/customers/CustomerCreditNotesList.tsx index 9e169713b..ced3b26f9 100644 --- a/src/components/customers/CustomerCreditNotesList.tsx +++ b/src/components/customers/CustomerCreditNotesList.tsx @@ -3,6 +3,7 @@ import { gql } from '@apollo/client' import CreditNotesTable from '~/components/creditNote/CreditNotesTable' import { Avatar, Icon, Typography } from '~/components/designSystem' import { GenericPlaceholder } from '~/components/GenericPlaceholder' +import { PageSectionTitle } from '~/components/layouts/Section' import { intlFormatNumber } from '~/core/formats/intlFormatNumber' import { deserializeAmount } from '~/core/serializers/serializeAmount' import { @@ -52,70 +53,79 @@ export const CustomerCreditNotesList = ({ const creditNotes = data?.creditNotes?.collection return ( -
- - {translate('text_63725b30957fd5b26b308dd7')} - -
-
- - - -
- - {translate('text_63725b30957fd5b26b308dd9')} - - - {translate('text_63725b30957fd5b26b308ddb', { - count: creditNotesCreditsAvailableCount, - })} - +
+
+ + +
+
+ + + +
+ + {translate('text_63725b30957fd5b26b308dd9')} + + + {translate('text_63725b30957fd5b26b308ddb', { + count: creditNotesCreditsAvailableCount, + })} + +
+ + {intlFormatNumber( + deserializeAmount( + creditNotesBalanceAmountCents || 0, + userCurrency || CurrencyEnum.Usd, + ) || 0, + { + currencyDisplay: 'symbol', + currency: userCurrency, + }, + )} +
- - {intlFormatNumber( - deserializeAmount( - creditNotesBalanceAmountCents || 0, - userCurrency || CurrencyEnum.Usd, - ) || 0, - { - currencyDisplay: 'symbol', - currency: userCurrency, - }, - )} -
-
- - {translate('text_63725b30957fd5b26b308ddf')} - - + + } /> + + {!!error && !isLoading ? ( + location.reload()} + image={} + /> + ) : ( + + )}
- {!!error && !isLoading ? ( - location.reload()} - image={} - /> - ) : ( - - )}
) } From 9095a02c19162c3103b7725e5e82dfba61f91aaa Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 15:07:34 +0200 Subject: [PATCH 52/64] refactor(CreditNotesTable): remove legacy onKeyDown --- .../creditNote/CreditNotesTable.tsx | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/src/components/creditNote/CreditNotesTable.tsx b/src/components/creditNote/CreditNotesTable.tsx index 2909216ce..99aa03d4a 100644 --- a/src/components/creditNote/CreditNotesTable.tsx +++ b/src/components/creditNote/CreditNotesTable.tsx @@ -1,16 +1,13 @@ import { ApolloError, gql, LazyQueryHookOptions } from '@apollo/client' import { useRef } from 'react' -import { generatePath, useNavigate } from 'react-router-dom' +import { generatePath } from 'react-router-dom' import styled, { css } from 'styled-components' import CreditNoteBadge from '~/components/creditNote/CreditNoteBadge' import { AvailableFiltersEnum, Filters } from '~/components/designSystem/Filters' import { addToast } from '~/core/apolloClient' import { intlFormatNumber } from '~/core/formats/intlFormatNumber' -import { - CUSTOMER_CREDIT_NOTE_DETAILS_ROUTE, - CUSTOMER_INVOICE_CREDIT_NOTE_DETAILS_ROUTE, -} from '~/core/router' +import { CUSTOMER_INVOICE_CREDIT_NOTE_DETAILS_ROUTE } from '~/core/router' import { deserializeAmount } from '~/core/serializers/serializeAmount' import { formatDateToTZ } from '~/core/timezone' import { copyToClipboard } from '~/core/utils/copyToClipboard' @@ -24,11 +21,10 @@ import { useDownloadCreditNoteMutation, } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' -import { useListKeysNavigation } from '~/hooks/ui/useListKeyNavigation' import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' import { usePermissions } from '~/hooks/usePermissions' import EmptyImage from '~/public/images/maneki/empty.svg' -import { BaseListItem, NAV_HEIGHT, theme } from '~/styles' +import { BaseListItem, theme } from '~/styles' import { tw } from '~/styles/utils' import { @@ -97,10 +93,6 @@ gql` ${CreditNoteForVoidCreditNoteDialogFragmentDoc} ` -// Needed to be able to pass both ids to the keyboard navigation function -const ID_SPLIT_KEY = '&-%-&' -const NAVIGATION_KEY_BASE = 'creditNote-item-' - type TCreditNoteTableProps = { creditNotes: GetCreditNotesListQuery['creditNotes']['collection'] | undefined error: ApolloError | undefined @@ -140,14 +132,10 @@ const CreditNotesTable = ({ filtersContainerClassName, }: TCreditNoteTableProps) => { const { translate } = useInternationalization() - const navigate = useNavigate() const voidCreditNoteDialogRef = useRef(null) - const listContainerElementRef = useRef(null) const { formatTimeOrgaTZ } = useOrganizationInfos() const { hasPermissions } = usePermissions() - const isCustomer = !!customerTimezone - const [downloadCreditNote, { loading: loadingCreditNoteDownload }] = useDownloadCreditNoteMutation({ onCompleted({ downloadCreditNote: data }) { @@ -155,26 +143,6 @@ const CreditNotesTable = ({ }, }) - const { onKeyDown } = useListKeysNavigation({ - getElmId: (i) => `${NAVIGATION_KEY_BASE}${i}`, - navigate: (id) => { - const [customerId, invoiceId, creditNoteId] = String(id).split(ID_SPLIT_KEY) - - navigate( - generatePath( - isCustomer - ? CUSTOMER_CREDIT_NOTE_DETAILS_ROUTE - : CUSTOMER_INVOICE_CREDIT_NOTE_DETAILS_ROUTE, - { - customerId, - invoiceId, - creditNoteId, - }, - ), - ) - }, - }) - const showCustomerName = !customerTimezone return ( From ae53e956d4b7c90817202675bb929ef698716b47 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 15:07:44 +0200 Subject: [PATCH 53/64] refactor(CustomerCreditNotesList): remove unused import --- src/components/customers/CustomerCreditNotesList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/customers/CustomerCreditNotesList.tsx b/src/components/customers/CustomerCreditNotesList.tsx index ced3b26f9..59d92b46f 100644 --- a/src/components/customers/CustomerCreditNotesList.tsx +++ b/src/components/customers/CustomerCreditNotesList.tsx @@ -15,7 +15,6 @@ import { import { useInternationalization } from '~/hooks/core/useInternationalization' import { useDebouncedSearch } from '~/hooks/useDebouncedSearch' import ErrorImage from '~/public/images/maneki/error.svg' -import { SectionHeader } from '~/styles/customer' import { SearchInput } from '../SearchInput' From 52c09f206e139115a12cac54f3cc371d1d36c7be Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Sun, 26 Jan 2025 15:25:11 +0200 Subject: [PATCH 54/64] fix(e2e): add missing data-test attributes --- src/components/customers/overview/CustomerCoupons.tsx | 1 + src/components/customers/overview/CustomerSubscriptionsList.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/components/customers/overview/CustomerCoupons.tsx b/src/components/customers/overview/CustomerCoupons.tsx index 7ae31a7d4..dfc6500e1 100644 --- a/src/components/customers/overview/CustomerCoupons.tsx +++ b/src/components/customers/overview/CustomerCoupons.tsx @@ -99,6 +99,7 @@ export const CustomerCoupons = memo(() => { data={coupons || []} containerSize={4} isLoading={loading} + rowDataTestId={(coupon) => coupon.coupon?.name} columns={[ { key: 'coupon.name', diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index 8f6c0d329..b2043c613 100644 --- a/src/components/customers/overview/CustomerSubscriptionsList.tsx +++ b/src/components/customers/overview/CustomerSubscriptionsList.tsx @@ -265,6 +265,7 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip hasPermissions(['subscriptionsCreate']) ? { title: translate('text_6250304370f0f700a8fdc28b'), + dataTest: 'add-subscription', onClick: () => { navigate( generatePath(CREATE_SUBSCRIPTION, { @@ -290,6 +291,7 @@ export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscrip data={annotatedSubscriptions || []} containerSize={4} isLoading={loading} + rowDataTestId={(subscription) => subscription.name || `subscription-${subscription.id}`} onRowActionLink={({ id }) => generatePath(CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, { customerId: customerId as string, From 49278239b6da12716b3c992c67ffe176fbc50b2e Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Mon, 27 Jan 2025 18:00:05 +0200 Subject: [PATCH 55/64] fix(CustomerSettings): remove bottom padding --- src/components/customers/CustomerSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/customers/CustomerSettings.tsx b/src/components/customers/CustomerSettings.tsx index 498ddb5b8..8a46273a1 100644 --- a/src/components/customers/CustomerSettings.tsx +++ b/src/components/customers/CustomerSettings.tsx @@ -249,7 +249,7 @@ export const CustomerSettings = ({ customerId }: CustomerSettingsProps) => { return ( <> - + {!!loading ? ( From 9b5d9474e80b81de49498382f95cc80f10d48344 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 12:11:14 +0200 Subject: [PATCH 56/64] chore(translations): update translations --- translations/base.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/translations/base.json b/translations/base.json index b4a7aba38..59ba9a085 100644 --- a/translations/base.json +++ b/translations/base.json @@ -2799,11 +2799,10 @@ "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.", - "text_1737892224509yezgypqk5vp": "Partner information", + "text_1737892224509yezgypqk5vp": "Customer information", "text_17378922245103cc9xrd1tjj": "Billing information", "text_1737892224510vc53d10q4h5": "Metadata", "text_1737892224510jnd7cbdp2yg": "External connections", "text_1737895765672pwk47419syk": "Total remaining credits from credited notes. This amount will affect future invoices.", "text_1737895837105yr0kl7kkyuz": "List of credited notes. These amounts may impact future invoices; consider voiding the credits if needed." } - From eea658c42169ce85471eb3ccd5836cc8f6a99a52 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 12:11:35 +0200 Subject: [PATCH 57/64] fix(CustomerMainInfos): render billing information conditionally --- .../customers/CustomerMainInfos.tsx | 202 ++++++++++-------- 1 file changed, 114 insertions(+), 88 deletions(-) diff --git a/src/components/customers/CustomerMainInfos.tsx b/src/components/customers/CustomerMainInfos.tsx index fc85a74e4..5fd5ec61b 100644 --- a/src/components/customers/CustomerMainInfos.tsx +++ b/src/components/customers/CustomerMainInfos.tsx @@ -332,6 +332,28 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf !!connectedNetsuiteIntegration?.id || (paymentProvider && !!linkedProvider?.name) + const hasBillingInformation = + !!currency || + !!legalName || + !!legalNumber || + !!taxIdentificationNumber || + !!email || + !!url || + !!phone || + !!addressLine1 || + !!addressLine2 || + !!state || + !!country || + !!city || + !!zipcode || + (shippingAddress && + (shippingAddress.addressLine1 || + shippingAddress.addressLine2 || + shippingAddress.state || + shippingAddress.country || + shippingAddress.city || + shippingAddress.zipcode)) + return (
- - {currency && ( - - - {translate('text_632b4acf0c41206cbcb8c324')} - - {currency} - - )} - {legalName && ( - - - {translate('text_626c0c301a16a600ea061471')} - - {legalName} - - )} - {legalNumber && ( - - - {translate('text_626c0c301a16a600ea061475')} - - {legalNumber} - - )} - {taxIdentificationNumber && ( - - - {translate('text_648053ee819b60364c675d05')} - - {taxIdentificationNumber} - - )} - {email && ( - - - {translate('text_626c0c301a16a600ea061479')} - - {email.split(',').join(', ')} - - )} - {url && ( - - - {translate('text_641b164cff8497006bcbd2b3')} - - {url} - - )} - {phone && ( - - - {translate('text_626c0c301a16a600ea06147d')} - - {phone} - - )} - {(addressLine1 || addressLine2 || state || country || city || zipcode) && ( - - - {translate('text_626c0c301a16a600ea06148d')} - -
- {addressLine1} - {addressLine2} - - {zipcode} {city} {state} + {hasBillingInformation && ( + + {currency && ( + + + {translate('text_632b4acf0c41206cbcb8c324')} - {country && {CountryCodes[country]}} -
-
- )} - {shippingAddress && - (shippingAddress.addressLine1 || - shippingAddress.addressLine2 || - shippingAddress.state || - shippingAddress.country || - shippingAddress.city || - shippingAddress.zipcode) && ( + {currency} + + )} + {legalName && ( - {translate('text_667d708c1359b49f5a5a822a')} + {translate('text_626c0c301a16a600ea061471')} + + {legalName} + + )} + {legalNumber && ( + + + {translate('text_626c0c301a16a600ea061475')} + + {legalNumber} + + )} + {taxIdentificationNumber && ( + + + {translate('text_648053ee819b60364c675d05')} + + {taxIdentificationNumber} + + )} + {email && ( + + + {translate('text_626c0c301a16a600ea061479')} + + {email.split(',').join(', ')} + + )} + {url && ( + + + {translate('text_641b164cff8497006bcbd2b3')} + + {url} + + )} + {phone && ( + + + {translate('text_626c0c301a16a600ea06147d')} + + {phone} + + )} + {(addressLine1 || addressLine2 || state || country || city || zipcode) && ( + + + {translate('text_626c0c301a16a600ea06148d')} -
- {shippingAddress.addressLine1} - {shippingAddress.addressLine2} + {addressLine1} + {addressLine2} - {shippingAddress.zipcode} {shippingAddress.city} {shippingAddress.state} + {zipcode} {city} {state} - {shippingAddress.country && ( - - {CountryCodes[shippingAddress.country]} - + {country && ( + {CountryCodes[country]} )}
)} -
+ {shippingAddress && + (shippingAddress.addressLine1 || + shippingAddress.addressLine2 || + shippingAddress.state || + shippingAddress.country || + shippingAddress.city || + shippingAddress.zipcode) && ( + + + {translate('text_667d708c1359b49f5a5a822a')} + + +
+ {shippingAddress.addressLine1} + {shippingAddress.addressLine2} + + {shippingAddress.zipcode} {shippingAddress.city} {shippingAddress.state} + + {shippingAddress.country && ( + + {CountryCodes[shippingAddress.country]} + + )} +
+
+ )} + + )} {!!metadata?.length && ( From 37389f9d2f725424295519c0a8af36730f6e450c Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 13:23:49 +0200 Subject: [PATCH 58/64] chore(translations): update translations --- translations/base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/base.json b/translations/base.json index 59ba9a085..d7c828926 100644 --- a/translations/base.json +++ b/translations/base.json @@ -2793,7 +2793,7 @@ "text_1736972452609qdjngeuqsz0": "Downgrade", "text_1736972452609g2v8mzgvi2t": "Scheduled", "text_1737059551511f5acxkfz7p4": "Retrieve all customer details, including connected external apps.", - "text_17376404438209bh9jk7xa2s": "Details", + "text_17376404438209bh9jk7xa2s": "Information", "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.", From ffe87340ed2c89faf377abfa4e1375df2c6625b3 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 13:25:10 +0200 Subject: [PATCH 59/64] fix(navigation): navigate to information tab after save --- src/hooks/useCreateEditCustomer.ts | 19 +++++++++++++------ src/pages/CustomerDetails.tsx | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/hooks/useCreateEditCustomer.ts b/src/hooks/useCreateEditCustomer.ts index cea315f88..e7e7011fa 100644 --- a/src/hooks/useCreateEditCustomer.ts +++ b/src/hooks/useCreateEditCustomer.ts @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { generatePath, useNavigate, useParams } from 'react-router-dom' import { addToast, hasDefinedGQLError } from '~/core/apolloClient' -import { CUSTOMER_DETAILS_ROUTE, CUSTOMERS_LIST_ROUTE, ERROR_404_ROUTE } from '~/core/router' +import { CUSTOMER_DETAILS_TAB_ROUTE, CUSTOMERS_LIST_ROUTE, ERROR_404_ROUTE } from '~/core/router' import { AddCustomerDrawerFragment, CreateCustomerInput, @@ -18,6 +18,7 @@ import { useUpdateCustomerMutation, } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' +import { CustomerDetailsTabsOptions } from '~/pages/CustomerDetails' gql` fragment CustomerForExternalAppsAccordion on Customer { @@ -177,6 +178,14 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { skip: !customerId, }) + const goToCustomerInformationPage = (_customerId: string) => + navigate( + generatePath(CUSTOMER_DETAILS_TAB_ROUTE, { + customerId: _customerId, + tab: CustomerDetailsTabsOptions.information, + }), + ) + const [create] = useCreateCustomerMutation({ context: { silentErrorCodes: [LagoApiError.UnprocessableEntity] }, onCompleted({ createCustomer }) { @@ -185,7 +194,7 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { message: translate('text_6250304370f0f700a8fdc295'), severity: 'success', }) - navigate(generatePath(CUSTOMER_DETAILS_ROUTE, { customerId: createCustomer.id })) + goToCustomerInformationPage(createCustomer.id) } }, }) @@ -198,7 +207,7 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { message: translate('text_626162c62f790600f850b7da'), severity: 'success', }) - navigate(generatePath(CUSTOMER_DETAILS_ROUTE, { customerId: updateCustomer.id })) + goToCustomerInformationPage(updateCustomer.id) } }, }) @@ -248,9 +257,7 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { isEdition: !!customerId, customer: customer || undefined, onClose: () => - customerId - ? navigate(generatePath(CUSTOMER_DETAILS_ROUTE, { customerId })) - : navigate(CUSTOMERS_LIST_ROUTE), + customerId ? goToCustomerInformationPage(customerId) : navigate(CUSTOMERS_LIST_ROUTE), onSave, } } diff --git a/src/pages/CustomerDetails.tsx b/src/pages/CustomerDetails.tsx index 9de85dc5f..e32163773 100644 --- a/src/pages/CustomerDetails.tsx +++ b/src/pages/CustomerDetails.tsx @@ -94,7 +94,7 @@ export enum CustomerDetailsTabsOptions { invoices = 'invoices', settings = 'settings', usage = 'usage', - details = 'details', + information = 'information', } const CustomerDetails = () => { @@ -434,7 +434,7 @@ const CustomerDetails = () => { title: translate('text_17376404438209bh9jk7xa2s'), link: generatePath(CUSTOMER_DETAILS_TAB_ROUTE, { customerId: customerId as string, - tab: CustomerDetailsTabsOptions.details, + tab: CustomerDetailsTabsOptions.information, }), component: ( Date: Tue, 28 Jan 2025 13:45:59 +0200 Subject: [PATCH 60/64] refactor(overdueBalance): refresh from card --- src/components/OverviewCard.tsx | 27 +++++++++++++------ .../customers/overview/CustomerOverview.tsx | 23 +++++++--------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/OverviewCard.tsx b/src/components/OverviewCard.tsx index b252911c0..d4d1d7916 100644 --- a/src/components/OverviewCard.tsx +++ b/src/components/OverviewCard.tsx @@ -9,6 +9,7 @@ interface OverviewCardProps { caption: string isAccentContent?: boolean isLoading?: boolean + refresh?: () => void } export const OverviewCard: FC = ({ @@ -18,12 +19,13 @@ export const OverviewCard: FC = ({ caption, isAccentContent, isLoading, + refresh, }) => { return ( {isLoading ? (
- +
@@ -31,13 +33,22 @@ export const OverviewCard: FC = ({
) : ( <> -
- {title} - {tooltipContent && ( - - - - )} +
+
+ {title} + + {tooltipContent && ( + + + + )} +
+ + {refresh && }
diff --git a/src/components/customers/overview/CustomerOverview.tsx b/src/components/customers/overview/CustomerOverview.tsx index 6f9edb273..3176cd003 100644 --- a/src/components/customers/overview/CustomerOverview.tsx +++ b/src/components/customers/overview/CustomerOverview.tsx @@ -102,6 +102,15 @@ export const CustomerOverview: FC = ({ }, }) + const refreshOverdueBalances = () => + getCustomerOverdueBalances({ + variables: { + expireCache: true, + externalCustomerId: externalCustomerId || '', + currency, + }, + }) + useEffect(() => { if (!externalCustomerId) return @@ -157,19 +166,6 @@ export const CustomerOverview: FC = ({ { - getCustomerOverdueBalances({ - variables: { - expireCache: true, - externalCustomerId: externalCustomerId || '', - currency, - }, - }) - }, - }} /> @@ -259,6 +255,7 @@ export const CustomerOverview: FC = ({ overdueFormattedData.invoiceCount, )} isAccentContent={hasOverdueInvoices} + refresh={refreshOverdueBalances} /> )} From 4163298972c8892ab0180aaf97696524df3d57eb Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 13:59:01 +0200 Subject: [PATCH 61/64] refactor(useCreateEditCustomer): redirect to overview --- src/hooks/useCreateEditCustomer.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/hooks/useCreateEditCustomer.ts b/src/hooks/useCreateEditCustomer.ts index e7e7011fa..e039eb36b 100644 --- a/src/hooks/useCreateEditCustomer.ts +++ b/src/hooks/useCreateEditCustomer.ts @@ -3,7 +3,12 @@ import { useEffect } from 'react' import { generatePath, useNavigate, useParams } from 'react-router-dom' import { addToast, hasDefinedGQLError } from '~/core/apolloClient' -import { CUSTOMER_DETAILS_TAB_ROUTE, CUSTOMERS_LIST_ROUTE, ERROR_404_ROUTE } from '~/core/router' +import { + CUSTOMER_DETAILS_ROUTE, + CUSTOMER_DETAILS_TAB_ROUTE, + CUSTOMERS_LIST_ROUTE, + ERROR_404_ROUTE, +} from '~/core/router' import { AddCustomerDrawerFragment, CreateCustomerInput, @@ -194,7 +199,11 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { message: translate('text_6250304370f0f700a8fdc295'), severity: 'success', }) - goToCustomerInformationPage(createCustomer.id) + navigate( + generatePath(CUSTOMER_DETAILS_ROUTE, { + customerId: createCustomer.id, + }), + ) } }, }) From 9636f1de10afec2868443977486f7b2b55b4b73a Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 14:21:39 +0200 Subject: [PATCH 62/64] refactor(loadingState): improve loading state --- src/components/designSystem/NavigationTab.tsx | 2 +- src/pages/CustomerDetails.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/designSystem/NavigationTab.tsx b/src/components/designSystem/NavigationTab.tsx index ad02a3202..e7f6132fa 100644 --- a/src/components/designSystem/NavigationTab.tsx +++ b/src/components/designSystem/NavigationTab.tsx @@ -110,7 +110,7 @@ export const NavigationTab = ({ diff --git a/src/pages/CustomerDetails.tsx b/src/pages/CustomerDetails.tsx index e32163773..9e3840654 100644 --- a/src/pages/CustomerDetails.tsx +++ b/src/pages/CustomerDetails.tsx @@ -319,10 +319,10 @@ const CustomerDetails = () => { <>
{loading ? ( -
+
-
- +
+
From 0899311d8007fb1c690ecfb144872bdc81ba4c65 Mon Sep 17 00:00:00 2001 From: Stefan Moraru Date: Tue, 28 Jan 2025 14:36:30 +0200 Subject: [PATCH 63/64] refactor(CustomerInvoicesTab): loading state --- src/components/customers/CustomerInvoicesTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/customers/CustomerInvoicesTab.tsx b/src/components/customers/CustomerInvoicesTab.tsx index 6c90fab5f..fdd59f51c 100644 --- a/src/components/customers/CustomerInvoicesTab.tsx +++ b/src/components/customers/CustomerInvoicesTab.tsx @@ -112,7 +112,7 @@ export const CustomerInvoicesTab = ({ )} {initialLoad && ( -
+
)} - {showInvoices && !hasInvoices && !isSearching && ( + {showInvoices && !hasInvoices && !hasDraftInvoices && !isSearching && (
Date: Tue, 28 Jan 2025 14:39:27 +0200 Subject: [PATCH 64/64] fix(translations): remove unused translations --- translations/base.json | 1 - translations/de.json | 1 - translations/es.json | 1 - translations/fr.json | 1 - translations/it.json | 1 - translations/nb.json | 1 - translations/sv.json | 1 - 7 files changed, 7 deletions(-) diff --git a/translations/base.json b/translations/base.json index d7c828926..9cf9162bd 100644 --- a/translations/base.json +++ b/translations/base.json @@ -1344,7 +1344,6 @@ "text_6670a6577ecbf200898af646": "Total amount associated with overdue invoices, which are pending or failed and past their due dates.", "text_6670a6577ecbf200898af647": "Overdue invoices", "text_6670a6577ecbf200898af64a": "across {{count}} invoices", - "text_6670a7222702d70114cc7953": "Refresh", "text_6670a7222702d70114cc7954": "Invoice overview", "text_6670a7222702d70114cc7955": "{{count}} invoice totaling {{amount}} is overdue.|{{count}} invoices totaling {{amount}} are overdue.", "text_6670a7222702d70114cc7957": "Total invoiced", diff --git a/translations/de.json b/translations/de.json index 3c25e9452..8f0d3fa00 100644 --- a/translations/de.json +++ b/translations/de.json @@ -53,7 +53,6 @@ "text_6419c64eace749372fc72b62": "PDF herunterladen", "text_641c6acee4bc20004e62c534": "Es ist ein Problem aufgetreten, die Url ist ungültig oder abgelaufen.", "text_666c5b12fea4aa1e1b26bf55": "Überfällig", - "text_6670a7222702d70114cc7953": "Aktualisieren", "text_6670a7222702d70114cc7954": "Abrechnungsübersicht", "text_6670a7222702d70114cc7955": "{{count}} Rechnung in Höhe von {{amount}} ist überfällig.|{{count}} Rechnungen in Höhe von {{amount}} sind überfällig.", "text_6670a7222702d70114cc7956": "Zahlen Sie den Gesamtbetrag, um den überfälligen Saldo zu begleichen.", diff --git a/translations/es.json b/translations/es.json index 8eced70f1..cfc749180 100644 --- a/translations/es.json +++ b/translations/es.json @@ -53,7 +53,6 @@ "text_6419c64eace749372fc72b62": "Descargar PDF", "text_641c6acee4bc20004e62c534": "Algo salió mal, el token no es válido o ha expirado", "text_666c5b12fea4aa1e1b26bf55": "Atrasado", - "text_6670a7222702d70114cc7953": "Actualizar", "text_6670a7222702d70114cc7954": "Resumen de facturación", "text_6670a7222702d70114cc7955": "{{count}} factura por un total de {{amount}} está vencida.|{{count}} facturas por un total de {{amount}} están vencidas.", "text_6670a7222702d70114cc7956": "Pague el monto total para liquidar el saldo vencido.", diff --git a/translations/fr.json b/translations/fr.json index b97b60590..9d519ce21 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -53,7 +53,6 @@ "text_6419c64eace749372fc72b62": "Télécharger le PDF", "text_641c6acee4bc20004e62c534": "Un problème s'est produit, l’url n'est pas valide ou a expiré.", "text_666c5b12fea4aa1e1b26bf55": "Impayé", - "text_6670a7222702d70114cc7953": "Actualiser", "text_6670a7222702d70114cc7954": "Récapitulatif", "text_6670a7222702d70114cc7955": "{{count}} facture d'un montant de {{amount}} est impayée.|{{count}} factures d'un montant de {{amount}} sont impayées.", "text_6670a7222702d70114cc7956": "Payez ce montant pour régler votre solde dû.", diff --git a/translations/it.json b/translations/it.json index 45bffb379..89e99e937 100644 --- a/translations/it.json +++ b/translations/it.json @@ -53,7 +53,6 @@ "text_6419c64eace749372fc72b62": "Scarica PDF", "text_641c6acee4bc20004e62c534": "Qualcosa è andato storto, il token non è valido o è scaduto", "text_666c5b12fea4aa1e1b26bf55": "In ritardo", - "text_6670a7222702d70114cc7953": "Aggiorna", "text_6670a7222702d70114cc7954": "Panoramica della fatturazione", "text_6670a7222702d70114cc7955": "{{count}} fattura per un totale di {{amount}} è scaduta.|{{count}} fatture per un totale di {{amount}} sono scadute.", "text_6670a7222702d70114cc7956": "Paga l'importo totale per saldare il saldo scaduto.", diff --git a/translations/nb.json b/translations/nb.json index a11ef3f40..0a1dea8cb 100644 --- a/translations/nb.json +++ b/translations/nb.json @@ -52,7 +52,6 @@ "text_6419c64eace749372fc72b62": "Last ned PDF", "text_641c6acee4bc20004e62c534": "Noe gikk galt, token er ugyldig eller utløpt.", "text_666c5b12fea4aa1e1b26bf55": "Forfalt", - "text_6670a7222702d70114cc7953": "Oppdater", "text_6670a7222702d70114cc7954": "Fakturaoversikt", "text_6670a7222702d70114cc7955": "{{count}} faktura med totalt {{amount}} er forfalt.|X fakturaer med totalt {{amount}} er forfalt.", "text_6670a7222702d70114cc7956": "Betal totalbeløpet for å gjøre opp det forfalte beløpet.", diff --git a/translations/sv.json b/translations/sv.json index 799f4b6f5..c0014fa7c 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -53,7 +53,6 @@ "text_6419c64eace749372fc72b62": "Ladda ner PDF", "text_641c6acee4bc20004e62c534": "Något gick fel, länken är ogiltig eller har gått ut", "text_666c5b12fea4aa1e1b26bf55": "Försenad", - "text_6670a7222702d70114cc7953": "Uppdatera", "text_6670a7222702d70114cc7954": "Fakturaöversikt", "text_6670a7222702d70114cc7955": "{{count}} faktura med totalt {{amount}} är förfallen.|X fakturor med totalt {{amount}} är förfallna.", "text_6670a7222702d70114cc7956": "Betala totalbeloppet för att reglera det förfallna saldot.",