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/TimezoneDate.tsx b/src/components/TimezoneDate.tsx index 012c7f666..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', @@ -15,8 +16,9 @@ interface TimezoneDateProps { mainDateFormat?: string mainTimezone?: keyof typeof MainTimezoneEnum customerTimezone?: TimezoneEnum - mainTypographyProps?: Pick + 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" > { - 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' diff --git a/src/components/creditNote/CreditNotesTable.tsx b/src/components/creditNote/CreditNotesTable.tsx index 2bb6776ab..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,11 @@ 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 { VoidCreditNoteDialog, @@ -96,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 @@ -110,6 +103,7 @@ type TCreditNoteTableProps = { customerTimezone?: TimezoneEnum tableContainerSize?: ResponsiveStyleValue showFilters?: boolean + filtersContainerClassName?: string } const CreditNoteTableItemSkeleton = () => { @@ -135,16 +129,13 @@ const CreditNotesTable = ({ error, tableContainerSize, showFilters = true, + 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 }) { @@ -152,32 +143,17 @@ 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 ( <> {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; diff --git a/src/components/customers/CustomerCreditNotesList.tsx b/src/components/customers/CustomerCreditNotesList.tsx index a2178fa1b..59d92b46f 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 { @@ -14,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' @@ -52,70 +52,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={} - /> - ) : ( - - )}
) } diff --git a/src/components/customers/CustomerInvoicesTab.tsx b/src/components/customers/CustomerInvoicesTab.tsx index e7da4d3b7..fdd59f51c 100644 --- a/src/components/customers/CustomerInvoicesTab.tsx +++ b/src/components/customers/CustomerInvoicesTab.tsx @@ -1,9 +1,12 @@ 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 { PageSectionTitle } from '~/components/layouts/Section' import { CUSTOMER_DRAFT_INVOICES_LIST_ROUTE } from '~/core/router' import { + CurrencyEnum, InvoiceForInvoiceListFragmentDoc, InvoiceStatusTypeEnum, TimezoneEnum, @@ -44,9 +47,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, @@ -87,10 +97,22 @@ export const CustomerInvoicesTab = ({ customerId, customerTimezone }: CustomerIn 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 ? ( -
+
+ {showInvoices && hasInvoices && ( + + )} + + {initialLoad && ( +
- ) : !invoicesDraft?.length && - !invoicesFinalized?.length && - !variablesFinalized?.searchTerm ? ( - {translate('text_6250304370f0f700a8fdc293')} - ) : ( - <> - {!!invoicesDraft?.length && ( -
-
- - {translate('text_638f4d756d899445f18a49ee')} - -
- - - {invoicesDraftCount > DRAFT_INVOICES_ITEMS_COUNT && ( -
- - {translate('text_638f4d756d899445f18a4a0e')} - -
- )} + )} + + {showInvoices && !hasInvoices && !hasDraftInvoices && !isSearching && ( +
+ + + + {translate('text_6250304370f0f700a8fdc293')} + +
+ )} + + {showInvoices && hasDraftInvoices && ( +
+ + + + + {showSeeMore && ( +
+ + {translate('text_638f4d756d899445f18a4a0e')} +
)} +
+ )} - {(loadingFinalized || - !!invoicesFinalized?.length || - !!variablesFinalized?.searchTerm) && ( - <> -
- - {translate('text_6250304370f0f700a8fdc291')} - - -
- + - - )} - + } + /> + + +
)}
) diff --git a/src/components/customers/CustomerMainInfos.tsx b/src/components/customers/CustomerMainInfos.tsx index 5385f6c54..5fd5ec61b 100644 --- a/src/components/customers/CustomerMainInfos.tsx +++ b/src/components/customers/CustomerMainInfos.tsx @@ -1,11 +1,11 @@ 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, @@ -40,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', @@ -195,7 +194,17 @@ interface CustomerMainInfosProps { onEdit?: () => unknown } -const SHOW_MORE_THRESHOLD = 6 +const InfoSection = ({ title, children }: { title: string; children: React.ReactNode }) => ( +
+ {title} + + {children} +
+) + +const InfoBlock = ({ children }: { children: React.ReactNode }) => ( +
{children}
+) const InlineLink: FC> = ({ children, ...props }) => { return ( @@ -210,9 +219,6 @@ const InlineLink: FC> = ({ 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,21 +278,12 @@ 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 ( - - +
+
- +
@@ -295,7 +292,7 @@ export const CustomerMainInfos = ({ loading, customer, onEdit }: CustomerMainInf
- +
) const { @@ -325,446 +322,500 @@ 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) + + 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 ( - - - {translate('text_6250304370f0f700a8fdc27d')} - - - - { - infosRef.current = node - - if (node) { - updateRef(node) - } +
+ onEdit?.(), }} - data-id="customer-info-list" - $showMore={showMore} - > - {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_667d708c1359b49f5a5a822a')} + {translate('text_626162c62f790600f850b76a')} + + + {name} + + + )} + {(firstname || lastname) && ( + + + {translate('text_17261289386311s35rvzyxbz')} + + + {firstname} {lastname} + + + )} + + {translate('text_6250304370f0f700a8fdc283')} + {externalId} + + {timezone && ( + + + {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, + })} - {shippingAddress.country && ( - - {CountryCodes[shippingAddress.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} - - - ) : ( + {externalSalesforceId && ( + + + {translate('text_651fd42936a03200c126c683')} + + {externalSalesforceId} + + )} + + + {hasBillingInformation && ( + + {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} - {providerCustomer?.providerCustomerId} + {zipcode} {city} {state} - )} - - )} - {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} - - {CountryCodes[country]} )} - > - - {customer?.netsuiteCustomer?.externalCustomerId} +
+
+ )} + {shippingAddress && + (shippingAddress.addressLine1 || + shippingAddress.addressLine2 || + shippingAddress.state || + shippingAddress.country || + shippingAddress.city || + shippingAddress.zipcode) && ( + + + {translate('text_667d708c1359b49f5a5a822a')} - - - ) : 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 ? ( - - - - - - {connectedAnrokIntegration?.name} - - {!!connectedAnrokIntegration.externalAccountId && - customer?.anrokCustomer?.externalCustomerId && ( - - - {customer?.anrokCustomer?.externalCustomerId} +
+ {shippingAddress.addressLine1} + {shippingAddress.addressLine2} + + {shippingAddress.zipcode} {shippingAddress.city} {shippingAddress.state} + + {shippingAddress.country && ( + + {CountryCodes[shippingAddress.country]} - - )} - - ) : null} -
+ )} +
+ + )} + )} - {!!connectedHubspotIntegration && ( -
- {translate('text_1728658962985xpfdvl5ru8a')} - {integrationsLoading ? ( - - - - - ) : !!connectedHubspotIntegration && - customer?.hubspotCustomer?.integrationId && - customer?.hubspotCustomer.targetedObject ? ( - - - - - - {connectedHubspotIntegration?.name} - - - {translate( - getTargetedObjectTranslationKey[customer?.hubspotCustomer.targetedObject], - )} + {!!metadata?.length && ( + + {metadata.map((meta) => ( + + + {meta.key} - {!!connectedHubspotIntegration.portalId && - customer?.hubspotCustomer?.externalCustomerId && - !!customer?.hubspotCustomer.targetedObject && ( - - - {customer?.hubspotCustomer?.externalCustomerId} - - - )} - - ) : null} -
+ + {meta.value} + + + ))} + )} - {!!connectedSalesforceIntegration && ( -
- {translate('text_1728658962985xpfdvl5ru8a')} - {integrationsLoading ? ( - - - - - ) : !!connectedSalesforceIntegration && customer?.salesforceCustomer?.integrationId ? ( - - - - - - {connectedSalesforceIntegration?.name} - - {!!connectedSalesforceIntegration.instanceId && - customer?.salesforceCustomer?.externalCustomerId && ( - - - {customer?.salesforceCustomer?.externalCustomerId} - - + {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} + + )} + )} - - ) : null} -
- )} - - {!!metadata?.length && - metadata.map((meta) => ( -
- - {meta.key} - - - {meta.value} - -
- ))} - - {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')} - - )} - - ) -} + {paymentProvider === ProviderTypeEnum?.Stripe && + !!providerCustomer?.providerPaymentMethods?.length && ( + <> + {providerCustomer?.providerPaymentMethods?.map((method) => ( + + {translate(PaymentProviderMethodTranslationsLookup[method])} + + ))} + + )} +
+ + )} -const LoadingDetails = styled.div` - > *:first-child { - margin-bottom: ${theme.spacing(8)}; - } + {(!!customer?.netsuiteCustomer || !!connectedNetsuiteIntegration?.id) && ( + + + {translate('text_66423cad72bbad009f2f568f')} + - > *:not(:first-child) { - margin-bottom: ${theme.spacing(7)}; - } -` +
+ {integrationsLoading ? ( + + + + + ) : !!connectedNetsuiteIntegration && + customer?.netsuiteCustomer?.externalCustomerId ? ( + + + + + + + {connectedNetsuiteIntegration?.name} + + + + + {customer?.netsuiteCustomer?.externalCustomerId} + + + + ) : null} +
+
+ )} -const DetailsBlock = styled.div` - > *:not(:first-child) { - margin-bottom: ${theme.spacing(3)}; - } -` + {(!!customer?.xeroCustomer || !!connectedXeroIntegration?.id) && ( + + + {translate('text_66423cad72bbad009f2f568f')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedXeroIntegration && customer?.xeroCustomer?.externalCustomerId ? ( + + + + + + {connectedXeroIntegration?.name} + + + + {customer?.xeroCustomer?.externalCustomerId} + + + + ) : null} +
+
+ )} -const InfosBlock = styled.div<{ $showMore: boolean }>` - > *:not(:last-child) { - margin-bottom: ${theme.spacing(3)}; - } + {!!connectedAnrokIntegration && ( + + + {translate('text_6668821d94e4da4dfd8b3840')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedAnrokIntegration && customer?.anrokCustomer?.integrationId ? ( + + + + + + {connectedAnrokIntegration?.name} + + {!!connectedAnrokIntegration.externalAccountId && + customer?.anrokCustomer?.externalCustomerId && ( + + + {customer?.anrokCustomer?.externalCustomerId} + + + )} + + ) : null} +
+
+ )} - // Hide all items after the threshold - > *:nth-child(n + ${SHOW_MORE_THRESHOLD}) { - ${({ $showMore }) => ($showMore ? 'display: block;' : 'display: none;')} - } -` + {!!connectedHubspotIntegration && ( + + + {translate('text_1728658962985xpfdvl5ru8a')} + -const SectionHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: ${theme.spacing(4)}; -` +
+ {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 ShowMoreButton = styled.span` - color: ${theme.palette.primary[600]}; - cursor: pointer; -` + {!!connectedSalesforceIntegration && ( + + + {translate('text_1728658962985xpfdvl5ru8a')} + +
+ {integrationsLoading ? ( + + + + + ) : !!connectedSalesforceIntegration && + customer?.salesforceCustomer?.integrationId ? ( + + + + + + + {connectedSalesforceIntegration?.name} + + + {!!connectedSalesforceIntegration.instanceId && + customer?.salesforceCustomer?.externalCustomerId && ( + + + {customer?.salesforceCustomer?.externalCustomerId}{' '} + + + + )} + + ) : null} +
+
+ )} + + )} +
+
+ ) +} diff --git a/src/components/customers/CustomerSettings.tsx b/src/components/customers/CustomerSettings.tsx index aab06ef75..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 ? ( diff --git a/src/components/customers/overview/CustomerCoupons.tsx b/src/components/customers/overview/CustomerCoupons.tsx index e28ac5ffc..dfc6500e1 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,52 @@ export const CustomerCoupons = memo(() => { return ( <> {!!(coupons || [])?.length && ( - - - {translate('text_628b8c693e464200e00e469d')} - - - - - {translate('text_628b8c693e464200e00e46ab')} - - - {(coupons || []).map((appliedCoupon) => ( - - - - - - - {appliedCoupon.coupon?.name} - - - - {hasPermissions(['couponsDetach']) && ( + }, + }} + /> + +
coupon.coupon?.name} + columns={[ + { + key: 'coupon.name', + title: translate('text_62865498824cc10126ab2960'), + content: ({ coupon: { name } }) => ( +
+ + + + {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' diff --git a/src/components/customers/overview/CustomerOverview.tsx b/src/components/customers/overview/CustomerOverview.tsx index e3fb31d27..3176cd003 100644 --- a/src/components/customers/overview/CustomerOverview.tsx +++ b/src/components/customers/overview/CustomerOverview.tsx @@ -4,9 +4,8 @@ import { DateTime } from 'luxon' import { FC, useEffect, useMemo } from 'react' import { generatePath, useNavigate, useParams } from 'react-router-dom' -import { CustomerCoupons } from '~/components/customers/overview/CustomerCoupons' -import { CustomerSubscriptionsList } from '~/components/customers/overview/CustomerSubscriptionsList' -import { Alert, Button, Typography } from '~/components/designSystem' +import { 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' @@ -15,14 +14,12 @@ import { isSameDay } from '~/core/timezone' import { LocaleEnum } from '~/core/translations' import { CurrencyEnum, - TimezoneEnum, useGetCustomerGrossRevenuesLazyQuery, useGetCustomerOverdueBalancesLazyQuery, } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' import { useOrganizationInfos } from '~/hooks/useOrganizationInfos' import { usePermissions } from '~/hooks/usePermissions' -import { SectionHeader } from '~/styles/customer' gql` query getCustomerOverdueBalances( @@ -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() @@ -109,6 +102,15 @@ export const CustomerOverview: FC = ({ }, }) + const refreshOverdueBalances = () => + getCustomerOverdueBalances({ + variables: { + expireCache: true, + externalCustomerId: externalCustomerId || '', + currency, + }, + }) + useEffect(() => { if (!externalCustomerId) return @@ -158,28 +160,14 @@ export const CustomerOverview: FC = ({ const hasMadePaymentRequestToday = isSameDay(lastPaymentRequestDate, today) return ( - <> +
{(!overdueBalancesError || !grossRevenuesError) && (
- - {translate('text_6670a7222702d70114cc7954')} + - - {hasOverdueInvoices && !overdueBalancesError && ( = ({ overdueFormattedData.invoiceCount, )} isAccentContent={hasOverdueInvoices} + refresh={refreshOverdueBalances} /> )}
)} - {!isLoading && } - - +
) } diff --git a/src/components/customers/overview/CustomerSubscriptionsList.tsx b/src/components/customers/overview/CustomerSubscriptionsList.tsx index 0955b68e8..b2043c613 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 { - SubscriptionItemFragmentDoc, + 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 { CustomerSubscriptionDetailsTabsOptionsEnum } from '~/core/constants/tabsOptions' +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, 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 { tw } from '~/styles/utils' -import { SubscriptionItem, SubscriptionItemSkeleton } from '../subscriptions/SubscriptionItem' import { TerminateCustomerSubscriptionDialog, TerminateCustomerSubscriptionDialogRef, @@ -27,22 +45,202 @@ gql` id subscriptions(status: [active, pending]) { id + status + startedAt + nextPendingStartDate + name + nextName + externalId + subscriptionAt + endingAt plan { id amountCurrency + name + interval + } + nextPlan { + id + name + code + interval + } + nextSubscription { + id + name + externalId + status } - ...SubscriptionItem } } } - - ${SubscriptionItemFragmentDoc} ` 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 + isScheduled?: 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, + nextSubscription, + } = 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, + isScheduled: status === StatusTypeEnum.Pending, + } + + const _subDowngrade = isDowngrading && + nextPlan && { + id: nextSubscription?.id || nextPlan.id, + externalId: nextSubscription?.externalId, + name: nextSubscription?.name || nextName || nextPlan.name, + status: nextSubscription?.status, + 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: subscription.status as StatusTypeEnum, + }), + }) + } + + return actions +} + export const CustomerSubscriptionsList = ({ customerTimezone }: CustomerSubscriptionsListProps) => { const { customerId } = useParams() const navigate = useNavigate() @@ -52,90 +250,135 @@ 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 ? ( - {translate('text_6250304370f0f700a8fdc28f')} - ) : ( + { + 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 ( - - ) - })} - +
subscription.name || `subscription-${subscription.id}`} + onRowActionLink={({ id }) => + generatePath(CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, { + customerId: customerId as string, + subscriptionId: id, + tab: CustomerSubscriptionDetailsTabsOptionsEnum.overview, + }) + } + columns={[ + { + key: 'statusType.type', + title: translate('text_62d7f6178ec94cd09370e5fb'), + content: ({ statusType }) => , + }, + { + key: 'name', + maxSpace: true, + title: translate('text_6253f11816f710014600b9ed'), + content: ({ name, isDowngrade, isScheduled }) => ( + <> +
+ {isDowngrade && } + + + {name} + + + {isDowngrade && } + + {isScheduled && } +
+ + ), + }, + { + 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 ? ( + + ) : ( + - + ), + }, + ]} + 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 ( -
- -
- - -
-
- ) -} diff --git a/src/components/customers/subscriptions/SubscriptionLine.tsx b/src/components/customers/subscriptions/SubscriptionLine.tsx deleted file mode 100644 index 8909699f2..000000000 --- a/src/components/customers/subscriptions/SubscriptionLine.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { gql } from '@apollo/client' -import { RefObject } from 'react' -import { generatePath, useNavigate, useParams } from 'react-router-dom' -import styled from 'styled-components' - -import { - Avatar, - Button, - Icon, - Popper, - Status, - StatusType, - Tooltip, - Typography, -} from '~/components/designSystem' -import { TimezoneDate } from '~/components/TimezoneDate' -import { addToast } from '~/core/apolloClient' -import { CustomerSubscriptionDetailsTabsOptionsEnum } from '~/core/constants/tabsOptions' -import { - CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, - UPDATE_SUBSCRIPTION, - UPGRADE_DOWNGRADE_SUBSCRIPTION, -} from '~/core/router' -import { copyToClipboard } from '~/core/utils/copyToClipboard' -import { StatusTypeEnum, SubscriptionLinePlanFragment, TimezoneEnum } from '~/generated/graphql' -import { useInternationalization } from '~/hooks/core/useInternationalization' -import { usePermissions } from '~/hooks/usePermissions' -import { ItemContainer, ListItemLink, MenuPopper, NAV_HEIGHT, PopperOpener, theme } from '~/styles' - -import { TerminateCustomerSubscriptionDialogRef } from './TerminateCustomerSubscriptionDialog' - -gql` - fragment SubscriptionLinePlan on Plan { - id - name - code - } -` - -interface SubscriptionLineProps { - subscriptionId: string - subscriptionExternalId: string - subscriptionName?: string | null - date: string - plan: SubscriptionLinePlanFragment - isDowngrade?: boolean - hasBottomSection?: boolean - hasAboveSection?: boolean - status?: StatusTypeEnum | null - customerTimezone?: TimezoneEnum - terminateSubscriptionDialogRef: RefObject | null -} - -export const SubscriptionLine = ({ - subscriptionId, - subscriptionExternalId, - subscriptionName, - date, - plan, - isDowngrade, - status, - customerTimezone, - hasBottomSection, - hasAboveSection, - terminateSubscriptionDialogRef, -}: SubscriptionLineProps) => { - const navigate = useNavigate() - const { customerId } = useParams() - const { translate } = useInternationalization() - const { hasPermissions } = usePermissions() - - return ( - - - - - - - - - {subscriptionName || plan.name} - - - {plan.code} - - - - - - - - - - ( - - - - - - - )} - - - - {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)}; -` diff --git a/src/components/customers/usage/CustomerUsage.tsx b/src/components/customers/usage/CustomerUsage.tsx index 302894c3a..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')} - - - + + } + /> diff --git a/src/components/designSystem/Status.tsx b/src/components/designSystem/Status.tsx index 367434a66..876b32650 100644 --- a/src/components/designSystem/Status.tsx +++ b/src/components/designSystem/Status.tsx @@ -19,7 +19,7 @@ export enum StatusType { 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 +55,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 diff --git a/src/components/layouts/Section.tsx b/src/components/layouts/Section.tsx new file mode 100644 index 000000000..a55395fdc --- /dev/null +++ b/src/components/layouts/Section.tsx @@ -0,0 +1,52 @@ +import { Skeleton, Typography } from '@mui/material' + +import { Button } from '~/components/designSystem' +import { tw } from '~/styles/utils' + +export const PageSectionTitle = ({ + className, + title, + subtitle, + action, + customAction, + loading, +}: { + className?: string + title: string + subtitle?: string + action?: { title: string; onClick: () => void; dataTest?: string } + customAction?: React.ReactNode + loading?: boolean +}) => { + return ( +
+ {loading && ( +
+ +
+ )} + + {!loading && ( + <> +
+ + {title} + + + {subtitle && ( + {subtitle} + )} +
+ + {action && ( + + )} + + {customAction ? customAction : null} + + )} +
+ ) +} 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) => (
diff --git a/src/components/wallets/CustomerWalletList.tsx b/src/components/wallets/CustomerWalletList.tsx index 5e5ee6b8f..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,8 +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 { tw } from '~/styles/utils' import { TerminateCustomerWalletDialog, @@ -99,105 +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 ? (
@@ -206,7 +203,9 @@ export const CustomerWalletsList = ({ customerId, customerTimezone }: CustommerW ))}
) : !loading && !!hasNoWallet ? ( - {translate('text_62d175066d2dbf1d50bc9386')} + + {translate('text_62d175066d2dbf1d50bc9386')} + ) : ( { diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 2dca2ceda..df2e4fb95 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, 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, name?: string | null, externalId: string, status?: StatusTypeEnum | null } | 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,15 +15046,36 @@ export const GetCustomerSubscriptionForListDocument = gql` id subscriptions(status: [active, pending]) { id + status + startedAt + nextPendingStartDate + name + nextName + externalId + subscriptionAt + endingAt plan { id amountCurrency + name + interval + } + nextPlan { + id + name + code + interval + } + nextSubscription { + id + name + externalId + status } - ...SubscriptionItem } } } - ${SubscriptionItemFragmentDoc}`; + `; /** * __useGetCustomerSubscriptionForListQuery__ diff --git a/src/hooks/useCreateEditCustomer.ts b/src/hooks/useCreateEditCustomer.ts index cea315f88..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_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, @@ -18,6 +23,7 @@ import { useUpdateCustomerMutation, } from '~/generated/graphql' import { useInternationalization } from '~/hooks/core/useInternationalization' +import { CustomerDetailsTabsOptions } from '~/pages/CustomerDetails' gql` fragment CustomerForExternalAppsAccordion on Customer { @@ -177,6 +183,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 +199,11 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { message: translate('text_6250304370f0f700a8fdc295'), severity: 'success', }) - navigate(generatePath(CUSTOMER_DETAILS_ROUTE, { customerId: createCustomer.id })) + navigate( + generatePath(CUSTOMER_DETAILS_ROUTE, { + customerId: createCustomer.id, + }), + ) } }, }) @@ -198,7 +216,7 @@ export const useCreateEditCustomer: UseCreateEditCustomer = () => { message: translate('text_626162c62f790600f850b7da'), severity: 'success', }) - navigate(generatePath(CUSTOMER_DETAILS_ROUTE, { customerId: updateCustomer.id })) + goToCustomerInformationPage(updateCustomer.id) } }, }) @@ -248,9 +266,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 85cf8c304..9e3840654 100644 --- a/src/pages/CustomerDetails.tsx +++ b/src/pages/CustomerDetails.tsx @@ -1,7 +1,6 @@ import { gql } from '@apollo/client' import { useRef } from 'react' import { generatePath, useNavigate, useParams } from 'react-router-dom' -import styled from 'styled-components' import { AddCouponToCustomerDialog, @@ -15,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 { @@ -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 } from '~/styles' gql` fragment CustomerDetails on Customer { @@ -94,6 +94,7 @@ export enum CustomerDetailsTabsOptions { invoices = 'invoices', settings = 'settings', usage = 'usage', + information = 'information', } const CustomerDetails = () => { @@ -158,6 +159,7 @@ const CustomerDetails = () => { )} +
) } -const Content = styled.div` - display: grid; - grid-template-areas: - 'infos' - 'content' - 'details'; - grid-auto-rows: auto; - grid-gap: ${theme.spacing(8)}; - padding: ${theme.spacing(8)} ${theme.spacing(4)} ${theme.spacing(20)}; - min-height: calc(100vh - ${NAV_HEIGHT}px); - grid-auto-rows: min-content; - - ${theme.breakpoints.up('md')} { - padding: ${theme.spacing(8)} ${theme.spacing(12)} ${theme.spacing(20)}; - } - - ${theme.breakpoints.up('lg')} { - padding: 0 0 0 ${theme.spacing(12)}; - grid-template-columns: minmax(420px, 1fr) minmax(300px, 368px); - grid-template-rows: auto 1fr; - grid-template-areas: - 'infos details' - 'content details'; - } -` - -const MainInfos = styled.div` - grid-area: infos; - display: flex; - align-items: center; - - > *:first-child { - margin-right: ${theme.spacing(4)}; - } - - ${theme.breakpoints.up('lg')} { - padding-top: ${theme.spacing(8)}; - } -` - -const StyledTabs = styled.div` - grid-area: content; - - ${theme.breakpoints.up('lg')} { - } -` - -const CustomerMainInfosContainer = styled.div` - grid-area: details; - - ${theme.breakpoints.up('lg')} { - box-shadow: ${theme.shadows[8]}; - padding: ${theme.spacing(6)}; - } -` - -const SideBlock = styled.div` - > *:not(:last-child) { - margin-bottom: ${theme.spacing(8)}; - } - - margin-bottom: ${theme.spacing(20)}; -` - export default CustomerDetails 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 @@ + + + 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 diff --git a/translations/base.json b/translations/base.json index 1b06f5668..9cf9162bd 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)", @@ -1339,14 +1339,12 @@ "text_666c5b12fea4aa1e1b26bf70": "There are no overdue invoices.", "text_666c5b12fea4aa1e1b26bf73": "An invoice is overdue when its due date is past, as per your payment terms. ", "text_666c5d227d073444e90be894": "Due date", - "text_6670a2a7ae3562006c4ee3ce": "Show more", "text_6670a2a7ae3562006c4ee3db": "Request payment to settle the overdue amount.", "text_6670a2a7ae3562006c4ee3e7": "Total from past due invoices. This is the amount this customer owes you.", "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": "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", @@ -1448,7 +1446,6 @@ "text_64ef55a730b88e3d2117b3c4": "Start date in UTC±0", "text_64ef55a730b88e3d2117b3cc": "End date in UTC±0 (optional)", "text_64ef55a730b88e3d2117b3d4": "End date can’t be in the past and should be greater than start date", - "text_64ef55a730b88e3d2117b44e": "{{planName}} will be terminated on {{date}}", "text_64ef81071c6da2010dd24b1d": "The subscription started on {{date}} at {{time}} UTC {{offset}} for your customer.", "text_64ef8cc7c83f5d006131a488": "The subscription will start on {{date}} at {{time}} UTC {{offset}} for your customer.", "text_64ef81071c6da2010dd24b1e": "It won’t end until you manually terminate it.", @@ -1871,19 +1868,18 @@ "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", - "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_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.", "text_6250304370f0f700a8fdc278": "Refresh the page", "text_6253f11816f710014600b9ed": "Plan name", - "text_6253f11816f710014600b9f1": "Subscr. date", "text_62544c1db13ca10187214d7f": "Issuing date", "text_62544c1db13ca10187214d85": "Amount", "text_6253f11816f710014600ba1f": "Invoice ID copied to clipboard", @@ -1891,7 +1887,6 @@ "text_625434c7bb2cb40124c81a31": "Search or select a plan", "text_625434c7bb2cb40124c81a37": "No plans", "text_62681c60582e4f00aa82938a": "Downgrading to {{planName}} on {{dateStartNewPlan}}", - "text_6335e50b0b089e1d8ed50960": "{{planName}} will be activated on {{startDate}}", "text_6335e50b0b089e1d8ed508da": "As the subscription is pending, the new plan will take effect on the start date defined previously", "text_6335e8900c69f8ebdfef5312": "Subscription information", "text_626c0c09812bbc00e4c59e01": "Legal name", @@ -1966,8 +1961,6 @@ "text_628b8c693e464200e00e4685": "Select or search a coupon", "text_628b8c693e464200e00e4693": "Cancel", "text_628b8c693e464200e00e46a1": "Apply coupon", - "text_628b8c693e464200e00e469d": "Active coupon", - "text_628b8c693e464200e00e46ab": "Name", "text_628b8c693e464200e00e49f2": "Coupon successfully applied", "text_628b8c693e464200e00e4a10": "Remove", "text_628b8c693e464200e00e465f": "Remove this coupon from this customer", @@ -2792,5 +2785,23 @@ "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.", + "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.", + "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": "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." } diff --git a/translations/de.json b/translations/de.json index 80431fde4..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.", @@ -164,5 +163,7 @@ "text_634687079be251fdb43833cb": "Kundenname", "text_1729256593854oiy13slixjr": "Ihr überfälliger Saldo von {{companyName}}", "text_17326262367759e7w9yfbeno": "{{amount}} (zzgl. MwSt.)", - "text_17326286491076m81w5uy3el": "Gesamte aktuelle Nutzung (zzgl. MwSt.)" + "text_17326286491076m81w5uy3el": "Gesamte aktuelle Nutzung (zzgl. MwSt.)", + "text_1736972452609qdjngeuqsz0": "Herabstufen", + "text_1736972452609g2v8mzgvi2t": "Geplant" } diff --git a/translations/es.json b/translations/es.json index 699079636..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.", @@ -164,5 +163,7 @@ "text_634687079be251fdb43833cb": "Nombre del cliente", "text_1729256593854oiy13slixjr": "Su saldo vencido de {{companyName}}", "text_17326262367759e7w9yfbeno": "{{amount}} (sin impuestos)", - "text_17326286491076m81w5uy3el": "Uso total actual (sin impuestos)" + "text_17326286491076m81w5uy3el": "Uso total actual (sin impuestos)", + "text_1736972452609qdjngeuqsz0": "Degradar", + "text_1736972452609g2v8mzgvi2t": "Programado" } diff --git a/translations/fr.json b/translations/fr.json index 8f46553d1..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û.", @@ -164,5 +163,7 @@ "text_634687079be251fdb43833cb": "Nom du client", "text_1729256593854oiy13slixjr": "Votre solde impayé pour {{companyName}}", "text_17326262367759e7w9yfbeno": "{{amount}} (HT)", - "text_17326286491076m81w5uy3el": "Utilisation totale actuelle (HT)" + "text_17326286491076m81w5uy3el": "Utilisation totale actuelle (HT)", + "text_1736972452609qdjngeuqsz0": "Rétrogradation", + "text_1736972452609g2v8mzgvi2t": "Planifié" } diff --git a/translations/it.json b/translations/it.json index 4197baa3f..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.", @@ -137,5 +136,7 @@ "text_634687079be251fdb43833cb": "Nome del cliente", "text_1729256593854oiy13slixjr": "Il tuo saldo in ritardo da {{companyName}}", "text_17326262367759e7w9yfbeno": "{{amount}} (escl. tasse)", - "text_17326286491076m81w5uy3el": "Utilizzo totale corrente (escl. tasse)" + "text_17326286491076m81w5uy3el": "Utilizzo totale corrente (escl. tasse)", + "text_1736972452609qdjngeuqsz0": "Declassamento", + "text_1736972452609g2v8mzgvi2t": "Programmato" } diff --git a/translations/nb.json b/translations/nb.json index 63e22da3a..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.", @@ -163,5 +162,7 @@ "text_634687079be251fdb43833cb": "Kundenavn", "text_1729256593854oiy13slixjr": "Din forfalte saldo fra {{companyName}}", "text_17326262367759e7w9yfbeno": "{{amount}} (eks. mva)", - "text_17326286491076m81w5uy3el": "Total nåværende bruk (eks. mva)" + "text_17326286491076m81w5uy3el": "Total nåværende bruk (eks. mva)", + "text_1736972452609qdjngeuqsz0": "Nedgradering", + "text_1736972452609g2v8mzgvi2t": "Planlagt" } diff --git a/translations/sv.json b/translations/sv.json index ce4357b93..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.", @@ -164,5 +163,7 @@ "text_634687079be251fdb43833cb": "Kundnamn", "text_1729256593854oiy13slixjr": "Din förfallna saldo från {{companyName}}", "text_17326262367759e7w9yfbeno": "{{amount}} (exkl. moms)", - "text_17326286491076m81w5uy3el": "Total aktuell användning (exkl. moms)" + "text_17326286491076m81w5uy3el": "Total aktuell användning (exkl. moms)", + "text_1736972452609qdjngeuqsz0": "Nedgradering", + "text_1736972452609g2v8mzgvi2t": "Schemalagd" }