@tuturuuu/ui 0.5.0 → 0.6.1

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +5 -0
  3. package/package.json +42 -35
  4. package/src/components/ui/currency-input.tsx +65 -23
  5. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  6. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  7. package/src/components/ui/custom/combobox.test.tsx +141 -0
  8. package/src/components/ui/custom/combobox.tsx +105 -36
  9. package/src/components/ui/custom/settings/task-settings.tsx +50 -0
  10. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
  11. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  12. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  13. package/src/components/ui/finance/finance-layout.tsx +2 -4
  14. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  15. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  16. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  17. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  19. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  20. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  21. package/src/components/ui/finance/transactions/form.tsx +81 -22
  22. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  23. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
  24. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  25. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
  29. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  30. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  31. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  32. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  33. package/src/components/ui/finance/wallets/form.tsx +41 -197
  34. package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
  35. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  36. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  37. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  40. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  41. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  42. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
  43. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
  44. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  45. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  46. package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
  47. package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
  48. package/src/components/ui/storefront/accent-button.tsx +33 -0
  49. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  50. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  51. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  52. package/src/components/ui/storefront/image-panel.tsx +40 -0
  53. package/src/components/ui/storefront/index.ts +12 -0
  54. package/src/components/ui/storefront/listing-card.tsx +129 -0
  55. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  56. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  57. package/src/components/ui/storefront/types.ts +99 -0
  58. package/src/components/ui/storefront/utils.ts +90 -0
  59. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  60. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  61. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  62. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  63. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  64. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  65. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  66. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
  67. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  68. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  69. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  70. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  71. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  72. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  73. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  74. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  86. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
  87. package/src/hooks/useBoardRealtime.ts +54 -1
  88. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  89. 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 { Calculator } from '@tuturuuu/icons';
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
- wallets,
53
+ currency,
36
54
  wsId,
37
55
  }: {
38
56
  canUpdateWallets: boolean;
39
- wallets: WalletInput[];
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
- {wallets.length === 0 ? (
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
- <div
156
+ <WalletTotalCheckRow
102
157
  key={wallet.id}
103
- className="grid gap-2 rounded-md border p-3"
104
- >
105
- <div className="flex items-center justify-between gap-3">
106
- <div className="min-w-0">
107
- <div className="truncate font-medium text-sm">
108
- {wallet.name}
109
- </div>
110
- <div className="text-muted-foreground text-xs">
111
- {t('ledger_balance')}:{' '}
112
- <WalletCheckpointAmount
113
- amount={wallet.balance ?? 0}
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={!canSubmit || mutation.isPending}
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
+ });