@tuturuuu/ui 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +5 -0
- package/package.json +42 -35
- 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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ChevronUp,
|
|
8
8
|
Clock,
|
|
9
9
|
CreditCard,
|
|
10
|
+
Gauge,
|
|
10
11
|
TrendingDown,
|
|
11
12
|
} from '@tuturuuu/icons';
|
|
12
13
|
import {
|
|
@@ -15,7 +16,13 @@ import {
|
|
|
15
16
|
} from '@tuturuuu/internal-api/finance';
|
|
16
17
|
import type { Wallet } from '@tuturuuu/types';
|
|
17
18
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
Card,
|
|
21
|
+
CardContent,
|
|
22
|
+
CardDescription,
|
|
23
|
+
CardHeader,
|
|
24
|
+
CardTitle,
|
|
25
|
+
} from '@tuturuuu/ui/card';
|
|
19
26
|
import { useCurrencyFormatter } from '@tuturuuu/ui/hooks/use-currency-formatter';
|
|
20
27
|
import { Progress } from '@tuturuuu/ui/progress';
|
|
21
28
|
import { Separator } from '@tuturuuu/ui/separator';
|
|
@@ -77,12 +84,13 @@ export function CreditWalletSummary({
|
|
|
77
84
|
<Card>
|
|
78
85
|
<CardHeader className="pb-2">
|
|
79
86
|
<Skeleton className="h-6 w-48" />
|
|
87
|
+
<Skeleton className="h-4 w-80 max-w-full" />
|
|
80
88
|
</CardHeader>
|
|
81
89
|
<CardContent className="space-y-4">
|
|
82
|
-
<Skeleton className="h-
|
|
83
|
-
<div className="grid gap-
|
|
84
|
-
{[...Array(
|
|
85
|
-
<Skeleton key={i} className="h-
|
|
90
|
+
<Skeleton className="h-28 w-full" />
|
|
91
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
92
|
+
{[...Array(4)].map((_, i) => (
|
|
93
|
+
<Skeleton key={i} className="h-20" />
|
|
86
94
|
))}
|
|
87
95
|
</div>
|
|
88
96
|
</CardContent>
|
|
@@ -90,79 +98,125 @@ export function CreditWalletSummary({
|
|
|
90
98
|
);
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
if (error || !data)
|
|
101
|
+
if (error || !data) {
|
|
102
|
+
return (
|
|
103
|
+
<Card>
|
|
104
|
+
<CardHeader>
|
|
105
|
+
<CardTitle className="flex items-center gap-2 text-lg">
|
|
106
|
+
<CreditCard className="h-5 w-5" />
|
|
107
|
+
{t('credit_operations')}
|
|
108
|
+
</CardTitle>
|
|
109
|
+
<CardDescription>{t('credit_summary_unavailable')}</CardDescription>
|
|
110
|
+
</CardHeader>
|
|
111
|
+
</Card>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const utilization = Math.max(0, data.utilization);
|
|
116
|
+
const progressValue = Math.min(utilization, 100);
|
|
117
|
+
const outstandingDebt = Math.max(data.totalOutstanding, 0);
|
|
118
|
+
const hasCreditBalance = data.balance > 0 && outstandingDebt === 0;
|
|
119
|
+
const isOverLimit = data.availableCredit < 0;
|
|
94
120
|
|
|
95
121
|
return (
|
|
96
122
|
<Card>
|
|
97
123
|
<CardHeader className="pb-2">
|
|
98
124
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
99
125
|
<CreditCard className="h-5 w-5" />
|
|
100
|
-
{t('
|
|
126
|
+
{t('credit_operations')}
|
|
101
127
|
</CardTitle>
|
|
128
|
+
<CardDescription>{t('credit_operations_description')}</CardDescription>
|
|
102
129
|
</CardHeader>
|
|
103
130
|
<CardContent className="space-y-4">
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
<div className="grid gap-4 lg:grid-cols-[1.1fr_1.6fr]">
|
|
132
|
+
<div className="space-y-3 rounded-lg border p-4">
|
|
133
|
+
<div className="flex items-start justify-between gap-3">
|
|
134
|
+
<div>
|
|
135
|
+
<p className="text-muted-foreground text-sm">
|
|
136
|
+
{t('total_outstanding')}
|
|
137
|
+
</p>
|
|
138
|
+
<p className="font-semibold text-2xl">
|
|
139
|
+
{formatVisibleCurrency(outstandingDebt)}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
{isOverLimit ? (
|
|
143
|
+
<Badge variant="destructive">{t('credit_over_limit')}</Badge>
|
|
144
|
+
) : hasCreditBalance ? (
|
|
145
|
+
<Badge variant="secondary">{t('credit_balance_credit')}</Badge>
|
|
146
|
+
) : (
|
|
147
|
+
<Badge variant="secondary">{t('current_cycle')}</Badge>
|
|
114
148
|
)}
|
|
115
|
-
>
|
|
116
|
-
{data.utilization}%
|
|
117
|
-
</span>
|
|
118
|
-
</div>
|
|
119
|
-
<Progress
|
|
120
|
-
value={data.utilization}
|
|
121
|
-
className="h-3"
|
|
122
|
-
indicatorClassName={cn(
|
|
123
|
-
getUtilizationColor(data.utilization),
|
|
124
|
-
'transition-all duration-500'
|
|
125
|
-
)}
|
|
126
|
-
/>
|
|
127
|
-
<p className="text-muted-foreground text-xs">
|
|
128
|
-
{formatVisibleCurrency(data.totalOutstanding)}{' '}
|
|
129
|
-
{t('of_limit', { limit: formatVisibleCurrency(data.limit) })}
|
|
130
|
-
</p>
|
|
131
|
-
</div>
|
|
149
|
+
</div>
|
|
132
150
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<div className="flex items-center justify-between text-sm">
|
|
153
|
+
<span className="text-muted-foreground">
|
|
154
|
+
{t('credit_utilization')}
|
|
155
|
+
</span>
|
|
156
|
+
<span
|
|
157
|
+
className={cn(
|
|
158
|
+
'font-semibold',
|
|
159
|
+
getUtilizationTextColor(utilization)
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{utilization}%
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
<Progress
|
|
166
|
+
value={progressValue}
|
|
167
|
+
className="h-3"
|
|
168
|
+
indicatorClassName={cn(
|
|
169
|
+
getUtilizationColor(utilization),
|
|
170
|
+
'transition-all duration-500'
|
|
171
|
+
)}
|
|
172
|
+
/>
|
|
173
|
+
<p className="text-muted-foreground text-xs">
|
|
174
|
+
{formatVisibleCurrency(outstandingDebt)}{' '}
|
|
175
|
+
{t('of_limit', { limit: formatVisibleCurrency(data.limit) })}
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
152
178
|
</div>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
|
|
180
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
181
|
+
<SummaryTile
|
|
182
|
+
icon={<Gauge className="h-4 w-4" />}
|
|
183
|
+
label={
|
|
184
|
+
isOverLimit ? t('credit_over_limit') : t('credit_available')
|
|
185
|
+
}
|
|
186
|
+
value={formatVisibleCurrency(Math.abs(data.availableCredit))}
|
|
187
|
+
/>
|
|
188
|
+
<SummaryTile
|
|
189
|
+
icon={<TrendingDown className="h-4 w-4" />}
|
|
190
|
+
label={t('current_activity')}
|
|
191
|
+
value={
|
|
192
|
+
data.currentActivity !== 0
|
|
193
|
+
? formatVisibleCurrency(data.currentActivity)
|
|
194
|
+
: t('no_charges')
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
<SummaryTile
|
|
198
|
+
icon={<Calendar className="h-4 w-4" />}
|
|
199
|
+
label={t('next_statement')}
|
|
200
|
+
value={data.nextStatementDate}
|
|
201
|
+
subvalue={t('days_remaining', {
|
|
202
|
+
days: data.daysUntilStatement,
|
|
203
|
+
})}
|
|
204
|
+
/>
|
|
205
|
+
<SummaryTile
|
|
206
|
+
icon={<Clock className="h-4 w-4" />}
|
|
207
|
+
label={t('payment_due_date')}
|
|
208
|
+
value={data.nextPaymentDate}
|
|
209
|
+
subvalue={
|
|
210
|
+
data.daysUntilPayment < 0
|
|
211
|
+
? t('overdue')
|
|
212
|
+
: t('days_remaining', {
|
|
213
|
+
days: data.daysUntilPayment,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
162
217
|
</div>
|
|
163
218
|
</div>
|
|
164
219
|
|
|
165
|
-
{/* Expand/Collapse Toggle */}
|
|
166
220
|
<button
|
|
167
221
|
type="button"
|
|
168
222
|
onClick={handleToggle}
|
|
@@ -181,16 +235,14 @@ export function CreditWalletSummary({
|
|
|
181
235
|
)}
|
|
182
236
|
</button>
|
|
183
237
|
|
|
184
|
-
{/* Expandable Details */}
|
|
185
238
|
{showDetails && (
|
|
186
239
|
<>
|
|
187
240
|
<Separator />
|
|
188
241
|
<div className="space-y-3">
|
|
189
|
-
{/* Statement Balance */}
|
|
190
242
|
<DetailRow
|
|
191
243
|
icon={<TrendingDown className="h-4 w-4" />}
|
|
192
244
|
label={t('statement_balance')}
|
|
193
|
-
sublabel={`${t('previous_cycle')}: ${data.prevCycleStart}
|
|
245
|
+
sublabel={`${t('previous_cycle')}: ${data.prevCycleStart} - ${data.prevCycleEnd}`}
|
|
194
246
|
value={
|
|
195
247
|
data.statementBalance !== 0
|
|
196
248
|
? formatVisibleCurrency(data.statementBalance)
|
|
@@ -200,7 +252,6 @@ export function CreditWalletSummary({
|
|
|
200
252
|
|
|
201
253
|
<Separator />
|
|
202
254
|
|
|
203
|
-
{/* Next Statement Date */}
|
|
204
255
|
<DetailRow
|
|
205
256
|
icon={<Calendar className="h-4 w-4" />}
|
|
206
257
|
label={t('next_statement')}
|
|
@@ -214,7 +265,6 @@ export function CreditWalletSummary({
|
|
|
214
265
|
}
|
|
215
266
|
/>
|
|
216
267
|
|
|
217
|
-
{/* Payment Due Date */}
|
|
218
268
|
<DetailRow
|
|
219
269
|
icon={<Clock className="h-4 w-4" />}
|
|
220
270
|
label={t('payment_due_date')}
|
|
@@ -239,6 +289,31 @@ export function CreditWalletSummary({
|
|
|
239
289
|
);
|
|
240
290
|
}
|
|
241
291
|
|
|
292
|
+
function SummaryTile({
|
|
293
|
+
icon,
|
|
294
|
+
label,
|
|
295
|
+
subvalue,
|
|
296
|
+
value,
|
|
297
|
+
}: {
|
|
298
|
+
icon: React.ReactNode;
|
|
299
|
+
label: string;
|
|
300
|
+
subvalue?: React.ReactNode;
|
|
301
|
+
value: React.ReactNode;
|
|
302
|
+
}) {
|
|
303
|
+
return (
|
|
304
|
+
<div className="rounded-lg border p-3">
|
|
305
|
+
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
|
306
|
+
{icon}
|
|
307
|
+
<p className="text-xs">{label}</p>
|
|
308
|
+
</div>
|
|
309
|
+
<p className="font-semibold text-base">{value}</p>
|
|
310
|
+
{subvalue ? (
|
|
311
|
+
<p className="mt-1 text-muted-foreground text-xs">{subvalue}</p>
|
|
312
|
+
) : null}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
242
317
|
function DetailRow({
|
|
243
318
|
icon,
|
|
244
319
|
label,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { WalletDetailsActions } from './wallet-details-actions';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
transactionForm: vi.fn((_props: unknown) => null),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('next-intl', () => ({
|
|
11
|
+
useTranslations: () => (key: string) => key,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('@tuturuuu/ui/custom/modifiable-dialog-trigger', () => ({
|
|
15
|
+
default: ({ form, open }: { form?: ReactNode; open?: boolean }) =>
|
|
16
|
+
open ? <div data-testid="dialog">{form}</div> : null,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('@tuturuuu/ui/finance/transactions/form', () => ({
|
|
20
|
+
TransactionForm: (...args: Parameters<typeof mocks.transactionForm>) =>
|
|
21
|
+
mocks.transactionForm(...args),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@tuturuuu/ui/finance/wallets/form', () => ({
|
|
25
|
+
WalletForm: () => null,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('./wallet-delete-button', () => ({
|
|
29
|
+
WalletDeleteButton: () => null,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('WalletDetailsActions', () => {
|
|
33
|
+
const baseProps = {
|
|
34
|
+
wsId: 'ws-1',
|
|
35
|
+
walletId: 'wallet-1',
|
|
36
|
+
wallet: {
|
|
37
|
+
id: 'wallet-1',
|
|
38
|
+
name: 'Rewards Card',
|
|
39
|
+
type: 'CREDIT',
|
|
40
|
+
} as never,
|
|
41
|
+
canUpdateWallets: true,
|
|
42
|
+
canCreateTransactions: true,
|
|
43
|
+
canCreateConfidentialTransactions: true,
|
|
44
|
+
canDeleteWallets: false,
|
|
45
|
+
isPersonalWorkspace: true,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('prefills card payments as transfers into the credit wallet', () => {
|
|
53
|
+
render(<WalletDetailsActions {...baseProps} />);
|
|
54
|
+
|
|
55
|
+
fireEvent.click(screen.getByText('wallet-data-table.credit_payment'));
|
|
56
|
+
|
|
57
|
+
expect(mocks.transactionForm).toHaveBeenCalled();
|
|
58
|
+
const props = mocks.transactionForm.mock.calls.at(-1)?.[0];
|
|
59
|
+
|
|
60
|
+
expect(props).toEqual(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
initialMode: 'transfer',
|
|
63
|
+
initialTransfer: expect.objectContaining({
|
|
64
|
+
destination_wallet_id: 'wallet-1',
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('prefills card charges as expense transactions on the credit wallet', () => {
|
|
71
|
+
render(<WalletDetailsActions {...baseProps} />);
|
|
72
|
+
|
|
73
|
+
fireEvent.click(screen.getByText('wallet-data-table.credit_charge'));
|
|
74
|
+
|
|
75
|
+
const props = mocks.transactionForm.mock.calls.at(-1)?.[0];
|
|
76
|
+
|
|
77
|
+
expect(props).toEqual(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
initialMode: 'transaction',
|
|
80
|
+
initialTransaction: expect.objectContaining({
|
|
81
|
+
categoryKind: 'expense',
|
|
82
|
+
origin_wallet_id: 'wallet-1',
|
|
83
|
+
}),
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('prefills card credits as income transactions on the credit wallet', () => {
|
|
89
|
+
render(<WalletDetailsActions {...baseProps} />);
|
|
90
|
+
|
|
91
|
+
fireEvent.click(screen.getByText('wallet-data-table.credit_refund'));
|
|
92
|
+
|
|
93
|
+
const props = mocks.transactionForm.mock.calls.at(-1)?.[0];
|
|
94
|
+
|
|
95
|
+
expect(props).toEqual(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
initialMode: 'transaction',
|
|
98
|
+
initialTransaction: expect.objectContaining({
|
|
99
|
+
categoryKind: 'income',
|
|
100
|
+
origin_wallet_id: 'wallet-1',
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ArrowLeftRight,
|
|
5
|
+
CreditCard,
|
|
6
|
+
Pencil,
|
|
7
|
+
Plus,
|
|
8
|
+
RotateCcw,
|
|
9
|
+
} from '@tuturuuu/icons';
|
|
4
10
|
import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
|
|
5
11
|
import { Button } from '@tuturuuu/ui/button';
|
|
6
12
|
import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger';
|
|
@@ -11,10 +17,17 @@ import { useTranslations } from 'next-intl';
|
|
|
11
17
|
import { useState } from 'react';
|
|
12
18
|
import { WalletDeleteButton } from './wallet-delete-button';
|
|
13
19
|
|
|
20
|
+
export type WalletDetailsAction = 'charge' | 'payment' | 'credit' | 'edit';
|
|
21
|
+
|
|
22
|
+
type WalletTransactionAction =
|
|
23
|
+
| Exclude<WalletDetailsAction, 'edit'>
|
|
24
|
+
| 'transaction';
|
|
25
|
+
|
|
14
26
|
interface WalletDetailsActionsProps {
|
|
15
27
|
wsId: string;
|
|
16
28
|
walletId: string;
|
|
17
29
|
wallet: Wallet;
|
|
30
|
+
initialAction?: WalletDetailsAction | null;
|
|
18
31
|
canUpdateWallets: boolean;
|
|
19
32
|
canCreateTransactions: boolean;
|
|
20
33
|
canCreateConfidentialTransactions: boolean;
|
|
@@ -29,6 +42,7 @@ export function WalletDetailsActions({
|
|
|
29
42
|
wsId,
|
|
30
43
|
walletId,
|
|
31
44
|
wallet,
|
|
45
|
+
initialAction,
|
|
32
46
|
canUpdateWallets,
|
|
33
47
|
canCreateTransactions,
|
|
34
48
|
canCreateConfidentialTransactions,
|
|
@@ -40,14 +54,38 @@ export function WalletDetailsActions({
|
|
|
40
54
|
}: WalletDetailsActionsProps) {
|
|
41
55
|
const t = useTranslations();
|
|
42
56
|
|
|
43
|
-
const
|
|
44
|
-
|
|
57
|
+
const initialTransactionAction =
|
|
58
|
+
initialAction && initialAction !== 'edit' ? initialAction : null;
|
|
59
|
+
const [showEditDialog, setShowEditDialog] = useState(
|
|
60
|
+
initialAction === 'edit'
|
|
61
|
+
);
|
|
62
|
+
const [transactionAction, setTransactionAction] =
|
|
63
|
+
useState<WalletTransactionAction | null>(initialTransactionAction);
|
|
45
64
|
|
|
46
65
|
const hasAnyAction =
|
|
47
66
|
canUpdateWallets || canCreateTransactions || canDeleteWallets;
|
|
48
67
|
|
|
49
68
|
if (!hasAnyAction) return null;
|
|
50
69
|
|
|
70
|
+
const isCreditWallet = wallet.type === 'CREDIT';
|
|
71
|
+
const transactionDialogOpen = transactionAction !== null;
|
|
72
|
+
const transactionDialogTitle =
|
|
73
|
+
transactionAction === 'charge'
|
|
74
|
+
? t('wallet-data-table.credit_charge')
|
|
75
|
+
: transactionAction === 'payment'
|
|
76
|
+
? t('wallet-data-table.credit_payment')
|
|
77
|
+
: transactionAction === 'credit'
|
|
78
|
+
? t('wallet-data-table.credit_refund')
|
|
79
|
+
: t('ws-transactions.create');
|
|
80
|
+
const transactionDialogDescription =
|
|
81
|
+
transactionAction === 'charge'
|
|
82
|
+
? t('wallet-data-table.credit_charge_description')
|
|
83
|
+
: transactionAction === 'payment'
|
|
84
|
+
? t('wallet-data-table.credit_payment_description')
|
|
85
|
+
: transactionAction === 'credit'
|
|
86
|
+
? t('wallet-data-table.credit_refund_description')
|
|
87
|
+
: t('ws-transactions.create_description');
|
|
88
|
+
|
|
51
89
|
return (
|
|
52
90
|
<div className="flex flex-wrap items-center gap-2">
|
|
53
91
|
{canUpdateWallets && (
|
|
@@ -80,24 +118,90 @@ export function WalletDetailsActions({
|
|
|
80
118
|
|
|
81
119
|
{canCreateTransactions && (
|
|
82
120
|
<>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
{isCreditWallet ? (
|
|
122
|
+
<>
|
|
123
|
+
<Button
|
|
124
|
+
variant="outline"
|
|
125
|
+
size="sm"
|
|
126
|
+
onClick={() => setTransactionAction('charge')}
|
|
127
|
+
>
|
|
128
|
+
<CreditCard className="mr-2 h-4 w-4" />
|
|
129
|
+
{t('wallet-data-table.credit_charge')}
|
|
130
|
+
</Button>
|
|
131
|
+
<Button
|
|
132
|
+
variant="outline"
|
|
133
|
+
size="sm"
|
|
134
|
+
onClick={() => setTransactionAction('payment')}
|
|
135
|
+
>
|
|
136
|
+
<ArrowLeftRight className="mr-2 h-4 w-4" />
|
|
137
|
+
{t('wallet-data-table.credit_payment')}
|
|
138
|
+
</Button>
|
|
139
|
+
<Button
|
|
140
|
+
variant="outline"
|
|
141
|
+
size="sm"
|
|
142
|
+
onClick={() => setTransactionAction('credit')}
|
|
143
|
+
>
|
|
144
|
+
<RotateCcw className="mr-2 h-4 w-4" />
|
|
145
|
+
{t('wallet-data-table.credit_refund')}
|
|
146
|
+
</Button>
|
|
147
|
+
</>
|
|
148
|
+
) : (
|
|
149
|
+
<Button
|
|
150
|
+
variant="outline"
|
|
151
|
+
size="sm"
|
|
152
|
+
onClick={() => setTransactionAction('transaction')}
|
|
153
|
+
>
|
|
154
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
155
|
+
{t('ws-transactions.singular')}
|
|
156
|
+
</Button>
|
|
157
|
+
)}
|
|
91
158
|
<ModifiableDialogTrigger
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
open={transactionDialogOpen}
|
|
160
|
+
title={transactionDialogTitle}
|
|
161
|
+
createDescription={transactionDialogDescription}
|
|
162
|
+
setOpen={(open) =>
|
|
163
|
+
setTransactionAction(open ? transactionAction : null)
|
|
164
|
+
}
|
|
97
165
|
forceDefault
|
|
98
166
|
form={
|
|
99
167
|
<TransactionForm
|
|
100
168
|
wsId={wsId}
|
|
169
|
+
initialMode={
|
|
170
|
+
transactionAction === 'payment' ? 'transfer' : 'transaction'
|
|
171
|
+
}
|
|
172
|
+
initialTransaction={
|
|
173
|
+
transactionAction === 'charge'
|
|
174
|
+
? {
|
|
175
|
+
categoryKind: 'expense',
|
|
176
|
+
description: t(
|
|
177
|
+
'wallet-data-table.credit_charge_default_description'
|
|
178
|
+
),
|
|
179
|
+
origin_wallet_id: walletId,
|
|
180
|
+
}
|
|
181
|
+
: transactionAction === 'credit'
|
|
182
|
+
? {
|
|
183
|
+
categoryKind: 'income',
|
|
184
|
+
description: t(
|
|
185
|
+
'wallet-data-table.credit_refund_default_description'
|
|
186
|
+
),
|
|
187
|
+
origin_wallet_id: walletId,
|
|
188
|
+
}
|
|
189
|
+
: transactionAction === 'transaction'
|
|
190
|
+
? {
|
|
191
|
+
origin_wallet_id: walletId,
|
|
192
|
+
}
|
|
193
|
+
: undefined
|
|
194
|
+
}
|
|
195
|
+
initialTransfer={
|
|
196
|
+
transactionAction === 'payment'
|
|
197
|
+
? {
|
|
198
|
+
description: t(
|
|
199
|
+
'wallet-data-table.credit_payment_default_description'
|
|
200
|
+
),
|
|
201
|
+
destination_wallet_id: walletId,
|
|
202
|
+
}
|
|
203
|
+
: undefined
|
|
204
|
+
}
|
|
101
205
|
canCreateTransactions={canCreateTransactions}
|
|
102
206
|
canChangeFinanceWallets={canChangeFinanceWallets}
|
|
103
207
|
canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { WalletDetailsAmount } from './wallet-details-amount';
|
|
4
|
+
|
|
5
|
+
vi.mock('next-intl', () => ({
|
|
6
|
+
useTranslations: () => (key: string) => key,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock('../../shared/use-finance-balance-mode', () => ({
|
|
10
|
+
useFinanceBalanceMode: () => ({
|
|
11
|
+
mode: 'audited',
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../../shared/use-finance-confidential-visibility', () => ({
|
|
16
|
+
FINANCE_HIDDEN_AMOUNT: 'hidden amount',
|
|
17
|
+
useFinanceConfidentialVisibility: () => ({
|
|
18
|
+
isConfidential: false,
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('WalletDetailsAmount', () => {
|
|
23
|
+
it('uses an orange balance badge and compact context badges for varied wallets', () => {
|
|
24
|
+
render(
|
|
25
|
+
<WalletDetailsAmount
|
|
26
|
+
auditedBalance={95}
|
|
27
|
+
auditStatus="unresolved"
|
|
28
|
+
auditVariance={-5}
|
|
29
|
+
currency="USD"
|
|
30
|
+
ledgerBalance={100}
|
|
31
|
+
primary="$100.00"
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(
|
|
36
|
+
screen
|
|
37
|
+
.getByText('$95.00')
|
|
38
|
+
.closest('[data-wallet-details-balance-badge="varied"]')
|
|
39
|
+
).toHaveClass('text-dynamic-orange');
|
|
40
|
+
expect(screen.getByText('ledger')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('variance')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('suppresses audit context for clean checkpoints', () => {
|
|
45
|
+
render(
|
|
46
|
+
<WalletDetailsAmount
|
|
47
|
+
auditedBalance={100}
|
|
48
|
+
auditStatus="clean"
|
|
49
|
+
auditVariance={0}
|
|
50
|
+
currency="USD"
|
|
51
|
+
ledgerBalance={100}
|
|
52
|
+
primary="$100.00"
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(screen.queryByText('ledger')).not.toBeInTheDocument();
|
|
57
|
+
expect(screen.queryByText('variance')).not.toBeInTheDocument();
|
|
58
|
+
expect(
|
|
59
|
+
screen
|
|
60
|
+
.getByText('$100.00')
|
|
61
|
+
.closest('[data-wallet-details-balance-badge="varied"]')
|
|
62
|
+
).not.toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|