@tuturuuu/ui 0.5.0 → 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 +29 -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 +50 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
- 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/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
- 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 +2 -1
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
- 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 +1 -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 +64 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -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 +111 -37
- package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
- 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/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/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -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 +128 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
- 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-name-input.test.tsx +17 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
- 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/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
4
|
-
import {
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import {
|
|
5
|
+
Calculator,
|
|
6
|
+
Loader2,
|
|
7
|
+
TrendingDown,
|
|
8
|
+
TrendingUp,
|
|
9
|
+
Wallet as WalletIcon,
|
|
10
|
+
} from '@tuturuuu/icons';
|
|
5
11
|
import {
|
|
6
12
|
createWalletCheckpointBatch,
|
|
13
|
+
listWallets,
|
|
7
14
|
type WalletCheckpointBatchPayload,
|
|
8
15
|
} from '@tuturuuu/internal-api/finance';
|
|
16
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
9
17
|
import { Button } from '@tuturuuu/ui/button';
|
|
10
18
|
import {
|
|
11
19
|
Dialog,
|
|
@@ -18,12 +26,22 @@ import {
|
|
|
18
26
|
import { Input } from '@tuturuuu/ui/input';
|
|
19
27
|
import { Label } from '@tuturuuu/ui/label';
|
|
20
28
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
29
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
21
30
|
import { useTranslations } from 'next-intl';
|
|
31
|
+
import type { ReactNode } from 'react';
|
|
22
32
|
import { useMemo, useState } from 'react';
|
|
33
|
+
import { useFinanceBalanceMode } from '../../shared/use-finance-balance-mode';
|
|
34
|
+
import {
|
|
35
|
+
getWalletBalanceTone,
|
|
36
|
+
resolveWalletBalanceForMode,
|
|
37
|
+
} from '../../shared/wallet-balance-mode';
|
|
23
38
|
import { invalidateWalletMutationQueries } from '../query-invalidation';
|
|
24
39
|
import { WalletCheckpointAmount } from './wallet-checkpoint-amount';
|
|
25
40
|
|
|
26
41
|
type WalletInput = {
|
|
42
|
+
audit_balance?: number | null;
|
|
43
|
+
audit_status?: 'clean' | 'no_checkpoint' | 'unresolved' | null;
|
|
44
|
+
audit_variance?: number | null;
|
|
27
45
|
balance?: number | null;
|
|
28
46
|
currency: string;
|
|
29
47
|
id: string;
|
|
@@ -32,17 +50,42 @@ type WalletInput = {
|
|
|
32
50
|
|
|
33
51
|
export function WalletTotalCheckDialog({
|
|
34
52
|
canUpdateWallets,
|
|
35
|
-
|
|
53
|
+
currency,
|
|
36
54
|
wsId,
|
|
37
55
|
}: {
|
|
38
56
|
canUpdateWallets: boolean;
|
|
39
|
-
|
|
57
|
+
currency: string;
|
|
40
58
|
wsId: string;
|
|
41
59
|
}) {
|
|
42
60
|
const t = useTranslations('wallet-checkpoints');
|
|
43
61
|
const queryClient = useQueryClient();
|
|
62
|
+
const { isAuditedMode } = useFinanceBalanceMode();
|
|
44
63
|
const [open, setOpen] = useState(false);
|
|
45
64
|
const [values, setValues] = useState<Record<string, string>>({});
|
|
65
|
+
const walletsQuery = useQuery({
|
|
66
|
+
enabled: open && canUpdateWallets,
|
|
67
|
+
queryFn: () => listWallets(wsId),
|
|
68
|
+
queryKey: ['wallets', wsId, 'all-wallet-check'],
|
|
69
|
+
});
|
|
70
|
+
const wallets = useMemo<WalletInput[]>(
|
|
71
|
+
() =>
|
|
72
|
+
(walletsQuery.data ?? []).flatMap((wallet) => {
|
|
73
|
+
if (!wallet.id) return [];
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
audit_balance: wallet.audit_balance,
|
|
78
|
+
audit_status: wallet.audit_status,
|
|
79
|
+
audit_variance: wallet.audit_variance,
|
|
80
|
+
balance: wallet.balance,
|
|
81
|
+
currency: wallet.currency || currency,
|
|
82
|
+
id: wallet.id,
|
|
83
|
+
name: wallet.name,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}),
|
|
87
|
+
[currency, walletsQuery.data]
|
|
88
|
+
);
|
|
46
89
|
const mutation = useMutation({
|
|
47
90
|
mutationFn: (payload: WalletCheckpointBatchPayload) =>
|
|
48
91
|
createWalletCheckpointBatch(wsId, payload),
|
|
@@ -70,6 +113,7 @@ export function WalletTotalCheckDialog({
|
|
|
70
113
|
return [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
71
114
|
}, [values, wallets]);
|
|
72
115
|
const canSubmit =
|
|
116
|
+
!walletsQuery.isLoading &&
|
|
73
117
|
wallets.length > 0 &&
|
|
74
118
|
wallets.every((wallet) => Number.isFinite(Number(values[wallet.id])));
|
|
75
119
|
|
|
@@ -91,54 +135,37 @@ export function WalletTotalCheckDialog({
|
|
|
91
135
|
{t('all_wallet_check_description')}
|
|
92
136
|
</DialogDescription>
|
|
93
137
|
</DialogHeader>
|
|
94
|
-
{
|
|
138
|
+
{walletsQuery.isLoading ? (
|
|
139
|
+
<div className="flex items-center gap-2 rounded-md border border-dashed p-4 text-muted-foreground text-sm">
|
|
140
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
141
|
+
{t('loading_wallets')}
|
|
142
|
+
</div>
|
|
143
|
+
) : walletsQuery.isError ? (
|
|
144
|
+
<div className="rounded-md border border-dynamic-red/30 bg-dynamic-red/5 p-4 text-dynamic-red text-sm">
|
|
145
|
+
{walletsQuery.error instanceof Error
|
|
146
|
+
? walletsQuery.error.message
|
|
147
|
+
: t('wallets_load_error')}
|
|
148
|
+
</div>
|
|
149
|
+
) : wallets.length === 0 ? (
|
|
95
150
|
<div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
|
|
96
151
|
{t('no_wallets')}
|
|
97
152
|
</div>
|
|
98
153
|
) : (
|
|
99
154
|
<div className="grid gap-3">
|
|
100
155
|
{wallets.map((wallet) => (
|
|
101
|
-
<
|
|
156
|
+
<WalletTotalCheckRow
|
|
102
157
|
key={wallet.id}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
currency={wallet.currency}
|
|
115
|
-
/>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
<div className="text-muted-foreground text-xs">
|
|
119
|
-
{wallet.currency}
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="grid gap-2">
|
|
123
|
-
<Label htmlFor={`wallet-check-${wallet.id}`}>
|
|
124
|
-
{t('actual_balance_with_currency', {
|
|
125
|
-
currency: wallet.currency,
|
|
126
|
-
})}
|
|
127
|
-
</Label>
|
|
128
|
-
<Input
|
|
129
|
-
id={`wallet-check-${wallet.id}`}
|
|
130
|
-
inputMode="decimal"
|
|
131
|
-
value={values[wallet.id] ?? ''}
|
|
132
|
-
onChange={(event) =>
|
|
133
|
-
setValues((current) => ({
|
|
134
|
-
...current,
|
|
135
|
-
[wallet.id]: event.target.value,
|
|
136
|
-
}))
|
|
137
|
-
}
|
|
138
|
-
placeholder="0"
|
|
139
|
-
/>
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
158
|
+
isAuditedMode={isAuditedMode}
|
|
159
|
+
onValueChange={(value) =>
|
|
160
|
+
setValues((current) => ({
|
|
161
|
+
...current,
|
|
162
|
+
[wallet.id]: value,
|
|
163
|
+
}))
|
|
164
|
+
}
|
|
165
|
+
t={t}
|
|
166
|
+
value={values[wallet.id] ?? ''}
|
|
167
|
+
wallet={wallet}
|
|
168
|
+
/>
|
|
142
169
|
))}
|
|
143
170
|
{totals.length > 0 && (
|
|
144
171
|
<div className="rounded-md border p-3">
|
|
@@ -168,7 +195,9 @@ export function WalletTotalCheckDialog({
|
|
|
168
195
|
{t('cancel')}
|
|
169
196
|
</Button>
|
|
170
197
|
<Button
|
|
171
|
-
disabled={
|
|
198
|
+
disabled={
|
|
199
|
+
!canSubmit || mutation.isPending || walletsQuery.isError
|
|
200
|
+
}
|
|
172
201
|
onClick={() => {
|
|
173
202
|
mutation.mutate({
|
|
174
203
|
checked_at: new Date().toISOString(),
|
|
@@ -187,3 +216,147 @@ export function WalletTotalCheckDialog({
|
|
|
187
216
|
</>
|
|
188
217
|
);
|
|
189
218
|
}
|
|
219
|
+
|
|
220
|
+
function getAmountBadgeClassName(
|
|
221
|
+
tone: ReturnType<typeof getWalletBalanceTone>
|
|
222
|
+
) {
|
|
223
|
+
if (tone === 'positive') {
|
|
224
|
+
return 'border-dynamic-green/30 bg-dynamic-green/10 font-semibold text-dynamic-green';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (tone === 'negative') {
|
|
228
|
+
return 'border-dynamic-red/30 bg-dynamic-red/10 font-semibold text-dynamic-red';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return 'font-semibold text-muted-foreground';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function AmountBadge({
|
|
235
|
+
amount,
|
|
236
|
+
currency,
|
|
237
|
+
icon,
|
|
238
|
+
label,
|
|
239
|
+
signDisplay = 'auto',
|
|
240
|
+
}: {
|
|
241
|
+
amount: number;
|
|
242
|
+
currency: string;
|
|
243
|
+
icon?: ReactNode;
|
|
244
|
+
label: string;
|
|
245
|
+
signDisplay?: 'auto' | 'always' | 'exceptZero' | 'never';
|
|
246
|
+
}) {
|
|
247
|
+
return (
|
|
248
|
+
<Badge
|
|
249
|
+
variant="outline"
|
|
250
|
+
className={cn(
|
|
251
|
+
'flex w-fit items-center gap-1 whitespace-nowrap',
|
|
252
|
+
getAmountBadgeClassName(getWalletBalanceTone(amount))
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
{icon}
|
|
256
|
+
<span className="font-medium opacity-75">{label}</span>
|
|
257
|
+
<WalletCheckpointAmount
|
|
258
|
+
amount={amount}
|
|
259
|
+
currency={currency}
|
|
260
|
+
signDisplay={signDisplay}
|
|
261
|
+
/>
|
|
262
|
+
</Badge>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function WalletTotalCheckRow({
|
|
267
|
+
isAuditedMode,
|
|
268
|
+
onValueChange,
|
|
269
|
+
t,
|
|
270
|
+
value,
|
|
271
|
+
wallet,
|
|
272
|
+
}: {
|
|
273
|
+
isAuditedMode: boolean;
|
|
274
|
+
onValueChange: (value: string) => void;
|
|
275
|
+
t: ReturnType<typeof useTranslations>;
|
|
276
|
+
value: string;
|
|
277
|
+
wallet: WalletInput;
|
|
278
|
+
}) {
|
|
279
|
+
const {
|
|
280
|
+
auditStatus,
|
|
281
|
+
auditVariance,
|
|
282
|
+
contextBalance,
|
|
283
|
+
displayBalance,
|
|
284
|
+
hasAuditedBalance,
|
|
285
|
+
usesAuditedBalance,
|
|
286
|
+
} = resolveWalletBalanceForMode(wallet, isAuditedMode ? 'audited' : 'ledger');
|
|
287
|
+
const displayTone = getWalletBalanceTone(displayBalance);
|
|
288
|
+
const varianceTone = getWalletBalanceTone(auditVariance ?? 0);
|
|
289
|
+
const showAuditContext =
|
|
290
|
+
hasAuditedBalance &&
|
|
291
|
+
auditStatus !== 'clean' &&
|
|
292
|
+
auditStatus !== 'no_checkpoint' &&
|
|
293
|
+
auditVariance !== null &&
|
|
294
|
+
auditVariance !== 0;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="grid gap-2 rounded-md border p-3">
|
|
298
|
+
<div className="flex items-center justify-between gap-3">
|
|
299
|
+
<div className="min-w-0">
|
|
300
|
+
<div className="truncate font-medium text-sm">{wallet.name}</div>
|
|
301
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
302
|
+
<AmountBadge
|
|
303
|
+
amount={displayBalance}
|
|
304
|
+
currency={wallet.currency}
|
|
305
|
+
icon={
|
|
306
|
+
displayTone === 'positive' ? (
|
|
307
|
+
<TrendingUp className="h-3 w-3" />
|
|
308
|
+
) : displayTone === 'negative' ? (
|
|
309
|
+
<TrendingDown className="h-3 w-3" />
|
|
310
|
+
) : undefined
|
|
311
|
+
}
|
|
312
|
+
label={usesAuditedBalance ? t('audited') : t('ledger')}
|
|
313
|
+
/>
|
|
314
|
+
{showAuditContext && contextBalance !== null && (
|
|
315
|
+
<>
|
|
316
|
+
<AmountBadge
|
|
317
|
+
amount={contextBalance}
|
|
318
|
+
currency={wallet.currency}
|
|
319
|
+
icon={<WalletIcon className="h-3 w-3" />}
|
|
320
|
+
label={isAuditedMode ? t('ledger') : t('audited')}
|
|
321
|
+
/>
|
|
322
|
+
<AmountBadge
|
|
323
|
+
amount={auditVariance ?? 0}
|
|
324
|
+
currency={wallet.currency}
|
|
325
|
+
icon={
|
|
326
|
+
varianceTone === 'negative' ? (
|
|
327
|
+
<TrendingDown className="h-3 w-3" />
|
|
328
|
+
) : (
|
|
329
|
+
<TrendingUp className="h-3 w-3" />
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
label={t('variance')}
|
|
333
|
+
signDisplay="always"
|
|
334
|
+
/>
|
|
335
|
+
</>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
{isAuditedMode && auditStatus === 'no_checkpoint' && (
|
|
339
|
+
<div className="text-muted-foreground text-xs">
|
|
340
|
+
{t('no_checkpoint_short')}
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
<div className="text-muted-foreground text-xs">{wallet.currency}</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="grid gap-2">
|
|
347
|
+
<Label htmlFor={`wallet-check-${wallet.id}`}>
|
|
348
|
+
{t('actual_balance_with_currency', {
|
|
349
|
+
currency: wallet.currency,
|
|
350
|
+
})}
|
|
351
|
+
</Label>
|
|
352
|
+
<Input
|
|
353
|
+
id={`wallet-check-${wallet.id}`}
|
|
354
|
+
inputMode="decimal"
|
|
355
|
+
value={value}
|
|
356
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
357
|
+
placeholder="0"
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { walletColumns } from './columns';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
isConfidential: false,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('next-intl', () => ({
|
|
12
|
+
useTranslations: () => (key: string) => key,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../shared/use-finance-confidential-visibility', () => ({
|
|
16
|
+
FINANCE_HIDDEN_AMOUNT: 'hidden amount',
|
|
17
|
+
useFinanceConfidentialVisibility: () => ({
|
|
18
|
+
isConfidential: mocks.isConfidential,
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
function renderBalanceCell(
|
|
23
|
+
wallet: Wallet,
|
|
24
|
+
balanceMode: 'audited' | 'ledger' = 'audited'
|
|
25
|
+
) {
|
|
26
|
+
const columns = walletColumns({
|
|
27
|
+
extraData: {
|
|
28
|
+
balanceMode,
|
|
29
|
+
currency: wallet.currency || 'USD',
|
|
30
|
+
},
|
|
31
|
+
namespace: 'wallet-data-table',
|
|
32
|
+
t: (key: string) => key,
|
|
33
|
+
});
|
|
34
|
+
const balanceColumn = columns.find((column) => column.id === 'balance');
|
|
35
|
+
|
|
36
|
+
if (!balanceColumn || typeof balanceColumn.cell !== 'function') {
|
|
37
|
+
throw new Error('Expected balance column cell renderer');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render(
|
|
41
|
+
balanceColumn.cell({
|
|
42
|
+
row: {
|
|
43
|
+
original: wallet,
|
|
44
|
+
},
|
|
45
|
+
} as never) as ReactElement
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('wallet balance badge rendering', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
mocks.isConfidential = false;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('suppresses ledger and variance badges for clean checkpoints', () => {
|
|
55
|
+
renderBalanceCell({
|
|
56
|
+
audit_balance: 100,
|
|
57
|
+
audit_status: 'clean',
|
|
58
|
+
audit_variance: 0,
|
|
59
|
+
balance: 100,
|
|
60
|
+
currency: 'USD',
|
|
61
|
+
} as Wallet);
|
|
62
|
+
|
|
63
|
+
expect(screen.queryByText('ledger')).not.toBeInTheDocument();
|
|
64
|
+
expect(screen.queryByText('variance')).not.toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shows distinct context badges only while hovering the balance', () => {
|
|
68
|
+
renderBalanceCell({
|
|
69
|
+
audit_balance: 95,
|
|
70
|
+
audit_status: 'unresolved',
|
|
71
|
+
audit_variance: -5,
|
|
72
|
+
balance: 100,
|
|
73
|
+
currency: 'USD',
|
|
74
|
+
} as Wallet);
|
|
75
|
+
|
|
76
|
+
expect(screen.queryByText('ledger')).not.toBeInTheDocument();
|
|
77
|
+
expect(screen.queryByText('variance')).not.toBeInTheDocument();
|
|
78
|
+
|
|
79
|
+
const trigger = screen
|
|
80
|
+
.getByText('$95.00')
|
|
81
|
+
.closest('[data-wallet-balance-trigger]');
|
|
82
|
+
const balanceBadge = screen
|
|
83
|
+
.getByText('$95.00')
|
|
84
|
+
.closest('[data-wallet-balance-badge="varied"]');
|
|
85
|
+
|
|
86
|
+
expect(trigger).not.toBeNull();
|
|
87
|
+
expect(balanceBadge).toHaveClass('text-dynamic-orange');
|
|
88
|
+
fireEvent.mouseEnter(trigger as Element);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText('ledger')).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByText('variance')).toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
screen
|
|
95
|
+
.getByText('ledger')
|
|
96
|
+
.closest('[data-wallet-balance-context-badge="ledger"]')
|
|
97
|
+
).toHaveClass('text-dynamic-blue');
|
|
98
|
+
expect(
|
|
99
|
+
screen
|
|
100
|
+
.getByText('variance')
|
|
101
|
+
.closest('[data-wallet-balance-context-badge="variance"]')
|
|
102
|
+
).toHaveClass('text-dynamic-purple');
|
|
103
|
+
|
|
104
|
+
fireEvent.mouseLeave(trigger as Element);
|
|
105
|
+
|
|
106
|
+
expect(screen.queryByText('ledger')).not.toBeInTheDocument();
|
|
107
|
+
expect(screen.queryByText('variance')).not.toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('hides all audit amount context when numbers are hidden', () => {
|
|
111
|
+
mocks.isConfidential = true;
|
|
112
|
+
|
|
113
|
+
renderBalanceCell({
|
|
114
|
+
audit_balance: 95,
|
|
115
|
+
audit_status: 'unresolved',
|
|
116
|
+
audit_variance: -5,
|
|
117
|
+
balance: 100,
|
|
118
|
+
currency: 'USD',
|
|
119
|
+
} as Wallet);
|
|
120
|
+
|
|
121
|
+
expect(screen.getByText('hidden amount')).toBeInTheDocument();
|
|
122
|
+
expect(screen.queryByText('ledger')).not.toBeInTheDocument();
|
|
123
|
+
expect(screen.queryByText('variance')).not.toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { walletColumns } from './columns';
|
|
4
|
+
|
|
5
|
+
function getBalanceAccessor(balanceMode: 'audited' | 'ledger') {
|
|
6
|
+
const columns = walletColumns({
|
|
7
|
+
extraData: {
|
|
8
|
+
balanceMode,
|
|
9
|
+
currency: 'USD',
|
|
10
|
+
},
|
|
11
|
+
namespace: 'wallet-data-table',
|
|
12
|
+
t: (key: string) => key,
|
|
13
|
+
});
|
|
14
|
+
const balanceColumn = columns.find((column) => column.id === 'balance');
|
|
15
|
+
|
|
16
|
+
if (!balanceColumn || !('accessorFn' in balanceColumn)) {
|
|
17
|
+
throw new Error('Expected balance column accessor');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return balanceColumn.accessorFn as (wallet: Wallet) => number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('wallet columns', () => {
|
|
24
|
+
it('sorts the balance column by ledger balance in ledger mode', () => {
|
|
25
|
+
const accessor = getBalanceAccessor('ledger');
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
accessor({
|
|
29
|
+
audit_balance: 500,
|
|
30
|
+
balance: 100,
|
|
31
|
+
} as Wallet)
|
|
32
|
+
).toBe(100);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('sorts the balance column by audited balance in audited mode', () => {
|
|
36
|
+
const accessor = getBalanceAccessor('audited');
|
|
37
|
+
|
|
38
|
+
expect(
|
|
39
|
+
accessor({
|
|
40
|
+
audit_balance: -50,
|
|
41
|
+
balance: 100,
|
|
42
|
+
} as Wallet)
|
|
43
|
+
).toBe(-50);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('falls back to ledger balance for audited sorting without a checkpoint', () => {
|
|
47
|
+
const accessor = getBalanceAccessor('audited');
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
accessor({
|
|
51
|
+
audit_status: 'no_checkpoint',
|
|
52
|
+
balance: 100,
|
|
53
|
+
} as Wallet)
|
|
54
|
+
).toBe(100);
|
|
55
|
+
});
|
|
56
|
+
});
|