@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +126 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  22. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  23. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  24. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  25. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  29. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  30. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
  34. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
  35. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  36. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  37. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  38. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  39. package/src/components/ui/finance/wallets/form.tsx +41 -197
  40. package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
  41. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  42. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  43. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  48. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
  49. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
  50. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  51. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  52. package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
  53. package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
  54. package/src/components/ui/storefront/accent-button.tsx +33 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  56. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  58. package/src/components/ui/storefront/image-panel.tsx +40 -0
  59. package/src/components/ui/storefront/index.ts +12 -0
  60. package/src/components/ui/storefront/listing-card.tsx +129 -0
  61. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  62. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  63. package/src/components/ui/storefront/types.ts +99 -0
  64. package/src/components/ui/storefront/utils.ts +90 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  67. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  68. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  69. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  70. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  71. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  72. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  73. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  74. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  75. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
  76. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  77. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  79. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  80. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  81. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  83. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  100. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
  101. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  102. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  103. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  104. package/src/hooks/use-task-actions.ts +45 -0
  105. package/src/hooks/useBoardRealtime.ts +54 -1
  106. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  107. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -0,0 +1,541 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import type { ReactElement, ReactNode } from 'react';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
6
+ import { WalletCheckpointAmount } from './wallet-checkpoint-amount';
7
+ import { WalletCheckpointHistoryDialog } from './wallet-checkpoint-history-dialog';
8
+ import { WalletCheckpointPanel } from './wallet-checkpoint-panel';
9
+ import { WalletTotalCheckDialog } from './wallet-total-check-dialog';
10
+
11
+ const mocks = vi.hoisted(() => ({
12
+ createWalletCheckpointBatch: vi.fn(),
13
+ createWalletCheckpointReconciliation: vi.fn(),
14
+ defaultReconciliationCategoryId: '',
15
+ deleteWalletCheckpoint: vi.fn(),
16
+ getWalletCheckpointHistory: vi.fn(),
17
+ isConfidential: true,
18
+ listWallets: vi.fn(),
19
+ listTransactionCategories: vi.fn(),
20
+ listWalletCheckpoints: vi.fn(),
21
+ success: vi.fn(),
22
+ }));
23
+
24
+ vi.mock('@tuturuuu/internal-api/finance', () => ({
25
+ createWalletCheckpoint: vi.fn(),
26
+ createWalletCheckpointBatch: (
27
+ ...args: Parameters<typeof mocks.createWalletCheckpointBatch>
28
+ ) => mocks.createWalletCheckpointBatch(...args),
29
+ createWalletCheckpointReconciliation: (
30
+ ...args: Parameters<typeof mocks.createWalletCheckpointReconciliation>
31
+ ) => mocks.createWalletCheckpointReconciliation(...args),
32
+ deleteWalletCheckpoint: (
33
+ ...args: Parameters<typeof mocks.deleteWalletCheckpoint>
34
+ ) => mocks.deleteWalletCheckpoint(...args),
35
+ getWalletCheckpointHistory: (
36
+ ...args: Parameters<typeof mocks.getWalletCheckpointHistory>
37
+ ) => mocks.getWalletCheckpointHistory(...args),
38
+ listWallets: (...args: Parameters<typeof mocks.listWallets>) =>
39
+ mocks.listWallets(...args),
40
+ listTransactionCategories: (
41
+ ...args: Parameters<typeof mocks.listTransactionCategories>
42
+ ) => mocks.listTransactionCategories(...args),
43
+ listWalletCheckpoints: (
44
+ ...args: Parameters<typeof mocks.listWalletCheckpoints>
45
+ ) => mocks.listWalletCheckpoints(...args),
46
+ updateWalletCheckpoint: vi.fn(),
47
+ }));
48
+
49
+ vi.mock('@tuturuuu/ui/hooks/use-workspace-config', () => ({
50
+ useWorkspaceConfig: () => ({
51
+ data: mocks.defaultReconciliationCategoryId,
52
+ isLoading: false,
53
+ }),
54
+ }));
55
+
56
+ vi.mock('@tuturuuu/ui/select', () => ({
57
+ Select: ({
58
+ children,
59
+ onValueChange,
60
+ value,
61
+ }: {
62
+ children: ReactNode;
63
+ onValueChange: (value: string) => void;
64
+ value: string;
65
+ }) => (
66
+ <select
67
+ onChange={(event) => onValueChange(event.target.value)}
68
+ value={value}
69
+ >
70
+ {children}
71
+ </select>
72
+ ),
73
+ SelectContent: ({ children }: { children: ReactNode }) => children,
74
+ SelectItem: ({ children, value }: { children: ReactNode; value: string }) => (
75
+ <option value={value}>
76
+ {typeof children === 'string' ? children : value}
77
+ </option>
78
+ ),
79
+ SelectTrigger: ({ children }: { children: ReactNode }) => children,
80
+ SelectValue: () => null,
81
+ }));
82
+
83
+ vi.mock('@tuturuuu/ui/sonner', () => ({
84
+ toast: {
85
+ error: vi.fn(),
86
+ success: (...args: Parameters<typeof mocks.success>) =>
87
+ mocks.success(...args),
88
+ },
89
+ }));
90
+
91
+ vi.mock('next-intl', () => ({
92
+ useLocale: () => 'en-US',
93
+ useTranslations:
94
+ () => (key: string, values?: Record<string, string | number>) =>
95
+ values ? `${key}:${Object.values(values).join(',')}` : key,
96
+ }));
97
+
98
+ vi.mock('../../shared/use-finance-confidential-visibility', () => ({
99
+ FINANCE_HIDDEN_AMOUNT: '•••••',
100
+ useFinanceConfidentialVisibility: () => ({
101
+ isConfidential: mocks.isConfidential,
102
+ }),
103
+ }));
104
+
105
+ function renderWithQueryClient(ui: ReactElement) {
106
+ const queryClient = new QueryClient({
107
+ defaultOptions: {
108
+ queries: {
109
+ retry: false,
110
+ },
111
+ },
112
+ });
113
+ render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
114
+ }
115
+
116
+ describe('wallet checkpoint UI', () => {
117
+ beforeEach(() => {
118
+ vi.clearAllMocks();
119
+ mocks.defaultReconciliationCategoryId = '';
120
+ mocks.isConfidential = true;
121
+ mocks.createWalletCheckpointReconciliation.mockResolvedValue({
122
+ checked_at: '2026-06-11T10:00:00.000Z',
123
+ checkpoint_id: 'checkpoint-1',
124
+ created: true,
125
+ offset_amount: -12.34,
126
+ transaction_id: 'tx-1',
127
+ wallet_id: 'wallet-1',
128
+ });
129
+ mocks.createWalletCheckpointBatch.mockResolvedValue({
130
+ data: [],
131
+ totals_by_currency: [],
132
+ });
133
+ mocks.getWalletCheckpointHistory.mockResolvedValue({
134
+ audit_statuses: [],
135
+ checkpoints: [],
136
+ intervals: [],
137
+ latest_checkpoints: [],
138
+ totals_by_currency: [],
139
+ wallets: [],
140
+ });
141
+ mocks.listWallets.mockResolvedValue([]);
142
+ mocks.listTransactionCategories.mockResolvedValue([]);
143
+ mocks.listWalletCheckpoints.mockResolvedValue({
144
+ data: [],
145
+ intervals: [],
146
+ latest: null,
147
+ });
148
+ });
149
+
150
+ it('masks checkpoint amounts in confidential mode', () => {
151
+ render(<WalletCheckpointAmount amount={123.45} currency="USD" />);
152
+
153
+ expect(screen.getByText('•••••')).toBeInTheDocument();
154
+ });
155
+
156
+ it('shows checkpoint amounts when confidential mode is disabled', () => {
157
+ mocks.isConfidential = false;
158
+
159
+ render(<WalletCheckpointAmount amount={123.45} currency="USD" />);
160
+
161
+ expect(screen.getByText('$123.45')).toBeInTheDocument();
162
+ });
163
+
164
+ it('saves all-wallet checks with typed decimal values intact', async () => {
165
+ mocks.listWallets.mockResolvedValue([
166
+ {
167
+ balance: 0,
168
+ currency: 'USD',
169
+ id: 'wallet-1',
170
+ name: 'Cash',
171
+ },
172
+ {
173
+ balance: 0,
174
+ currency: 'VND',
175
+ id: 'wallet-2',
176
+ name: 'Bank',
177
+ },
178
+ ]);
179
+
180
+ renderWithQueryClient(
181
+ <WalletTotalCheckDialog wsId="ws-1" canUpdateWallets currency="USD" />
182
+ );
183
+
184
+ expect(mocks.listWallets).not.toHaveBeenCalled();
185
+
186
+ fireEvent.click(screen.getByRole('button', { name: 'all_wallet_check' }));
187
+ await waitFor(() => {
188
+ expect(mocks.listWallets).toHaveBeenCalledWith('ws-1');
189
+ });
190
+ expect(
191
+ screen.getByRole('button', { name: 'save_checkpoints' })
192
+ ).toBeDisabled();
193
+
194
+ await screen.findByLabelText('actual_balance_with_currency:USD');
195
+
196
+ fireEvent.change(
197
+ screen.getByLabelText('actual_balance_with_currency:USD'),
198
+ {
199
+ target: { value: '12.3405' },
200
+ }
201
+ );
202
+ fireEvent.change(
203
+ screen.getByLabelText('actual_balance_with_currency:VND'),
204
+ {
205
+ target: { value: '-5000' },
206
+ }
207
+ );
208
+ fireEvent.click(screen.getByRole('button', { name: 'save_checkpoints' }));
209
+
210
+ await waitFor(() => {
211
+ expect(mocks.createWalletCheckpointBatch).toHaveBeenCalledWith('ws-1', {
212
+ checked_at: expect.any(String),
213
+ entries: [
214
+ {
215
+ actual_balance: 12.3405,
216
+ wallet_id: 'wallet-1',
217
+ },
218
+ {
219
+ actual_balance: -5000,
220
+ wallet_id: 'wallet-2',
221
+ },
222
+ ],
223
+ });
224
+ });
225
+ });
226
+
227
+ it('renders clean and unresolved checkpoint intervals', async () => {
228
+ mocks.listWalletCheckpoints.mockResolvedValue({
229
+ data: [
230
+ {
231
+ actual_balance: 120,
232
+ checked_at: '2026-06-11T10:00:00.000Z',
233
+ created_at: '2026-06-11T10:01:00.000Z',
234
+ created_by: 'user-1',
235
+ currency: 'USD',
236
+ current_ledger_balance: 115,
237
+ current_variance: 5,
238
+ id: 'checkpoint-2',
239
+ ledger_balance: 110,
240
+ note: null,
241
+ original_variance: 10,
242
+ updated_at: '2026-06-11T10:01:00.000Z',
243
+ wallet_id: 'wallet-1',
244
+ },
245
+ ],
246
+ intervals: [
247
+ {
248
+ actual_delta: 10,
249
+ end_actual_balance: 110,
250
+ end_checked_at: '2026-06-10T10:00:00.000Z',
251
+ end_checkpoint_id: 'checkpoint-1',
252
+ interval_variance: 0,
253
+ is_clean: true,
254
+ ledger_delta: 10,
255
+ start_actual_balance: 100,
256
+ start_checked_at: '2026-06-09T10:00:00.000Z',
257
+ start_checkpoint_id: 'checkpoint-0',
258
+ transaction_count: 1,
259
+ },
260
+ {
261
+ actual_delta: 10,
262
+ end_actual_balance: 120,
263
+ end_checked_at: '2026-06-11T10:00:00.000Z',
264
+ end_checkpoint_id: 'checkpoint-2',
265
+ interval_variance: 5,
266
+ is_clean: false,
267
+ ledger_delta: 5,
268
+ start_actual_balance: 110,
269
+ start_checked_at: '2026-06-10T10:00:00.000Z',
270
+ start_checkpoint_id: 'checkpoint-1',
271
+ transaction_count: 2,
272
+ },
273
+ ],
274
+ latest: {
275
+ actual_balance: 120,
276
+ checked_at: '2026-06-11T10:00:00.000Z',
277
+ created_at: '2026-06-11T10:01:00.000Z',
278
+ created_by: 'user-1',
279
+ currency: 'USD',
280
+ current_ledger_balance: 115,
281
+ current_variance: 5,
282
+ id: 'checkpoint-2',
283
+ ledger_balance: 110,
284
+ note: null,
285
+ original_variance: 10,
286
+ updated_at: '2026-06-11T10:01:00.000Z',
287
+ wallet_id: 'wallet-1',
288
+ },
289
+ });
290
+
291
+ renderWithQueryClient(
292
+ <WalletCheckpointPanel
293
+ wsId="ws-1"
294
+ walletId="wallet-1"
295
+ walletName="Cash"
296
+ currency="USD"
297
+ canCreateTransactions
298
+ canUpdateWallets
299
+ />
300
+ );
301
+
302
+ expect(await screen.findByText('clean')).toBeInTheDocument();
303
+ expect(screen.getByText('unresolved')).toBeInTheDocument();
304
+ expect(
305
+ screen.getByRole('button', { name: 'reconcile' })
306
+ ).toBeInTheDocument();
307
+ expect(
308
+ screen.getByRole('button', { name: 'edit_checkpoint' })
309
+ ).toBeInTheDocument();
310
+ expect(
311
+ screen.getByRole('button', { name: 'delete_checkpoint' })
312
+ ).toBeInTheDocument();
313
+ });
314
+
315
+ it('submits checkpoint reconciliations through the recomputing endpoint', async () => {
316
+ renderWithQueryClient(
317
+ <WalletCheckpointAdjustmentDialog
318
+ wsId="ws-1"
319
+ walletId="wallet-1"
320
+ checkpointId="checkpoint-1"
321
+ walletName="Cash"
322
+ checkedAt="2026-06-11T10:00:00.000Z"
323
+ currency="USD"
324
+ variance={-12.34}
325
+ open
326
+ onOpenChange={vi.fn()}
327
+ onCreated={vi.fn()}
328
+ />
329
+ );
330
+
331
+ const reconcileButton = screen.getByRole('button', { name: 'reconcile' });
332
+ await waitFor(() => expect(reconcileButton).not.toBeDisabled());
333
+ fireEvent.click(reconcileButton);
334
+
335
+ await waitFor(() => {
336
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
337
+ 'ws-1',
338
+ 'wallet-1',
339
+ 'checkpoint-1',
340
+ expect.objectContaining({
341
+ category_id: undefined,
342
+ description: expect.stringContaining('Cash'),
343
+ })
344
+ );
345
+ });
346
+ });
347
+
348
+ it('preselects the configured reconciliation category', async () => {
349
+ mocks.defaultReconciliationCategoryId = 'category-reconcile';
350
+ mocks.listTransactionCategories.mockResolvedValue([
351
+ {
352
+ id: 'category-reconcile',
353
+ name: 'Audit',
354
+ },
355
+ ]);
356
+
357
+ renderWithQueryClient(
358
+ <WalletCheckpointAdjustmentDialog
359
+ wsId="ws-1"
360
+ walletId="wallet-1"
361
+ checkpointId="checkpoint-1"
362
+ walletName="Cash"
363
+ checkedAt="2026-06-11T10:00:00.000Z"
364
+ currency="USD"
365
+ variance={-12.34}
366
+ open
367
+ onOpenChange={vi.fn()}
368
+ onCreated={vi.fn()}
369
+ />
370
+ );
371
+
372
+ const reconcileButton = screen.getByRole('button', { name: 'reconcile' });
373
+ await waitFor(() => expect(reconcileButton).not.toBeDisabled());
374
+ fireEvent.click(reconcileButton);
375
+
376
+ await waitFor(() => {
377
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
378
+ 'ws-1',
379
+ 'wallet-1',
380
+ 'checkpoint-1',
381
+ expect.objectContaining({
382
+ category_id: 'category-reconcile',
383
+ })
384
+ );
385
+ });
386
+ });
387
+
388
+ it('submits no reconciliation category after clearing the configured default', async () => {
389
+ mocks.defaultReconciliationCategoryId = 'category-reconcile';
390
+ mocks.listTransactionCategories.mockResolvedValue([
391
+ {
392
+ id: 'category-reconcile',
393
+ name: 'Audit',
394
+ },
395
+ ]);
396
+
397
+ renderWithQueryClient(
398
+ <WalletCheckpointAdjustmentDialog
399
+ wsId="ws-1"
400
+ walletId="wallet-1"
401
+ checkpointId="checkpoint-1"
402
+ walletName="Cash"
403
+ checkedAt="2026-06-11T10:00:00.000Z"
404
+ currency="USD"
405
+ variance={-12.34}
406
+ open
407
+ onOpenChange={vi.fn()}
408
+ onCreated={vi.fn()}
409
+ />
410
+ );
411
+
412
+ const reconcileButton = screen.getByRole('button', { name: 'reconcile' });
413
+ await waitFor(() => expect(reconcileButton).not.toBeDisabled());
414
+ fireEvent.change(screen.getByRole('combobox'), {
415
+ target: { value: 'none' },
416
+ });
417
+ fireEvent.click(reconcileButton);
418
+
419
+ await waitFor(() => {
420
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
421
+ 'ws-1',
422
+ 'wallet-1',
423
+ 'checkpoint-1',
424
+ expect.objectContaining({
425
+ category_id: undefined,
426
+ })
427
+ );
428
+ });
429
+ });
430
+
431
+ it('renders checkpoint history and submits interval reconciliations with interval basis', async () => {
432
+ mocks.isConfidential = false;
433
+ mocks.getWalletCheckpointHistory.mockResolvedValue({
434
+ audit_statuses: [
435
+ {
436
+ audited_balance: 120,
437
+ checkpoint_ledger_balance: 110,
438
+ latest_actual_balance: 120,
439
+ latest_checked_at: '2026-06-11T10:00:00.000Z',
440
+ latest_checkpoint_id: 'checkpoint-2',
441
+ ledger_balance: 115,
442
+ post_checkpoint_delta: 0,
443
+ post_checkpoint_transaction_count: 0,
444
+ status: 'unresolved',
445
+ variance: 5,
446
+ wallet_id: 'wallet-1',
447
+ },
448
+ ],
449
+ checkpoints: [
450
+ {
451
+ actual_balance: 120,
452
+ checked_at: '2026-06-11T10:00:00.000Z',
453
+ created_at: '2026-06-11T10:01:00.000Z',
454
+ created_by: 'user-1',
455
+ currency: 'USD',
456
+ current_ledger_balance: 115,
457
+ current_variance: 5,
458
+ id: 'checkpoint-2',
459
+ ledger_balance: 110,
460
+ note: 'Audit',
461
+ original_variance: 10,
462
+ updated_at: '2026-06-11T10:01:00.000Z',
463
+ wallet_id: 'wallet-1',
464
+ },
465
+ ],
466
+ intervals: [
467
+ {
468
+ actual_delta: 20,
469
+ currency: 'USD',
470
+ end_actual_balance: 120,
471
+ end_checked_at: '2026-06-11T10:00:00.000Z',
472
+ end_checkpoint_id: 'checkpoint-2',
473
+ interval_variance: 5,
474
+ is_clean: false,
475
+ ledger_delta: 15,
476
+ start_actual_balance: 100,
477
+ start_checked_at: '2026-06-10T10:00:00.000Z',
478
+ start_checkpoint_id: 'checkpoint-1',
479
+ transaction_count: 3,
480
+ wallet_id: 'wallet-1',
481
+ wallet_name: 'Cash',
482
+ },
483
+ ],
484
+ latest_checkpoints: [],
485
+ totals_by_currency: [],
486
+ wallets: [
487
+ {
488
+ balance: 115,
489
+ currency: 'USD',
490
+ icon: null,
491
+ id: 'wallet-1',
492
+ image_src: null,
493
+ name: 'Cash',
494
+ type: 'STANDARD',
495
+ },
496
+ ],
497
+ });
498
+
499
+ renderWithQueryClient(
500
+ <WalletCheckpointHistoryDialog
501
+ wsId="ws-1"
502
+ financePrefix="/finance"
503
+ canCreateTransactions
504
+ />
505
+ );
506
+
507
+ fireEvent.click(screen.getByRole('button', { name: 'checkpoint_history' }));
508
+
509
+ expect(await screen.findByText('Cash')).toBeInTheDocument();
510
+ expect(screen.getByText('transaction_count')).toBeInTheDocument();
511
+
512
+ const firstReconcileButton = screen.getAllByRole('button', {
513
+ name: 'reconcile',
514
+ })[0];
515
+ if (!firstReconcileButton) {
516
+ throw new Error('Expected a reconcile button');
517
+ }
518
+ fireEvent.click(firstReconcileButton);
519
+ const reconcileButtons = screen.getAllByRole('button', {
520
+ name: 'reconcile',
521
+ });
522
+ const lastReconcileButton = reconcileButtons.at(-1);
523
+ if (!lastReconcileButton) {
524
+ throw new Error('Expected a checkpoint reconcile button');
525
+ }
526
+ await waitFor(() => expect(lastReconcileButton).not.toBeDisabled());
527
+ fireEvent.click(lastReconcileButton);
528
+
529
+ await waitFor(() => {
530
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
531
+ 'ws-1',
532
+ 'wallet-1',
533
+ 'checkpoint-2',
534
+ expect.objectContaining({
535
+ category_id: undefined,
536
+ description: expect.stringContaining('Cash'),
537
+ })
538
+ );
539
+ });
540
+ });
541
+ });