@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.
- package/CHANGELOG.md +43 -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 +126 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
- 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/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- 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 +197 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
- 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 +3 -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 +71 -5
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -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 +117 -36
- package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
- 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/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -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/__tests__/use-task-context-actions.test.ts +11 -0
- 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 +124 -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 +268 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- 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-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
- 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/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- 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/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- package/src/hooks/use-task-actions.ts +45 -0
- 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
|
+
});
|