@tuturuuu/ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +50 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
- package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -1,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
|
-
<
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
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,3 +1,5 @@
|
|
|
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(() => {
|
|
@@ -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
|
});
|
|
@@ -101,7 +105,9 @@ vi.mock('../checkpoints/wallet-checkpoint-panel', () => ({
|
|
|
101
105
|
}));
|
|
102
106
|
|
|
103
107
|
vi.mock('./credit-wallet-summary', () => ({
|
|
104
|
-
CreditWalletSummary: (
|
|
108
|
+
CreditWalletSummary: (
|
|
109
|
+
...args: Parameters<typeof mocks.creditWalletSummary>
|
|
110
|
+
) => mocks.creditWalletSummary(...args),
|
|
105
111
|
}));
|
|
106
112
|
|
|
107
113
|
vi.mock('./interest', () => ({
|
|
@@ -109,7 +115,9 @@ vi.mock('./interest', () => ({
|
|
|
109
115
|
}));
|
|
110
116
|
|
|
111
117
|
vi.mock('./wallet-details-actions', () => ({
|
|
112
|
-
WalletDetailsActions: (
|
|
118
|
+
WalletDetailsActions: (
|
|
119
|
+
...args: Parameters<typeof mocks.walletDetailsActions>
|
|
120
|
+
) => mocks.walletDetailsActions(...args),
|
|
113
121
|
}));
|
|
114
122
|
|
|
115
123
|
vi.mock('./wallet-role-access-dialog', () => ({
|
|
@@ -162,4 +170,58 @@ describe('wallet details page', () => {
|
|
|
162
170
|
headers: {},
|
|
163
171
|
});
|
|
164
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
|
+
});
|
|
165
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 {
|
|
16
|
-
import { formatCurrency, getCurrencyLocale } from '@tuturuuu/utils/format';
|
|
15
|
+
import { getCurrencyLocale } from '@tuturuuu/utils/format';
|
|
17
16
|
import {
|
|
18
17
|
getPermissions,
|
|
19
18
|
getWorkspace,
|
|
@@ -31,7 +30,10 @@ import { WalletCheckpointPanel } from '../checkpoints/wallet-checkpoint-panel';
|
|
|
31
30
|
import { WalletIconDisplay } from '../wallet-icon-display';
|
|
32
31
|
import { CreditWalletSummary } from './credit-wallet-summary';
|
|
33
32
|
import { WalletInterestSection } from './interest';
|
|
34
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
type WalletDetailsAction,
|
|
35
|
+
WalletDetailsActions,
|
|
36
|
+
} from './wallet-details-actions';
|
|
35
37
|
import { WalletDetailsAmount } from './wallet-details-amount';
|
|
36
38
|
import WalletRoleAccessDialog from './wallet-role-access-dialog';
|
|
37
39
|
|
|
@@ -39,6 +41,7 @@ interface Props {
|
|
|
39
41
|
wsId: string;
|
|
40
42
|
walletId: string;
|
|
41
43
|
searchParams: {
|
|
44
|
+
action?: string;
|
|
42
45
|
q: string;
|
|
43
46
|
page: string;
|
|
44
47
|
pageSize: string;
|
|
@@ -55,6 +58,7 @@ interface Props {
|
|
|
55
58
|
export default async function WalletDetailsPage({
|
|
56
59
|
wsId,
|
|
57
60
|
walletId,
|
|
61
|
+
searchParams,
|
|
58
62
|
defaultCurrency,
|
|
59
63
|
internalApiOptions,
|
|
60
64
|
permissions,
|
|
@@ -114,31 +118,10 @@ export default async function WalletDetailsPage({
|
|
|
114
118
|
|
|
115
119
|
const currency = wallet.currency || resolvedDefaultCurrency || 'USD';
|
|
116
120
|
const workspaceCurrency = resolvedDefaultCurrency || 'USD';
|
|
121
|
+
const initialAction = getInitialWalletAction(searchParams.action);
|
|
117
122
|
|
|
118
123
|
// Fetch exchange rates for conversion display
|
|
119
124
|
const exchangeRates = await getExchangeRates();
|
|
120
|
-
let convertedBalanceText: string | null = null;
|
|
121
|
-
if (
|
|
122
|
-
currency !== workspaceCurrency &&
|
|
123
|
-
exchangeRates.length > 0 &&
|
|
124
|
-
wallet.balance &&
|
|
125
|
-
wallet.balance !== 0
|
|
126
|
-
) {
|
|
127
|
-
const converted = convertCurrency(
|
|
128
|
-
wallet.balance,
|
|
129
|
-
currency,
|
|
130
|
-
workspaceCurrency,
|
|
131
|
-
exchangeRates
|
|
132
|
-
);
|
|
133
|
-
if (converted !== null) {
|
|
134
|
-
convertedBalanceText = formatCurrency(
|
|
135
|
-
Math.abs(converted),
|
|
136
|
-
workspaceCurrency,
|
|
137
|
-
undefined,
|
|
138
|
-
{ signDisplay: 'never', maximumFractionDigits: 0 }
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
125
|
|
|
143
126
|
return (
|
|
144
127
|
<div className="flex min-h-full w-full flex-col">
|
|
@@ -161,6 +144,7 @@ export default async function WalletDetailsPage({
|
|
|
161
144
|
wsId={wsId}
|
|
162
145
|
walletId={walletId}
|
|
163
146
|
wallet={wallet as Wallet}
|
|
147
|
+
initialAction={initialAction}
|
|
164
148
|
canUpdateWallets={canUpdateWallets}
|
|
165
149
|
canCreateTransactions={canCreateTransactions}
|
|
166
150
|
canCreateConfidentialTransactions={canCreateConfidentialTransactions}
|
|
@@ -172,6 +156,12 @@ export default async function WalletDetailsPage({
|
|
|
172
156
|
/>
|
|
173
157
|
</div>
|
|
174
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
|
+
)}
|
|
175
165
|
<div className="grid h-fit gap-4 md:grid-cols-2">
|
|
176
166
|
<Card className="grid gap-4">
|
|
177
167
|
<div className="grid h-fit gap-2 rounded-lg p-4">
|
|
@@ -195,11 +185,17 @@ export default async function WalletDetailsPage({
|
|
|
195
185
|
label={t('wallet-data-table.balance')}
|
|
196
186
|
value={
|
|
197
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}
|
|
198
194
|
primary={Intl.NumberFormat(getCurrencyLocale(currency), {
|
|
199
195
|
style: 'currency',
|
|
200
196
|
currency,
|
|
201
197
|
}).format(wallet.balance || 0)}
|
|
202
|
-
|
|
198
|
+
workspaceCurrency={workspaceCurrency}
|
|
203
199
|
/>
|
|
204
200
|
}
|
|
205
201
|
/>
|
|
@@ -294,12 +290,6 @@ export default async function WalletDetailsPage({
|
|
|
294
290
|
<Separator className="my-4" />
|
|
295
291
|
</>
|
|
296
292
|
)}
|
|
297
|
-
{wallet.type === 'CREDIT' && (
|
|
298
|
-
<>
|
|
299
|
-
<CreditWalletSummary wsId={wsId} wallet={wallet as Wallet} />
|
|
300
|
-
<Separator className="my-4" />
|
|
301
|
-
</>
|
|
302
|
-
)}
|
|
303
293
|
<WalletCheckpointPanel
|
|
304
294
|
wsId={wsId}
|
|
305
295
|
walletId={walletId}
|
|
@@ -341,6 +331,18 @@ export default async function WalletDetailsPage({
|
|
|
341
331
|
);
|
|
342
332
|
}
|
|
343
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
|
+
|
|
344
346
|
function DetailItem({
|
|
345
347
|
icon,
|
|
346
348
|
label,
|
|
@@ -352,9 +354,14 @@ function DetailItem({
|
|
|
352
354
|
}) {
|
|
353
355
|
if (!value) return undefined;
|
|
354
356
|
return (
|
|
355
|
-
<div className="flex items-
|
|
356
|
-
|
|
357
|
-
|
|
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>
|
|
358
365
|
</div>
|
|
359
366
|
);
|
|
360
367
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { WalletsDataTable } from './wallets-data-table';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
dataTableProps: undefined as
|
|
9
|
+
| {
|
|
10
|
+
data?: Array<{ id: string; name?: string | null }>;
|
|
11
|
+
onRefresh?: () => void;
|
|
12
|
+
onSearch?: (query: string) => void;
|
|
13
|
+
}
|
|
14
|
+
| undefined,
|
|
15
|
+
listInfiniteWallets: vi.fn(),
|
|
16
|
+
replace: vi.fn(),
|
|
17
|
+
searchParams: new URLSearchParams(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@tuturuuu/internal-api/finance', () => ({
|
|
21
|
+
listInfiniteWallets: (
|
|
22
|
+
...args: Parameters<typeof mocks.listInfiniteWallets>
|
|
23
|
+
) => mocks.listInfiniteWallets(...args),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('@tuturuuu/ui/custom/modifiable-dialog-trigger', () => ({
|
|
27
|
+
default: () => null,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('@tuturuuu/ui/custom/tables/data-table', () => ({
|
|
31
|
+
DataTable: (props: {
|
|
32
|
+
data?: Array<{ id: string; name?: string | null }>;
|
|
33
|
+
onRefresh?: () => void;
|
|
34
|
+
onSearch?: (query: string) => void;
|
|
35
|
+
}) => {
|
|
36
|
+
mocks.dataTableProps = props;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div data-testid="wallet-table">
|
|
40
|
+
{(props.data ?? []).map((wallet) => (
|
|
41
|
+
<div key={wallet.id}>{wallet.name}</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock('@tuturuuu/ui/finance/wallets/columns', () => ({
|
|
49
|
+
walletColumns: vi.fn(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('@tuturuuu/ui/finance/wallets/form', () => ({
|
|
53
|
+
WalletForm: () => null,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock('@tuturuuu/ui/hooks/use-exchange-rates', () => ({
|
|
57
|
+
useExchangeRates: () => ({
|
|
58
|
+
data: {
|
|
59
|
+
data: [],
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
vi.mock('next/navigation', () => ({
|
|
65
|
+
usePathname: () => '/ws-1/finance/wallets',
|
|
66
|
+
useRouter: () => ({
|
|
67
|
+
replace: (...args: Parameters<typeof mocks.replace>) =>
|
|
68
|
+
mocks.replace(...args),
|
|
69
|
+
}),
|
|
70
|
+
useSearchParams: () => mocks.searchParams,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
vi.mock('next-intl', () => ({
|
|
74
|
+
useTranslations:
|
|
75
|
+
() => (key: string, values?: Record<string, string | number>) =>
|
|
76
|
+
values
|
|
77
|
+
? `${key}:${Object.values(values)
|
|
78
|
+
.map((value) => String(value))
|
|
79
|
+
.join(',')}`
|
|
80
|
+
: key,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
vi.mock('../shared/use-finance-balance-mode', () => ({
|
|
84
|
+
useFinanceBalanceMode: () => ({
|
|
85
|
+
mode: 'audited',
|
|
86
|
+
}),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
function renderWithQueryClient(ui: ReactElement) {
|
|
90
|
+
const queryClient = new QueryClient({
|
|
91
|
+
defaultOptions: {
|
|
92
|
+
queries: {
|
|
93
|
+
retry: false,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('wallets data table infinite loading', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
vi.clearAllMocks();
|
|
104
|
+
mocks.dataTableProps = undefined;
|
|
105
|
+
mocks.searchParams = new URLSearchParams();
|
|
106
|
+
|
|
107
|
+
globalThis.IntersectionObserver = class IntersectionObserver {
|
|
108
|
+
disconnect() {}
|
|
109
|
+
observe() {}
|
|
110
|
+
takeRecords() {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
unobserve() {}
|
|
114
|
+
} as unknown as typeof IntersectionObserver;
|
|
115
|
+
|
|
116
|
+
mocks.listInfiniteWallets.mockImplementation(
|
|
117
|
+
async (
|
|
118
|
+
_workspaceId: string,
|
|
119
|
+
query: {
|
|
120
|
+
offset?: number;
|
|
121
|
+
}
|
|
122
|
+
) => {
|
|
123
|
+
if (query.offset === 20) {
|
|
124
|
+
return {
|
|
125
|
+
data: [{ id: 'wallet-2', name: 'Bank' }],
|
|
126
|
+
hasMore: false,
|
|
127
|
+
nextOffset: null,
|
|
128
|
+
totalCount: 2,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
data: [{ id: 'wallet-1', name: 'Cash' }],
|
|
134
|
+
hasMore: true,
|
|
135
|
+
nextOffset: 20,
|
|
136
|
+
totalCount: 2,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('loads the next server page from the fallback load-more button', async () => {
|
|
143
|
+
renderWithQueryClient(
|
|
144
|
+
<WalletsDataTable wsId="ws-1" currency="USD" query="bank" />
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(await screen.findByText('Cash')).toBeInTheDocument();
|
|
148
|
+
|
|
149
|
+
fireEvent.click(
|
|
150
|
+
screen.getByRole('button', { name: 'wallet-data-table.load_more' })
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(await screen.findByText('Bank')).toBeInTheDocument();
|
|
154
|
+
expect(mocks.listInfiniteWallets).toHaveBeenCalledWith('ws-1', {
|
|
155
|
+
limit: 20,
|
|
156
|
+
offset: 20,
|
|
157
|
+
q: 'bank',
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('keeps search in the URL and drops legacy page params', async () => {
|
|
162
|
+
mocks.searchParams = new URLSearchParams('page=2&pageSize=50');
|
|
163
|
+
|
|
164
|
+
renderWithQueryClient(<WalletsDataTable wsId="ws-1" currency="USD" />);
|
|
165
|
+
|
|
166
|
+
await waitFor(() => expect(mocks.dataTableProps).toBeDefined());
|
|
167
|
+
mocks.dataTableProps?.onSearch?.(' cash ');
|
|
168
|
+
|
|
169
|
+
expect(mocks.replace).toHaveBeenCalledWith('/ws-1/finance/wallets?q=cash');
|
|
170
|
+
});
|
|
171
|
+
});
|