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