@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -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 +50 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
  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/wallet-filter.tsx +21 -2
  22. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
  23. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  24. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
  25. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
  28. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  29. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  30. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  31. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  32. package/src/components/ui/finance/wallets/form.tsx +41 -197
  33. package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
  34. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  35. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  36. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  40. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  41. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
  42. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
  43. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  44. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  45. package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
  46. package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
  47. package/src/components/ui/storefront/accent-button.tsx +33 -0
  48. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  49. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  50. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  51. package/src/components/ui/storefront/image-panel.tsx +40 -0
  52. package/src/components/ui/storefront/index.ts +12 -0
  53. package/src/components/ui/storefront/listing-card.tsx +129 -0
  54. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  55. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  56. package/src/components/ui/storefront/types.ts +99 -0
  57. package/src/components/ui/storefront/utils.ts +90 -0
  58. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  59. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  60. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  61. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  62. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  63. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  64. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  65. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
  66. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  67. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  68. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  69. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  70. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  71. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  72. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  73. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  85. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
  86. package/src/hooks/useBoardRealtime.ts +54 -1
  87. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  88. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -0,0 +1,617 @@
1
+ 'use client';
2
+
3
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { ExternalLink, History, Search } from '@tuturuuu/icons';
5
+ import {
6
+ getWalletCheckpointHistory,
7
+ type WalletCheckpoint,
8
+ type WalletCheckpointAuditStatus,
9
+ type WalletCheckpointHistoryInterval,
10
+ } from '@tuturuuu/internal-api/finance';
11
+ import { Badge } from '@tuturuuu/ui/badge';
12
+ import { Button } from '@tuturuuu/ui/button';
13
+ import {
14
+ Dialog,
15
+ DialogContent,
16
+ DialogDescription,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from '@tuturuuu/ui/dialog';
20
+ import { Input } from '@tuturuuu/ui/input';
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@tuturuuu/ui/select';
28
+ import { Skeleton } from '@tuturuuu/ui/skeleton';
29
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
30
+ import { cn } from '@tuturuuu/utils/format';
31
+ import Link from 'next/link';
32
+ import { useLocale, useTranslations } from 'next-intl';
33
+ import { useMemo, useState } from 'react';
34
+ import { invalidateWalletMutationQueries } from '../query-invalidation';
35
+ import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
36
+ import { WalletCheckpointAmount } from './wallet-checkpoint-amount';
37
+
38
+ const ALL = 'all';
39
+
40
+ type CheckpointStatus = 'clean' | 'no_checkpoint' | 'unresolved';
41
+
42
+ type ReconcileTarget = {
43
+ checkedAt: string;
44
+ checkpointId: string;
45
+ currency: string;
46
+ variance: number;
47
+ walletId: string;
48
+ walletName: string;
49
+ };
50
+
51
+ type WindowRow =
52
+ | {
53
+ interval: WalletCheckpointHistoryInterval;
54
+ status: Exclude<CheckpointStatus, 'no_checkpoint'>;
55
+ type: 'interval';
56
+ }
57
+ | {
58
+ auditStatus: WalletCheckpointAuditStatus;
59
+ currency: string;
60
+ type: 'no_checkpoint';
61
+ walletName: string | null;
62
+ };
63
+
64
+ export function WalletCheckpointHistoryDialog({
65
+ canCreateTransactions,
66
+ financePrefix = '/finance',
67
+ wsId,
68
+ }: {
69
+ canCreateTransactions: boolean;
70
+ financePrefix?: string;
71
+ wsId: string;
72
+ }) {
73
+ const t = useTranslations('wallet-checkpoints');
74
+ const locale = useLocale();
75
+ const queryClient = useQueryClient();
76
+ const [open, setOpen] = useState(false);
77
+ const [search, setSearch] = useState('');
78
+ const [currency, setCurrency] = useState(ALL);
79
+ const [status, setStatus] = useState<typeof ALL | CheckpointStatus>(ALL);
80
+ const [reconcileTarget, setReconcileTarget] =
81
+ useState<ReconcileTarget | null>(null);
82
+ const query = useQuery({
83
+ queryKey: ['wallet-checkpoint-history', wsId],
84
+ queryFn: () => getWalletCheckpointHistory(wsId, { limit: 100 }),
85
+ enabled: open,
86
+ });
87
+ const formatDate = (value: string) =>
88
+ new Intl.DateTimeFormat(locale, {
89
+ dateStyle: 'medium',
90
+ timeStyle: 'short',
91
+ }).format(new Date(value));
92
+ const wallets = query.data?.wallets ?? [];
93
+ const walletById = useMemo(
94
+ () => new Map(wallets.map((wallet) => [wallet.id, wallet])),
95
+ [wallets]
96
+ );
97
+ const auditStatusByWalletId = useMemo(
98
+ () =>
99
+ new Map(
100
+ (query.data?.audit_statuses ?? []).map((auditStatus) => [
101
+ auditStatus.wallet_id,
102
+ auditStatus,
103
+ ])
104
+ ),
105
+ [query.data?.audit_statuses]
106
+ );
107
+ const currencies = useMemo(() => {
108
+ const values = new Set<string>();
109
+ for (const wallet of wallets) values.add(wallet.currency);
110
+ for (const interval of query.data?.intervals ?? []) {
111
+ values.add(interval.currency);
112
+ }
113
+ for (const checkpoint of query.data?.checkpoints ?? []) {
114
+ values.add(checkpoint.currency);
115
+ }
116
+ return [...values].sort((a, b) => a.localeCompare(b));
117
+ }, [query.data?.checkpoints, query.data?.intervals, wallets]);
118
+ const normalizedSearch = search.trim().toLowerCase();
119
+ const windowRows = useMemo<WindowRow[]>(() => {
120
+ const intervals = (query.data?.intervals ?? []).map((interval) => {
121
+ const status: Exclude<CheckpointStatus, 'no_checkpoint'> =
122
+ interval.is_clean ? 'clean' : 'unresolved';
123
+
124
+ return {
125
+ interval,
126
+ status,
127
+ type: 'interval' as const,
128
+ };
129
+ });
130
+ const noCheckpointRows = (query.data?.audit_statuses ?? [])
131
+ .filter((auditStatus) => auditStatus.status === 'no_checkpoint')
132
+ .map((auditStatus) => {
133
+ const wallet = walletById.get(auditStatus.wallet_id);
134
+ return {
135
+ auditStatus,
136
+ currency: wallet?.currency ?? 'USD',
137
+ type: 'no_checkpoint' as const,
138
+ walletName: wallet?.name ?? null,
139
+ };
140
+ });
141
+
142
+ return [...intervals, ...noCheckpointRows];
143
+ }, [query.data?.audit_statuses, query.data?.intervals, walletById]);
144
+ const filteredWindowRows = windowRows.filter((row) => {
145
+ const rowCurrency =
146
+ row.type === 'interval' ? row.interval.currency : row.currency;
147
+ const rowStatus = row.type === 'interval' ? row.status : 'no_checkpoint';
148
+ const walletName =
149
+ row.type === 'interval' ? row.interval.wallet_name : row.walletName;
150
+
151
+ return (
152
+ (currency === ALL || rowCurrency === currency) &&
153
+ (status === ALL || rowStatus === status) &&
154
+ (!normalizedSearch ||
155
+ (walletName ?? '').toLowerCase().includes(normalizedSearch))
156
+ );
157
+ });
158
+ const filteredCheckpoints = (query.data?.checkpoints ?? []).filter(
159
+ (checkpoint) => {
160
+ const wallet = walletById.get(checkpoint.wallet_id);
161
+ const checkpointStatus = getCheckpointStatus(
162
+ checkpoint,
163
+ auditStatusByWalletId.get(checkpoint.wallet_id)
164
+ );
165
+
166
+ return (
167
+ (currency === ALL || checkpoint.currency === currency) &&
168
+ (status === ALL || checkpointStatus === status) &&
169
+ (!normalizedSearch ||
170
+ (wallet?.name ?? '').toLowerCase().includes(normalizedSearch))
171
+ );
172
+ }
173
+ );
174
+
175
+ const refresh = () => {
176
+ queryClient.invalidateQueries({
177
+ queryKey: ['wallet-checkpoint-history', wsId],
178
+ });
179
+ invalidateWalletMutationQueries(queryClient, wsId);
180
+ };
181
+
182
+ return (
183
+ <>
184
+ <Button variant="outline" onClick={() => setOpen(true)}>
185
+ <History className="mr-2 h-4 w-4" />
186
+ {t('checkpoint_history')}
187
+ </Button>
188
+ <Dialog open={open} onOpenChange={setOpen}>
189
+ <DialogContent className="flex max-h-[92vh] w-[min(96vw,1200px)] max-w-none flex-col overflow-hidden">
190
+ <DialogHeader>
191
+ <DialogTitle>{t('checkpoint_history')}</DialogTitle>
192
+ <DialogDescription>
193
+ {t('checkpoint_history_description')}
194
+ </DialogDescription>
195
+ </DialogHeader>
196
+ <div className="grid gap-3 md:grid-cols-[1fr_180px_180px]">
197
+ <div className="relative">
198
+ <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
199
+ <Input
200
+ className="pl-9"
201
+ value={search}
202
+ onChange={(event) => setSearch(event.target.value)}
203
+ placeholder={t('search_wallets')}
204
+ />
205
+ </div>
206
+ <Select value={currency} onValueChange={setCurrency}>
207
+ <SelectTrigger>
208
+ <SelectValue />
209
+ </SelectTrigger>
210
+ <SelectContent>
211
+ <SelectItem value={ALL}>{t('all_currencies')}</SelectItem>
212
+ {currencies.map((value) => (
213
+ <SelectItem key={value} value={value}>
214
+ {value}
215
+ </SelectItem>
216
+ ))}
217
+ </SelectContent>
218
+ </Select>
219
+ <Select
220
+ value={status}
221
+ onValueChange={(value) =>
222
+ setStatus(value as typeof ALL | CheckpointStatus)
223
+ }
224
+ >
225
+ <SelectTrigger>
226
+ <SelectValue />
227
+ </SelectTrigger>
228
+ <SelectContent>
229
+ <SelectItem value={ALL}>{t('all_statuses')}</SelectItem>
230
+ <SelectItem value="clean">{t('clean')}</SelectItem>
231
+ <SelectItem value="unresolved">{t('unresolved')}</SelectItem>
232
+ <SelectItem value="no_checkpoint">
233
+ {t('no_checkpoint')}
234
+ </SelectItem>
235
+ </SelectContent>
236
+ </Select>
237
+ </div>
238
+ <Tabs defaultValue="windows" className="min-h-0 flex-1">
239
+ <TabsList>
240
+ <TabsTrigger value="windows">{t('windows')}</TabsTrigger>
241
+ <TabsTrigger value="checkpoints">{t('checkpoints')}</TabsTrigger>
242
+ </TabsList>
243
+ <TabsContent value="windows" className="min-h-0">
244
+ <div className="max-h-[58vh] space-y-3 overflow-y-auto pr-1">
245
+ {query.isLoading ? (
246
+ <HistorySkeleton />
247
+ ) : filteredWindowRows.length === 0 ? (
248
+ <EmptyState>{t('no_windows')}</EmptyState>
249
+ ) : (
250
+ filteredWindowRows.map((row) =>
251
+ row.type === 'interval' ? (
252
+ <IntervalRow
253
+ key={row.interval.end_checkpoint_id}
254
+ canCreateTransactions={canCreateTransactions}
255
+ formatDate={formatDate}
256
+ interval={row.interval}
257
+ onReconcile={setReconcileTarget}
258
+ />
259
+ ) : (
260
+ <NoCheckpointRow
261
+ key={row.auditStatus.wallet_id}
262
+ auditStatus={row.auditStatus}
263
+ currency={row.currency}
264
+ walletName={row.walletName}
265
+ />
266
+ )
267
+ )
268
+ )}
269
+ </div>
270
+ </TabsContent>
271
+ <TabsContent value="checkpoints" className="min-h-0">
272
+ <div className="max-h-[58vh] space-y-3 overflow-y-auto pr-1">
273
+ {query.isLoading ? (
274
+ <HistorySkeleton />
275
+ ) : filteredCheckpoints.length === 0 ? (
276
+ <EmptyState>{t('no_checkpoints')}</EmptyState>
277
+ ) : (
278
+ filteredCheckpoints.map((checkpoint) => {
279
+ const wallet = walletById.get(checkpoint.wallet_id);
280
+ const auditStatus = auditStatusByWalletId.get(
281
+ checkpoint.wallet_id
282
+ );
283
+
284
+ return (
285
+ <CheckpointRow
286
+ key={checkpoint.id}
287
+ auditStatus={auditStatus}
288
+ canCreateTransactions={canCreateTransactions}
289
+ checkpoint={checkpoint}
290
+ financePrefix={financePrefix}
291
+ formatDate={formatDate}
292
+ onReconcile={setReconcileTarget}
293
+ walletName={wallet?.name ?? null}
294
+ wsId={wsId}
295
+ />
296
+ );
297
+ })
298
+ )}
299
+ </div>
300
+ </TabsContent>
301
+ </Tabs>
302
+ </DialogContent>
303
+ </Dialog>
304
+ {reconcileTarget && (
305
+ <WalletCheckpointAdjustmentDialog
306
+ checkedAt={reconcileTarget.checkedAt}
307
+ checkpointId={reconcileTarget.checkpointId}
308
+ currency={reconcileTarget.currency}
309
+ onCreated={refresh}
310
+ onOpenChange={(nextOpen) => !nextOpen && setReconcileTarget(null)}
311
+ open={!!reconcileTarget}
312
+ variance={reconcileTarget.variance}
313
+ walletId={reconcileTarget.walletId}
314
+ walletName={reconcileTarget.walletName}
315
+ wsId={wsId}
316
+ />
317
+ )}
318
+ </>
319
+ );
320
+ }
321
+
322
+ function IntervalRow({
323
+ canCreateTransactions,
324
+ formatDate,
325
+ interval,
326
+ onReconcile,
327
+ }: {
328
+ canCreateTransactions: boolean;
329
+ formatDate: (value: string) => string;
330
+ interval: WalletCheckpointHistoryInterval;
331
+ onReconcile: (target: ReconcileTarget) => void;
332
+ }) {
333
+ const t = useTranslations('wallet-checkpoints');
334
+ const status: CheckpointStatus = interval.is_clean ? 'clean' : 'unresolved';
335
+
336
+ return (
337
+ <div
338
+ className={cn(
339
+ 'grid gap-3 rounded-md border p-3',
340
+ interval.is_clean
341
+ ? 'border-dynamic-green/40'
342
+ : 'border-dynamic-yellow/50'
343
+ )}
344
+ >
345
+ <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
346
+ <div>
347
+ <div className="font-medium text-sm">
348
+ {interval.wallet_name ?? interval.wallet_id}
349
+ </div>
350
+ <div className="text-muted-foreground text-xs">
351
+ {formatDate(interval.start_checked_at)} -{' '}
352
+ {formatDate(interval.end_checked_at)}
353
+ </div>
354
+ </div>
355
+ <StatusBadge status={status} />
356
+ </div>
357
+ <div className="grid gap-3 text-sm md:grid-cols-4">
358
+ <Metric label={t('actual_delta')}>
359
+ <WalletCheckpointAmount
360
+ amount={interval.actual_delta}
361
+ currency={interval.currency}
362
+ signDisplay="always"
363
+ />
364
+ </Metric>
365
+ <Metric label={t('ledger_delta')}>
366
+ <WalletCheckpointAmount
367
+ amount={interval.ledger_delta}
368
+ currency={interval.currency}
369
+ signDisplay="always"
370
+ />
371
+ </Metric>
372
+ <Metric label={t('variance')}>
373
+ <WalletCheckpointAmount
374
+ amount={interval.interval_variance}
375
+ currency={interval.currency}
376
+ signDisplay="always"
377
+ />
378
+ </Metric>
379
+ <Metric label={t('transaction_count')}>
380
+ {interval.transaction_count}
381
+ </Metric>
382
+ </div>
383
+ {!interval.is_clean && canCreateTransactions && (
384
+ <div>
385
+ <Button
386
+ size="sm"
387
+ variant="outline"
388
+ onClick={() =>
389
+ onReconcile({
390
+ checkedAt: interval.end_checked_at,
391
+ checkpointId: interval.end_checkpoint_id,
392
+ currency: interval.currency,
393
+ variance: interval.interval_variance,
394
+ walletId: interval.wallet_id,
395
+ walletName: interval.wallet_name ?? interval.wallet_id,
396
+ })
397
+ }
398
+ >
399
+ {t('reconcile')}
400
+ </Button>
401
+ </div>
402
+ )}
403
+ </div>
404
+ );
405
+ }
406
+
407
+ function NoCheckpointRow({
408
+ auditStatus,
409
+ currency,
410
+ walletName,
411
+ }: {
412
+ auditStatus: WalletCheckpointAuditStatus;
413
+ currency: string;
414
+ walletName: string | null;
415
+ }) {
416
+ const t = useTranslations('wallet-checkpoints');
417
+
418
+ return (
419
+ <div className="grid gap-3 rounded-md border border-dashed p-3">
420
+ <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
421
+ <div>
422
+ <div className="font-medium text-sm">
423
+ {walletName ?? auditStatus.wallet_id}
424
+ </div>
425
+ <div className="text-muted-foreground text-xs">
426
+ {t('no_checkpoint_detail')}
427
+ </div>
428
+ </div>
429
+ <StatusBadge status="no_checkpoint" />
430
+ </div>
431
+ <Metric label={t('ledger_balance')}>
432
+ <WalletCheckpointAmount
433
+ amount={auditStatus.ledger_balance}
434
+ currency={currency}
435
+ />
436
+ </Metric>
437
+ </div>
438
+ );
439
+ }
440
+
441
+ function CheckpointRow({
442
+ auditStatus,
443
+ canCreateTransactions,
444
+ checkpoint,
445
+ financePrefix,
446
+ formatDate,
447
+ onReconcile,
448
+ walletName,
449
+ wsId,
450
+ }: {
451
+ auditStatus?: WalletCheckpointAuditStatus;
452
+ canCreateTransactions: boolean;
453
+ checkpoint: WalletCheckpoint;
454
+ financePrefix: string;
455
+ formatDate: (value: string) => string;
456
+ onReconcile: (target: ReconcileTarget) => void;
457
+ walletName: string | null;
458
+ wsId: string;
459
+ }) {
460
+ const t = useTranslations('wallet-checkpoints');
461
+ const isLatest = auditStatus?.latest_checkpoint_id === checkpoint.id;
462
+ const checkpointStatus = getCheckpointStatus(checkpoint, auditStatus);
463
+ const variance = isLatest
464
+ ? (auditStatus?.variance ?? checkpoint.current_variance)
465
+ : checkpoint.current_variance;
466
+
467
+ return (
468
+ <div className="grid gap-3 rounded-md border p-3">
469
+ <div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
470
+ <div>
471
+ <div className="font-medium text-sm">
472
+ {walletName ?? checkpoint.wallet_id}
473
+ </div>
474
+ <div className="text-muted-foreground text-xs">
475
+ {formatDate(checkpoint.checked_at)}
476
+ </div>
477
+ </div>
478
+ <div className="flex flex-wrap items-center gap-2">
479
+ <StatusBadge status={checkpointStatus} />
480
+ <Button size="sm" variant="ghost" asChild>
481
+ <Link
482
+ href={`/${wsId}${financePrefix}/wallets/${checkpoint.wallet_id}`}
483
+ >
484
+ <ExternalLink className="mr-2 h-4 w-4" />
485
+ {t('open_wallet')}
486
+ </Link>
487
+ </Button>
488
+ </div>
489
+ </div>
490
+ <div className="grid gap-3 text-sm md:grid-cols-5">
491
+ <Metric label={t('actual_balance')}>
492
+ <WalletCheckpointAmount
493
+ amount={checkpoint.actual_balance}
494
+ currency={checkpoint.currency}
495
+ />
496
+ </Metric>
497
+ <Metric label={t('ledger_at_checkpoint')}>
498
+ <WalletCheckpointAmount
499
+ amount={checkpoint.ledger_balance}
500
+ currency={checkpoint.currency}
501
+ />
502
+ </Metric>
503
+ <Metric label={t('post_checkpoint_delta')}>
504
+ {isLatest && auditStatus ? (
505
+ <WalletCheckpointAmount
506
+ amount={auditStatus.post_checkpoint_delta}
507
+ currency={checkpoint.currency}
508
+ signDisplay="always"
509
+ />
510
+ ) : (
511
+ '-'
512
+ )}
513
+ </Metric>
514
+ <Metric label={t('audited_balance')}>
515
+ {isLatest && auditStatus ? (
516
+ <WalletCheckpointAmount
517
+ amount={auditStatus.audited_balance}
518
+ currency={checkpoint.currency}
519
+ />
520
+ ) : (
521
+ '-'
522
+ )}
523
+ </Metric>
524
+ <Metric label={t('variance')}>
525
+ <WalletCheckpointAmount
526
+ amount={variance}
527
+ currency={checkpoint.currency}
528
+ signDisplay="always"
529
+ />
530
+ </Metric>
531
+ </div>
532
+ {checkpoint.note && (
533
+ <div className="text-muted-foreground text-sm">
534
+ {t('note')}: {checkpoint.note}
535
+ </div>
536
+ )}
537
+ {variance !== 0 && canCreateTransactions && (
538
+ <div>
539
+ <Button
540
+ size="sm"
541
+ variant="outline"
542
+ onClick={() =>
543
+ onReconcile({
544
+ checkedAt: checkpoint.checked_at,
545
+ checkpointId: checkpoint.id,
546
+ currency: checkpoint.currency,
547
+ variance,
548
+ walletId: checkpoint.wallet_id,
549
+ walletName: walletName ?? checkpoint.wallet_id,
550
+ })
551
+ }
552
+ >
553
+ {t('reconcile')}
554
+ </Button>
555
+ </div>
556
+ )}
557
+ </div>
558
+ );
559
+ }
560
+
561
+ function getCheckpointStatus(
562
+ checkpoint: WalletCheckpoint,
563
+ auditStatus?: WalletCheckpointAuditStatus
564
+ ): CheckpointStatus {
565
+ if (auditStatus?.latest_checkpoint_id === checkpoint.id) {
566
+ return auditStatus.status;
567
+ }
568
+
569
+ return checkpoint.current_variance === 0 ? 'clean' : 'unresolved';
570
+ }
571
+
572
+ function StatusBadge({ status }: { status: CheckpointStatus }) {
573
+ const t = useTranslations('wallet-checkpoints');
574
+
575
+ if (status === 'clean') {
576
+ return <Badge variant="secondary">{t('clean')}</Badge>;
577
+ }
578
+
579
+ if (status === 'no_checkpoint') {
580
+ return <Badge variant="outline">{t('no_checkpoint')}</Badge>;
581
+ }
582
+
583
+ return <Badge variant="outline">{t('unresolved')}</Badge>;
584
+ }
585
+
586
+ function Metric({
587
+ children,
588
+ label,
589
+ }: {
590
+ children: React.ReactNode;
591
+ label: string;
592
+ }) {
593
+ return (
594
+ <div>
595
+ <div className="text-muted-foreground text-xs">{label}</div>
596
+ <div className="font-medium">{children}</div>
597
+ </div>
598
+ );
599
+ }
600
+
601
+ function EmptyState({ children }: { children: React.ReactNode }) {
602
+ return (
603
+ <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
604
+ {children}
605
+ </div>
606
+ );
607
+ }
608
+
609
+ function HistorySkeleton() {
610
+ return (
611
+ <div className="space-y-3">
612
+ <Skeleton className="h-24 w-full" />
613
+ <Skeleton className="h-24 w-full" />
614
+ <Skeleton className="h-24 w-full" />
615
+ </div>
616
+ );
617
+ }
@@ -146,7 +146,7 @@ export function WalletCheckpointPanel({
146
146
  currency={currency}
147
147
  formatDate={formatDate}
148
148
  intervals={query.data?.intervals ?? []}
149
- onAdjust={setAdjusting}
149
+ onReconcile={setAdjusting}
150
150
  />
151
151
  <WalletCheckpointTimeline
152
152
  canUpdateWallets={canUpdateWallets}
@@ -181,6 +181,7 @@ export function WalletCheckpointPanel({
181
181
  {adjusting && (
182
182
  <WalletCheckpointAdjustmentDialog
183
183
  checkedAt={adjusting.end_checked_at}
184
+ checkpointId={adjusting.end_checkpoint_id}
184
185
  currency={currency}
185
186
  onCreated={refresh}
186
187
  onOpenChange={(open) => !open && setAdjusting(null)}
@@ -47,13 +47,13 @@ export function WalletCheckpointIntervals({
47
47
  currency,
48
48
  formatDate,
49
49
  intervals,
50
- onAdjust,
50
+ onReconcile,
51
51
  }: {
52
52
  canCreateTransactions: boolean;
53
53
  currency: string;
54
54
  formatDate: (value: string) => string;
55
55
  intervals: WalletCheckpointInterval[];
56
- onAdjust: (interval: WalletCheckpointInterval) => void;
56
+ onReconcile: (interval: WalletCheckpointInterval) => void;
57
57
  }) {
58
58
  const t = useTranslations('wallet-checkpoints');
59
59
  if (intervals.length === 0) return null;
@@ -112,9 +112,9 @@ export function WalletCheckpointIntervals({
112
112
  className="mt-3"
113
113
  size="sm"
114
114
  variant="outline"
115
- onClick={() => onAdjust(interval)}
115
+ onClick={() => onReconcile(interval)}
116
116
  >
117
- {t('create_adjustment')}
117
+ {t('reconcile')}
118
118
  </Button>
119
119
  )}
120
120
  </div>