@tuturuuu/ui 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +126 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
- package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- package/src/hooks/use-task-actions.ts +45 -0
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -1,34 +1,254 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
BookOpen,
|
|
5
|
+
Scale,
|
|
6
|
+
TrendingDown,
|
|
7
|
+
TrendingUp,
|
|
8
|
+
TriangleAlert,
|
|
9
|
+
} from '@tuturuuu/icons';
|
|
10
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
11
|
+
import type { ExchangeRate } from '@tuturuuu/utils/exchange-rates';
|
|
12
|
+
import { convertCurrency } from '@tuturuuu/utils/exchange-rates';
|
|
13
|
+
import { cn, formatCurrency, getCurrencyLocale } from '@tuturuuu/utils/format';
|
|
14
|
+
import { useTranslations } from 'next-intl';
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
16
|
+
import { useFinanceBalanceMode } from '../../shared/use-finance-balance-mode';
|
|
3
17
|
import {
|
|
4
18
|
FINANCE_HIDDEN_AMOUNT,
|
|
5
19
|
useFinanceConfidentialVisibility,
|
|
6
20
|
} from '../../shared/use-finance-confidential-visibility';
|
|
21
|
+
import {
|
|
22
|
+
getWalletBalanceTone,
|
|
23
|
+
resolveWalletBalanceForMode,
|
|
24
|
+
} from '../../shared/wallet-balance-mode';
|
|
7
25
|
|
|
8
26
|
interface WalletDetailsAmountProps {
|
|
27
|
+
auditedBalance?: number | null;
|
|
28
|
+
auditStatus?: 'clean' | 'no_checkpoint' | 'unresolved' | null;
|
|
29
|
+
auditVariance?: number | null;
|
|
30
|
+
currency?: string;
|
|
9
31
|
primary: string;
|
|
10
32
|
converted?: string | null;
|
|
33
|
+
exchangeRates?: ExchangeRate[];
|
|
34
|
+
ledgerBalance?: number | null;
|
|
35
|
+
workspaceCurrency?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type AmountBadgeTone = ReturnType<typeof getWalletBalanceTone> | 'varied';
|
|
39
|
+
|
|
40
|
+
function getAmountBadgeClassName(tone: AmountBadgeTone) {
|
|
41
|
+
if (tone === 'varied') {
|
|
42
|
+
return 'border-dynamic-orange/40 bg-dynamic-orange/10 font-semibold text-dynamic-orange';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (tone === 'positive') {
|
|
46
|
+
return 'border-dynamic-green/30 bg-dynamic-green/10 font-semibold text-dynamic-green';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (tone === 'negative') {
|
|
50
|
+
return 'border-dynamic-red/30 bg-dynamic-red/10 font-semibold text-dynamic-red';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return 'font-semibold text-muted-foreground';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getContextAmountBadgeClassName(tone: 'ledger' | 'variance') {
|
|
57
|
+
if (tone === 'ledger') {
|
|
58
|
+
return 'border-dynamic-blue/30 bg-dynamic-blue/10 font-semibold text-dynamic-blue';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return 'border-dynamic-purple/30 bg-dynamic-purple/10 font-semibold text-dynamic-purple';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function AmountBadge({
|
|
65
|
+
children,
|
|
66
|
+
icon,
|
|
67
|
+
tone,
|
|
68
|
+
}: {
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
icon?: ReactNode;
|
|
71
|
+
tone: AmountBadgeTone;
|
|
72
|
+
}) {
|
|
73
|
+
return (
|
|
74
|
+
<Badge
|
|
75
|
+
variant="outline"
|
|
76
|
+
data-wallet-details-balance-badge={tone}
|
|
77
|
+
className={cn(
|
|
78
|
+
'flex w-fit items-center gap-1 whitespace-nowrap px-2 py-1 text-sm',
|
|
79
|
+
getAmountBadgeClassName(tone)
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{icon}
|
|
83
|
+
<span>{children}</span>
|
|
84
|
+
</Badge>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ContextAmountBadge({
|
|
89
|
+
children,
|
|
90
|
+
icon,
|
|
91
|
+
label,
|
|
92
|
+
tone,
|
|
93
|
+
}: {
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
icon?: ReactNode;
|
|
96
|
+
label: string;
|
|
97
|
+
tone: 'ledger' | 'variance';
|
|
98
|
+
}) {
|
|
99
|
+
return (
|
|
100
|
+
<Badge
|
|
101
|
+
variant="outline"
|
|
102
|
+
className={cn(
|
|
103
|
+
'flex w-fit items-center gap-1 whitespace-nowrap',
|
|
104
|
+
getContextAmountBadgeClassName(tone)
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{icon}
|
|
108
|
+
<span className="font-medium opacity-75">{label}</span>
|
|
109
|
+
<span>{children}</span>
|
|
110
|
+
</Badge>
|
|
111
|
+
);
|
|
11
112
|
}
|
|
12
113
|
|
|
13
114
|
export function WalletDetailsAmount({
|
|
115
|
+
auditedBalance,
|
|
116
|
+
auditStatus,
|
|
117
|
+
auditVariance,
|
|
118
|
+
currency,
|
|
14
119
|
primary,
|
|
15
120
|
converted,
|
|
121
|
+
exchangeRates,
|
|
122
|
+
ledgerBalance,
|
|
123
|
+
workspaceCurrency,
|
|
16
124
|
}: WalletDetailsAmountProps) {
|
|
125
|
+
const t = useTranslations('wallet-checkpoints');
|
|
17
126
|
const { isConfidential: areNumbersHidden } =
|
|
18
127
|
useFinanceConfidentialVisibility();
|
|
128
|
+
const { mode } = useFinanceBalanceMode();
|
|
19
129
|
|
|
20
130
|
if (areNumbersHidden) {
|
|
21
131
|
return (
|
|
22
|
-
<
|
|
132
|
+
<Badge variant="outline" className="font-semibold text-muted-foreground">
|
|
133
|
+
{FINANCE_HIDDEN_AMOUNT}
|
|
134
|
+
</Badge>
|
|
23
135
|
);
|
|
24
136
|
}
|
|
25
137
|
|
|
138
|
+
const {
|
|
139
|
+
auditStatus: resolvedAuditStatus,
|
|
140
|
+
auditVariance: resolvedAuditVariance,
|
|
141
|
+
contextBalance,
|
|
142
|
+
displayBalance,
|
|
143
|
+
hasAuditedBalance,
|
|
144
|
+
isAuditedMode,
|
|
145
|
+
} = resolveWalletBalanceForMode(
|
|
146
|
+
{
|
|
147
|
+
audit_balance: auditedBalance,
|
|
148
|
+
audit_status: auditStatus ?? undefined,
|
|
149
|
+
audit_variance: auditVariance,
|
|
150
|
+
balance: ledgerBalance,
|
|
151
|
+
},
|
|
152
|
+
mode
|
|
153
|
+
);
|
|
154
|
+
const hasResolvableBalance =
|
|
155
|
+
typeof ledgerBalance === 'number' || typeof auditedBalance === 'number';
|
|
156
|
+
const resolvedCurrency = currency ?? 'USD';
|
|
157
|
+
const resolvedWorkspaceCurrency = workspaceCurrency ?? resolvedCurrency;
|
|
158
|
+
const displayPrimary = hasResolvableBalance
|
|
159
|
+
? Intl.NumberFormat(getCurrencyLocale(resolvedCurrency), {
|
|
160
|
+
style: 'currency',
|
|
161
|
+
currency: resolvedCurrency,
|
|
162
|
+
}).format(displayBalance)
|
|
163
|
+
: primary;
|
|
164
|
+
let displayConverted = converted;
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
typeof displayBalance === 'number' &&
|
|
168
|
+
resolvedCurrency !== resolvedWorkspaceCurrency &&
|
|
169
|
+
exchangeRates &&
|
|
170
|
+
exchangeRates.length > 0 &&
|
|
171
|
+
displayBalance !== 0
|
|
172
|
+
) {
|
|
173
|
+
const convertedBalance = convertCurrency(
|
|
174
|
+
displayBalance,
|
|
175
|
+
resolvedCurrency,
|
|
176
|
+
resolvedWorkspaceCurrency,
|
|
177
|
+
exchangeRates
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (convertedBalance !== null) {
|
|
181
|
+
displayConverted = formatCurrency(
|
|
182
|
+
Math.abs(convertedBalance),
|
|
183
|
+
resolvedWorkspaceCurrency,
|
|
184
|
+
undefined,
|
|
185
|
+
{ signDisplay: 'never', maximumFractionDigits: 0 }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const showAuditContext =
|
|
191
|
+
hasAuditedBalance &&
|
|
192
|
+
resolvedAuditStatus &&
|
|
193
|
+
resolvedAuditStatus !== 'clean' &&
|
|
194
|
+
resolvedAuditStatus !== 'no_checkpoint' &&
|
|
195
|
+
resolvedAuditVariance !== null &&
|
|
196
|
+
resolvedAuditVariance !== 0;
|
|
197
|
+
const displayTone = showAuditContext
|
|
198
|
+
? 'varied'
|
|
199
|
+
: getWalletBalanceTone(displayBalance);
|
|
200
|
+
|
|
26
201
|
return (
|
|
27
|
-
<span>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
202
|
+
<span className="inline-flex flex-col items-start gap-1 align-middle">
|
|
203
|
+
<span className="flex flex-wrap items-center gap-1.5">
|
|
204
|
+
<AmountBadge
|
|
205
|
+
tone={displayTone}
|
|
206
|
+
icon={
|
|
207
|
+
displayTone === 'positive' ? (
|
|
208
|
+
<TrendingUp className="h-3.5 w-3.5" />
|
|
209
|
+
) : displayTone === 'negative' ? (
|
|
210
|
+
<TrendingDown className="h-3.5 w-3.5" />
|
|
211
|
+
) : displayTone === 'varied' ? (
|
|
212
|
+
<TriangleAlert className="h-3.5 w-3.5" />
|
|
213
|
+
) : undefined
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
{displayPrimary}
|
|
217
|
+
</AmountBadge>
|
|
218
|
+
{displayConverted && (
|
|
219
|
+
<span className="rounded-md border bg-muted/40 px-2 py-0.5 text-muted-foreground text-xs">
|
|
220
|
+
{'\u2248'} {displayConverted}
|
|
221
|
+
</span>
|
|
222
|
+
)}
|
|
223
|
+
</span>
|
|
224
|
+
{showAuditContext && typeof contextBalance === 'number' && (
|
|
225
|
+
<span className="flex flex-wrap items-center gap-1.5">
|
|
226
|
+
<ContextAmountBadge
|
|
227
|
+
tone="ledger"
|
|
228
|
+
icon={<BookOpen className="h-3 w-3" />}
|
|
229
|
+
label={isAuditedMode ? t('ledger') : t('audited')}
|
|
230
|
+
>
|
|
231
|
+
{formatCurrency(contextBalance, resolvedCurrency, undefined, {
|
|
232
|
+
signDisplay: 'auto',
|
|
233
|
+
})}
|
|
234
|
+
</ContextAmountBadge>
|
|
235
|
+
<ContextAmountBadge
|
|
236
|
+
tone="variance"
|
|
237
|
+
icon={<Scale className="h-3 w-3" />}
|
|
238
|
+
label={t('variance')}
|
|
239
|
+
>
|
|
240
|
+
{formatCurrency(
|
|
241
|
+
resolvedAuditVariance ?? 0,
|
|
242
|
+
resolvedCurrency,
|
|
243
|
+
undefined,
|
|
244
|
+
{ signDisplay: 'always' }
|
|
245
|
+
)}
|
|
246
|
+
</ContextAmountBadge>
|
|
247
|
+
</span>
|
|
248
|
+
)}
|
|
249
|
+
{isAuditedMode && resolvedAuditStatus === 'no_checkpoint' && (
|
|
250
|
+
<span className="text-muted-foreground text-xs">
|
|
251
|
+
{t('no_checkpoint_short')}
|
|
32
252
|
</span>
|
|
33
253
|
)}
|
|
34
254
|
</span>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
1
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
|
|
3
5
|
const mocks = vi.hoisted(() => {
|
|
4
6
|
const currencyRateLimit = vi.fn();
|
|
5
7
|
|
|
6
8
|
return {
|
|
7
|
-
|
|
9
|
+
createAdminClient: vi.fn(() => ({
|
|
8
10
|
from: vi.fn((table: string) => {
|
|
9
11
|
if (table === 'currency_exchange_rates') {
|
|
10
12
|
return {
|
|
@@ -31,6 +33,8 @@ const mocks = vi.hoisted(() => {
|
|
|
31
33
|
notFound: vi.fn(() => {
|
|
32
34
|
throw new Error('notFound');
|
|
33
35
|
}),
|
|
36
|
+
creditWalletSummary: vi.fn((_props: unknown) => null),
|
|
37
|
+
walletDetailsActions: vi.fn((_props: unknown) => null),
|
|
34
38
|
withForwardedInternalApiAuth: vi.fn(() => ({ headers: {} })),
|
|
35
39
|
};
|
|
36
40
|
});
|
|
@@ -44,8 +48,8 @@ vi.mock('@tuturuuu/internal-api', () => ({
|
|
|
44
48
|
}));
|
|
45
49
|
|
|
46
50
|
vi.mock('@tuturuuu/supabase/next/server', () => ({
|
|
47
|
-
|
|
48
|
-
mocks.
|
|
51
|
+
createAdminClient: (...args: Parameters<typeof mocks.createAdminClient>) =>
|
|
52
|
+
mocks.createAdminClient(...args),
|
|
49
53
|
}));
|
|
50
54
|
|
|
51
55
|
vi.mock('@tuturuuu/utils/workspace-helper', () => ({
|
|
@@ -96,8 +100,14 @@ vi.mock('../wallet-icon-display', () => ({
|
|
|
96
100
|
WalletIconDisplay: () => null,
|
|
97
101
|
}));
|
|
98
102
|
|
|
103
|
+
vi.mock('../checkpoints/wallet-checkpoint-panel', () => ({
|
|
104
|
+
WalletCheckpointPanel: () => null,
|
|
105
|
+
}));
|
|
106
|
+
|
|
99
107
|
vi.mock('./credit-wallet-summary', () => ({
|
|
100
|
-
CreditWalletSummary: (
|
|
108
|
+
CreditWalletSummary: (
|
|
109
|
+
...args: Parameters<typeof mocks.creditWalletSummary>
|
|
110
|
+
) => mocks.creditWalletSummary(...args),
|
|
101
111
|
}));
|
|
102
112
|
|
|
103
113
|
vi.mock('./interest', () => ({
|
|
@@ -105,7 +115,9 @@ vi.mock('./interest', () => ({
|
|
|
105
115
|
}));
|
|
106
116
|
|
|
107
117
|
vi.mock('./wallet-details-actions', () => ({
|
|
108
|
-
WalletDetailsActions: (
|
|
118
|
+
WalletDetailsActions: (
|
|
119
|
+
...args: Parameters<typeof mocks.walletDetailsActions>
|
|
120
|
+
) => mocks.walletDetailsActions(...args),
|
|
109
121
|
}));
|
|
110
122
|
|
|
111
123
|
vi.mock('./wallet-role-access-dialog', () => ({
|
|
@@ -158,4 +170,58 @@ describe('wallet details page', () => {
|
|
|
158
170
|
headers: {},
|
|
159
171
|
});
|
|
160
172
|
});
|
|
173
|
+
|
|
174
|
+
it('forwards credit wallet actions to the shared actions component', async () => {
|
|
175
|
+
mocks.getWallet.mockResolvedValue({
|
|
176
|
+
id: 'wallet-1',
|
|
177
|
+
name: 'Rewards Card',
|
|
178
|
+
type: 'CREDIT',
|
|
179
|
+
currency: 'USD',
|
|
180
|
+
balance: -120,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const { default: WalletDetailsPageModule } = await import(
|
|
184
|
+
'./wallet-details-page.js'
|
|
185
|
+
);
|
|
186
|
+
const WalletDetailsPage = WalletDetailsPageModule as unknown as (props: {
|
|
187
|
+
wsId: string;
|
|
188
|
+
walletId: string;
|
|
189
|
+
searchParams: {
|
|
190
|
+
action?: string;
|
|
191
|
+
q: string;
|
|
192
|
+
page: string;
|
|
193
|
+
pageSize: string;
|
|
194
|
+
};
|
|
195
|
+
}) => Promise<ReactElement>;
|
|
196
|
+
|
|
197
|
+
const element = await WalletDetailsPage({
|
|
198
|
+
wsId: 'ws-1',
|
|
199
|
+
walletId: 'wallet-1',
|
|
200
|
+
searchParams: { action: 'payment', q: '', page: '1', pageSize: '10' },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
render(element);
|
|
204
|
+
|
|
205
|
+
expect(mocks.walletDetailsActions).toHaveBeenCalled();
|
|
206
|
+
const actionProps = mocks.walletDetailsActions.mock.calls[0]?.[0];
|
|
207
|
+
|
|
208
|
+
expect(actionProps).toEqual(
|
|
209
|
+
expect.objectContaining({
|
|
210
|
+
initialAction: 'payment',
|
|
211
|
+
walletId: 'wallet-1',
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
expect(mocks.creditWalletSummary).toHaveBeenCalled();
|
|
215
|
+
const summaryProps = mocks.creditWalletSummary.mock.calls[0]?.[0];
|
|
216
|
+
|
|
217
|
+
expect(summaryProps).toEqual(
|
|
218
|
+
expect.objectContaining({
|
|
219
|
+
wsId: 'ws-1',
|
|
220
|
+
wallet: expect.objectContaining({
|
|
221
|
+
id: 'wallet-1',
|
|
222
|
+
type: 'CREDIT',
|
|
223
|
+
}),
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
});
|
|
161
227
|
});
|
|
@@ -12,8 +12,7 @@ import { InfiniteTransactionsList } from '@tuturuuu/ui/finance/transactions/infi
|
|
|
12
12
|
import { Separator } from '@tuturuuu/ui/separator';
|
|
13
13
|
import { Skeleton } from '@tuturuuu/ui/skeleton';
|
|
14
14
|
import type { ExchangeRate } from '@tuturuuu/utils/exchange-rates';
|
|
15
|
-
import {
|
|
16
|
-
import { formatCurrency, getCurrencyLocale } from '@tuturuuu/utils/format';
|
|
15
|
+
import { getCurrencyLocale } from '@tuturuuu/utils/format';
|
|
17
16
|
import {
|
|
18
17
|
getPermissions,
|
|
19
18
|
getWorkspace,
|
|
@@ -27,10 +26,14 @@ import { notFound } from 'next/navigation';
|
|
|
27
26
|
import { getTranslations } from 'next-intl/server';
|
|
28
27
|
import { Suspense } from 'react';
|
|
29
28
|
import { Card } from '../../../card';
|
|
29
|
+
import { WalletCheckpointPanel } from '../checkpoints/wallet-checkpoint-panel';
|
|
30
30
|
import { WalletIconDisplay } from '../wallet-icon-display';
|
|
31
31
|
import { CreditWalletSummary } from './credit-wallet-summary';
|
|
32
32
|
import { WalletInterestSection } from './interest';
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
type WalletDetailsAction,
|
|
35
|
+
WalletDetailsActions,
|
|
36
|
+
} from './wallet-details-actions';
|
|
34
37
|
import { WalletDetailsAmount } from './wallet-details-amount';
|
|
35
38
|
import WalletRoleAccessDialog from './wallet-role-access-dialog';
|
|
36
39
|
|
|
@@ -38,6 +41,7 @@ interface Props {
|
|
|
38
41
|
wsId: string;
|
|
39
42
|
walletId: string;
|
|
40
43
|
searchParams: {
|
|
44
|
+
action?: string;
|
|
41
45
|
q: string;
|
|
42
46
|
page: string;
|
|
43
47
|
pageSize: string;
|
|
@@ -54,6 +58,7 @@ interface Props {
|
|
|
54
58
|
export default async function WalletDetailsPage({
|
|
55
59
|
wsId,
|
|
56
60
|
walletId,
|
|
61
|
+
searchParams,
|
|
57
62
|
defaultCurrency,
|
|
58
63
|
internalApiOptions,
|
|
59
64
|
permissions,
|
|
@@ -113,31 +118,10 @@ export default async function WalletDetailsPage({
|
|
|
113
118
|
|
|
114
119
|
const currency = wallet.currency || resolvedDefaultCurrency || 'USD';
|
|
115
120
|
const workspaceCurrency = resolvedDefaultCurrency || 'USD';
|
|
121
|
+
const initialAction = getInitialWalletAction(searchParams.action);
|
|
116
122
|
|
|
117
123
|
// Fetch exchange rates for conversion display
|
|
118
124
|
const exchangeRates = await getExchangeRates();
|
|
119
|
-
let convertedBalanceText: string | null = null;
|
|
120
|
-
if (
|
|
121
|
-
currency !== workspaceCurrency &&
|
|
122
|
-
exchangeRates.length > 0 &&
|
|
123
|
-
wallet.balance &&
|
|
124
|
-
wallet.balance !== 0
|
|
125
|
-
) {
|
|
126
|
-
const converted = convertCurrency(
|
|
127
|
-
wallet.balance,
|
|
128
|
-
currency,
|
|
129
|
-
workspaceCurrency,
|
|
130
|
-
exchangeRates
|
|
131
|
-
);
|
|
132
|
-
if (converted !== null) {
|
|
133
|
-
convertedBalanceText = formatCurrency(
|
|
134
|
-
Math.abs(converted),
|
|
135
|
-
workspaceCurrency,
|
|
136
|
-
undefined,
|
|
137
|
-
{ signDisplay: 'never', maximumFractionDigits: 0 }
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
125
|
|
|
142
126
|
return (
|
|
143
127
|
<div className="flex min-h-full w-full flex-col">
|
|
@@ -160,6 +144,7 @@ export default async function WalletDetailsPage({
|
|
|
160
144
|
wsId={wsId}
|
|
161
145
|
walletId={walletId}
|
|
162
146
|
wallet={wallet as Wallet}
|
|
147
|
+
initialAction={initialAction}
|
|
163
148
|
canUpdateWallets={canUpdateWallets}
|
|
164
149
|
canCreateTransactions={canCreateTransactions}
|
|
165
150
|
canCreateConfidentialTransactions={canCreateConfidentialTransactions}
|
|
@@ -171,6 +156,12 @@ export default async function WalletDetailsPage({
|
|
|
171
156
|
/>
|
|
172
157
|
</div>
|
|
173
158
|
<Separator className="my-4" />
|
|
159
|
+
{wallet.type === 'CREDIT' && (
|
|
160
|
+
<>
|
|
161
|
+
<CreditWalletSummary wsId={wsId} wallet={wallet as Wallet} />
|
|
162
|
+
<Separator className="my-4" />
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
174
165
|
<div className="grid h-fit gap-4 md:grid-cols-2">
|
|
175
166
|
<Card className="grid gap-4">
|
|
176
167
|
<div className="grid h-fit gap-2 rounded-lg p-4">
|
|
@@ -194,11 +185,17 @@ export default async function WalletDetailsPage({
|
|
|
194
185
|
label={t('wallet-data-table.balance')}
|
|
195
186
|
value={
|
|
196
187
|
<WalletDetailsAmount
|
|
188
|
+
auditedBalance={wallet.audit_balance}
|
|
189
|
+
auditStatus={wallet.audit_status}
|
|
190
|
+
auditVariance={wallet.audit_variance}
|
|
191
|
+
currency={currency}
|
|
192
|
+
exchangeRates={exchangeRates}
|
|
193
|
+
ledgerBalance={wallet.balance ?? 0}
|
|
197
194
|
primary={Intl.NumberFormat(getCurrencyLocale(currency), {
|
|
198
195
|
style: 'currency',
|
|
199
196
|
currency,
|
|
200
197
|
}).format(wallet.balance || 0)}
|
|
201
|
-
|
|
198
|
+
workspaceCurrency={workspaceCurrency}
|
|
202
199
|
/>
|
|
203
200
|
}
|
|
204
201
|
/>
|
|
@@ -293,12 +290,15 @@ export default async function WalletDetailsPage({
|
|
|
293
290
|
<Separator className="my-4" />
|
|
294
291
|
</>
|
|
295
292
|
)}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
293
|
+
<WalletCheckpointPanel
|
|
294
|
+
wsId={wsId}
|
|
295
|
+
walletId={walletId}
|
|
296
|
+
walletName={wallet.name ?? t('ws-wallets.singular')}
|
|
297
|
+
currency={currency}
|
|
298
|
+
canUpdateWallets={canUpdateWallets}
|
|
299
|
+
canCreateTransactions={canCreateTransactions}
|
|
300
|
+
/>
|
|
301
|
+
<Separator className="my-4" />
|
|
302
302
|
{/* Interest Tracking Section - for Momo/ZaloPay wallets */}
|
|
303
303
|
<WalletInterestSection wsId={wsId} wallet={wallet as Wallet} />
|
|
304
304
|
<Suspense
|
|
@@ -331,6 +331,18 @@ export default async function WalletDetailsPage({
|
|
|
331
331
|
);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
function getInitialWalletAction(action?: string): WalletDetailsAction | null {
|
|
335
|
+
switch (action) {
|
|
336
|
+
case 'charge':
|
|
337
|
+
case 'payment':
|
|
338
|
+
case 'credit':
|
|
339
|
+
case 'edit':
|
|
340
|
+
return action;
|
|
341
|
+
default:
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
334
346
|
function DetailItem({
|
|
335
347
|
icon,
|
|
336
348
|
label,
|
|
@@ -342,9 +354,14 @@ function DetailItem({
|
|
|
342
354
|
}) {
|
|
343
355
|
if (!value) return undefined;
|
|
344
356
|
return (
|
|
345
|
-
<div className="flex items-
|
|
346
|
-
|
|
347
|
-
|
|
357
|
+
<div className="flex min-w-0 items-start gap-3 rounded-md py-1.5">
|
|
358
|
+
<span className="mt-0.5 flex shrink-0 text-muted-foreground [&_svg]:h-5 [&_svg]:w-5">
|
|
359
|
+
{icon}
|
|
360
|
+
</span>
|
|
361
|
+
<div className="min-w-0 flex-1">
|
|
362
|
+
<div className="text-muted-foreground text-xs">{label}</div>
|
|
363
|
+
<div className="min-w-0 font-medium">{value}</div>
|
|
364
|
+
</div>
|
|
348
365
|
</div>
|
|
349
366
|
);
|
|
350
367
|
}
|