Skip to content

Commit 7669dbe

Browse files
authored
Merge pull request #1210 from helpwave/issue/1206-Customer-API-Intgration-side
feat: add locale formatters
2 parents 01eb8b8 + f948c2d commit 7669dbe

File tree

7 files changed

+176
-144
lines changed

7 files changed

+176
-144
lines changed

customer/api/dataclasses/customer_product.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ export type CustomerProductStatus =
1212
| 'expired'
1313
| 'refunded'
1414

15+
export const CustomerProductStatusCancelable: CustomerProductStatus[] = ['trialing', 'active', 'activation', 'payment']
16+
export const CustomerProductStatusPlannedCancellation: CustomerProductStatus[] = ['scheduled', 'canceled', 'expired', 'refunded']
17+
1518
export type CustomerProductStatusTranslation = Record<CustomerProductStatus, string>
1619

17-
export const defaultCustomerProductStatusTranslation : Translation<CustomerProductStatusTranslation> = {
20+
export const defaultCustomerProductStatusTranslation: Translation<CustomerProductStatusTranslation> = {
1821
en: {
1922
trialing: 'Trialing',
2023
active: 'Active',

customer/pages/invoices/index.tsx

Lines changed: 68 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { NextPage } from 'next'
2-
import { useLanguage, type Languages } from '@helpwave/common/hooks/useLanguage'
2+
import { type Languages } from '@helpwave/common/hooks/useLanguage'
33
import { useTranslation, type PropsForTranslation } from '@helpwave/common/hooks/useTranslation'
44
import { Page } from '@/components/layout/Page'
55
import titleWrapper from '@/utils/titleWrapper'
@@ -16,6 +16,7 @@ import { withAuth } from '@/hooks/useAuth'
1616
import { withOrganization } from '@/hooks/useOrganization'
1717
import { useMyInvoicesQuery } from '@/api/mutations/invoice_mutations'
1818
import EmbeddedCheckoutButton from '@/components/forms/StripeCheckOutForm'
19+
import { defaultLocaleFormatters } from '@/utils/locale'
1920

2021
type InvoicesTranslation = {
2122
invoices: string,
@@ -103,39 +104,9 @@ const PaymentDisplay = ({ amount }: PaymentDisplayProps) => {
103104

104105
const Invoices: NextPage<PropsForTranslation<InvoicesTranslation, InvoicesServerSideProps>> = ({ overwriteTranslation }) => {
105106
const translation = useTranslation(defaultInvoicesTranslations, overwriteTranslation)
106-
const { isError, isLoading, data } = useMyInvoicesQuery()
107-
const language = useLanguage()
108-
109-
function formatDate(date?: Date) {
110-
if (date == null || date == undefined) {
111-
return null
112-
}
113-
114-
const languageToLocaleMapping = {
115-
de: 'de-DE',
116-
en: 'en-US',
117-
}
118-
119-
const locale = languageToLocaleMapping[language.language] || 'en-US'
120-
121-
122-
const options: Intl.DateTimeFormatOptions = {
123-
year: 'numeric',
124-
month: '2-digit',
125-
day: '2-digit',
126-
hour: '2-digit',
127-
minute: '2-digit',
128-
hour12: locale === 'en-US'
129-
}
130-
131-
let formatted = date.toLocaleString(locale, options)
132-
133-
if (locale === 'de-DE') {
134-
formatted = formatted.replace(/(\d{2}:\d{2})$/, '$1 Uhr')
135-
}
107+
const localeTranslation = useTranslation(defaultLocaleFormatters)
136108

137-
return formatted
138-
}
109+
const { isError, isLoading, data } = useMyInvoicesQuery()
139110

140111
return (
141112
<Page pageTitle={titleWrapper(translation.invoices)}>
@@ -144,78 +115,78 @@ const Invoices: NextPage<PropsForTranslation<InvoicesTranslation, InvoicesServer
144115
<h2 className={tw('font-bold font-space text-3xl')}>{translation.invoices}</h2>
145116
{!isError && !isLoading && data && (
146117
<PaymentDisplay
147-
amount={data.filter(value => value.status !== 'paid').reduce((previousValue, currentValue) => previousValue + currentValue.totalAmount, 0)}/>
118+
amount={data.filter(value => value.status !== 'paid').reduce((previousValue, currentValue) => previousValue + currentValue.totalAmount, 0)} />
148119
)}
149120
</div>
150121
{(isError) && (<span className={tw('There was an Error')}></span>)}
151-
{!isError && isLoading && (<LoadingAnimation/>)}
122+
{!isError && isLoading && (<LoadingAnimation />)}
152123
{!isError && !isLoading && data.length > 0 ? (
153-
<div className={tw('flex flex-wrap gap-x-8 gap-y-12')}>
154-
<Table
155-
className={tw('w-full h-full')}
156-
data={data}
157-
identifierMapping={dataObject => dataObject.uuid}
158-
header={[
159-
(<span key="date">{translation.date}</span>),
160-
(<span key="name">{translation.title}</span>),
161-
(<span key="price">{translation.price}</span>),
162-
(<span key="paymentStatus">{translation.paymentStatus}</span>),
163-
(<span key="paymentDate">{translation.paymentDate}</span>),
164-
(<span key="actions">{translation.actions}</span>),
165-
]}
166-
rowMappingToCells={invoice => [
167-
(<span key={invoice.uuid + '-date'}>{formatDate(invoice.date)}</span>),
168-
(<span key={invoice.uuid + '-name'}>{invoice.title}</span>),
169-
(
170-
<span key={invoice.uuid + '-price'} className={tw('font-semibold')}>
171-
{invoice.totalAmount + '€'}
124+
<div className={tw('flex flex-wrap gap-x-8 gap-y-12')}>
125+
<Table
126+
className={tw('w-full h-full')}
127+
data={data}
128+
identifierMapping={dataObject => dataObject.uuid}
129+
header={[
130+
(<span key="date">{translation.date}</span>),
131+
(<span key="name">{translation.title}</span>),
132+
(<span key="price">{translation.price}</span>),
133+
(<span key="paymentStatus">{translation.paymentStatus}</span>),
134+
(<span key="paymentDate">{translation.paymentDate}</span>),
135+
(<span key="actions">{translation.actions}</span>),
136+
]}
137+
rowMappingToCells={invoice => [
138+
(<span key={invoice.uuid + '-date'}>{localeTranslation.formatDate(invoice.date)}</span>),
139+
(<span key={invoice.uuid + '-name'}>{invoice.title}</span>),
140+
(
141+
<span key={invoice.uuid + '-price'} className={tw('font-semibold')}>
142+
{localeTranslation.formatMoney(invoice.totalAmount)}
172143
</span>
173-
),
174-
(
175-
<span
176-
key={invoice.uuid + '-payment-status'}
177-
className={tx('w-full px-4 py-2 rounded-full',{
178-
'bg-hw-negative-500 text-white': invoice.status === 'overdue',
179-
'bg-hw-warn-500 text-white': invoice.status === 'pending',
180-
'bg-hw-positive-500 text-white': invoice.status === 'paid'
181-
})}
182-
>
144+
),
145+
(
146+
<span
147+
key={invoice.uuid + '-payment-status'}
148+
className={tx('w-full px-4 py-2 rounded-full', {
149+
'bg-hw-negative-500 text-white': invoice.status === 'overdue',
150+
'bg-hw-warn-500 text-white': invoice.status === 'pending',
151+
'bg-hw-positive-500 text-white': invoice.status === 'paid'
152+
})}
153+
>
183154
{translation[invoice.status]}
184155
</span>
185-
),
186-
(
187-
<span key={invoice.uuid + '-payment-date'}>
188-
{invoice.status === 'paid' && invoice.createdAt != undefined ? formatDate(invoice.updatedAt) : '-'}
156+
),
157+
(
158+
<span key={invoice.uuid + '-payment-date'}>
159+
{invoice.status === 'paid' && invoice.createdAt != undefined ? localeTranslation.formatDate(invoice.updatedAt) : '-'}
189160
</span>
190-
),
191-
invoice.status === 'overdue' || invoice.status === 'pending' ?
192-
(
193-
<EmbeddedCheckoutButton
194-
key={invoice.uuid + '-pay'}
195-
invoiceId={invoice.uuid}
196-
>
197-
{translation.payNow}
198-
</EmbeddedCheckoutButton>
199-
) : (
200-
invoice.status !== 'paid' ?
201-
(
202-
<Button
203-
disabled={true}
204-
variant="text-border"
205-
className={tw('mr-2')}>
206-
{translation[invoice.status]}
207-
</Button>
208-
) :
209-
(
210-
<Button>
211-
PDF
212-
</Button>
213-
)
214-
)
215-
]}
216-
/>
217-
</div>
218-
) :
161+
),
162+
invoice.status === 'overdue' || invoice.status === 'pending' ?
163+
(
164+
<EmbeddedCheckoutButton
165+
key={invoice.uuid + '-pay'}
166+
invoiceId={invoice.uuid}
167+
>
168+
{translation.payNow}
169+
</EmbeddedCheckoutButton>
170+
) : (
171+
invoice.status !== 'paid' ?
172+
(
173+
<Button
174+
disabled={true}
175+
variant="text-border"
176+
className={tw('mr-2')}>
177+
{translation[invoice.status]}
178+
</Button>
179+
) :
180+
(
181+
<Button>
182+
PDF
183+
</Button>
184+
)
185+
)
186+
]}
187+
/>
188+
</div>
189+
) :
219190
(
220191
<div className={tw('flex flex-row justify-center w-full text-gray-400')}>{translation.noInvoices}</div>
221192
)}

customer/pages/products/index.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ import type {
2626
CustomerProductStatus,
2727
ResolvedCustomerProduct
2828
} from '@/api/dataclasses/customer_product'
29+
import {
30+
CustomerProductStatusCancelable,
31+
CustomerProductStatusPlannedCancellation
32+
} from '@/api/dataclasses/customer_product'
2933
import {
3034
defaultCustomerProductStatusTranslation
3135
} from '@/api/dataclasses/customer_product'
3236
import { ContractList } from '@/components/ContractList'
37+
import { defaultLocaleFormatters } from '@/utils/locale'
3338

3439
type ProductsTranslation = {
3540
bookProduct: string,
@@ -45,8 +50,11 @@ type ProductsTranslation = {
4550
lookAt: string,
4651
cancelSubscription: string,
4752
cancelSubscriptionDescription: string,
53+
endsAt: string,
4854
} & ProductPlanTranslation
4955

56+
57+
5058
const defaultProductsTranslations: Record<Languages, ProductsTranslation> = {
5159
en: {
5260
...defaultProductPlanTranslation.en,
@@ -63,6 +71,7 @@ const defaultProductsTranslations: Record<Languages, ProductsTranslation> = {
6371
lookAt: 'Look at',
6472
cancelSubscription: 'Cancel',
6573
cancelSubscriptionDescription: 'This will permantly cancel your subscription and you will be required to book the product again.',
74+
endsAt: 'Ends at',
6675
},
6776
de: {
6877
...defaultProductPlanTranslation.de,
@@ -74,11 +83,12 @@ const defaultProductsTranslations: Record<Languages, ProductsTranslation> = {
7483
contract: 'Vertrag',
7584
contracts: 'Verträge',
7685
manage: 'Verwalten',
77-
cancel: 'Cancel',
86+
cancel: 'Kündigen',
7887
noContracts: 'Keine Verträge',
7988
lookAt: 'Anzeigen',
8089
cancelSubscription: 'Abonement aufheben',
8190
cancelSubscriptionDescription: 'Das Produkt wird permanent deaboniert und sie müssen zur erneuten Nutzung dieses erneut buchen.',
91+
endsAt: 'Endet am',
8292
}
8393
}
8494

@@ -112,6 +122,7 @@ const ProductsPage: NextPage = () => {
112122
const [customerProductModalValue, setCustomerProductModalValue] = useState<ResolvedCustomerProduct>()
113123
const [cancelDialogId, setCancelDialogId] = useState<string>()
114124
const cancelMutation = useCustomerProductDeleteMutation()
125+
const localeTranslation = useTranslation(defaultLocaleFormatters)
115126

116127
const isError = bookedProductsError || productsError
117128
const isLoading = bookedProductsLoading || productsLoading
@@ -129,8 +140,8 @@ const ProductsPage: NextPage = () => {
129140
<span>{customerProductModalValue?.product.description}</span>
130141
<ContractList
131142
productIds={customerProductModalValue ? [customerProductModalValue.product.uuid] : []}></ContractList>
132-
<Button color="hw-negative" className={tw('!w-fit')}
133-
onClick={() => setCancelDialogId(customerProductModalValue?.uuid)}>
143+
<Button color="hw-negative" className={tw('!w-fit')} disabled={!CustomerProductStatusCancelable.includes(customerProductModalValue?.status || 'active')}
144+
onClick={() => setCancelDialogId(customerProductModalValue?.uuid)}>
134145
{translation.cancel}
135146
</Button>
136147
</Modal>
@@ -151,26 +162,27 @@ const ProductsPage: NextPage = () => {
151162

152163
<Section titleText={translation.myProducts}>
153164
{isError && (<span>{translation.error}</span>)}
154-
{!isError && isLoading && (<LoadingAnimation/>)}
165+
{!isError && isLoading && (<LoadingAnimation />)}
155166
{!isError && !isLoading && products && bookedProducts && (
156167
<div className={tw('grid grid-cols-1 desktop:grid-cols-2 gap-x-8 gap-y-12')}>
157168
{bookedProducts.map((bookedProduct, index) => {
158169
return (
159170
<div
160171
key={index}
161-
className={tw('flex flex-col justify-between gap-y-2 bg-gray-100 px-4 py-2 rounded-md')}
172+
className={tw('flex flex-col border-4 justify-between gap-y-2 bg-gray-100 px-8 py-4 rounded-md')}
162173
>
163174
<div className={tw('flex flex-col gap-y-2')}>
164175
<div className={tw('flex flex-row gap-x-2 justify-between')}>
165176
<h4 className={tw('font-bold font-space text-2xl')}>{bookedProduct.product.name}</h4>
166-
<CustomerProductStatusDisplay customerProductStatus={bookedProduct.status}/>
177+
<CustomerProductStatusDisplay customerProductStatus={bookedProduct.status} />
167178
</div>
168179
<span>{bookedProduct.product.description}</span>
169-
<span>{`${translation.productPlan(bookedProduct.productPlan)} (${bookedProduct.productPlan.costEuro}€)`}</span>
180+
<span>{`${translation.productPlan(bookedProduct.productPlan)} (${localeTranslation.formatMoney(bookedProduct.productPlan.costEuro)})`}</span>
181+
{bookedProduct.cancellationDate && CustomerProductStatusPlannedCancellation.includes(bookedProduct.status) ? <span>{translation.endsAt} {localeTranslation.formatDate(bookedProduct.cancellationDate)}</span> : <></>}
170182
</div>
171183
<div className={tw('flex flex-row justify-end gap-x-4')}>
172184
<Button className={tw('!w-fit')}
173-
onClick={() => setCustomerProductModalValue(bookedProduct)}>{translation.manage}</Button>
185+
onClick={() => setCustomerProductModalValue(bookedProduct)}>{translation.manage}</Button>
174186
</div>
175187
</div>
176188
)
@@ -180,7 +192,7 @@ const ProductsPage: NextPage = () => {
180192
onClick={() => router.push('/products/shop').catch(console.error)}
181193
className={tw('flex flex-row justify-center items-center h-full min-h-[200px] w-full gap-x-2 border-4 border-dashed border-gray-200 hover:brightness-90 px-4 py-2 rounded-md')}
182194
>
183-
<Plus size={32}/>
195+
<Plus size={32} />
184196
<h4 className={tw('font-bold text-lg')}>{translation.bookProduct}</h4>
185197
</button>
186198
</div>

customer/pages/products/overview.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { VoucherAPI } from '@/api/services/voucher'
2828
import type { Voucher } from '@/api/dataclasses/voucher'
2929
import { Chip } from '@helpwave/common/components/ChipList'
3030
import { useCustomerProductsCalculateQuery } from '@/api/mutations/customer_product_mutations'
31+
import { defaultLocaleFormatters } from '@/utils/locale'
3132
import { ProductAPI } from '@/api/services/product'
3233
import { LoadingAnimation } from '@helpwave/common/components/LoadingAnimation'
3334

@@ -123,6 +124,7 @@ const CartOverview: NextPage = () => {
123124
voucherUuid: item.voucher?.uuid
124125
})))
125126

127+
const localeTranslation = useTranslation(defaultLocaleFormatters)
126128
useEffect(() => {
127129
refetch().catch(console.error)
128130
}, [cart])
@@ -250,7 +252,7 @@ const CartOverview: NextPage = () => {
250252
return [
251253
<span key={cartItem.id + 'name'}>{product?.name ?? 'Not Found'}</span>,
252254
<span key={cartItem.id + 'price'} className={tx({ 'text-hw-primary-500': priceResult.saving !== 0 })}>
253-
{`${priceResult.finalPrice}€`}
255+
{localeTranslation.formatMoney(priceResult.finalPrice)}
254256
</span>,
255257
<span key={cartItem.id + 'plan'}>{plan ? translation.productPlan(plan) : ''}</span>,
256258
!voucher ? (
@@ -288,15 +290,15 @@ const CartOverview: NextPage = () => {
288290
className={tw('flex flex-row items-center gap-x-2')}
289291
onClick={() => router.push('/products/shop')}
290292
>
291-
<ChevronLeft size={20}/>
293+
<ChevronLeft size={20} />
292294
{`${translation.back}`}
293295
</Button>
294296
<Button
295297
className={tw('flex flex-row items-center gap-x-2')}
296298
onClick={() => router.push('/products/pay')}
297299
disabled={cart.length === 0}
298300
>
299-
<Coins size={20}/>
301+
<Coins size={20} />
300302
{`${translation.checkout}`}
301303
</Button>
302304
</Section>

0 commit comments

Comments
 (0)