@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
@@ -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 { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
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-8 w-full" />
83
- <div className="grid gap-4 md:grid-cols-3">
84
- {[...Array(3)].map((_, i) => (
85
- <Skeleton key={i} className="h-16" />
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) return null;
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('credit_summary')}
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
- {/* Utilization Bar */}
105
- <div className="space-y-2">
106
- <div className="flex items-center justify-between text-sm">
107
- <span className="text-muted-foreground">
108
- {t('credit_utilization')}
109
- </span>
110
- <span
111
- className={cn(
112
- 'font-semibold',
113
- getUtilizationTextColor(data.utilization)
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
- {/* Hero Stats */}
134
- <div className="grid gap-3 sm:grid-cols-3">
135
- <div className="rounded-lg border p-3">
136
- <p className="text-muted-foreground text-xs">
137
- {t('available_credit')}
138
- </p>
139
- <p className="font-semibold text-lg">
140
- {formatVisibleCurrency(data.availableCredit)}
141
- </p>
142
- </div>
143
- <div className="rounded-lg border p-3">
144
- <p className="text-muted-foreground text-xs">
145
- {t('total_outstanding')}
146
- </p>
147
- <p className="font-semibold text-lg">
148
- {data.totalOutstanding > 0
149
- ? formatVisibleCurrency(data.totalOutstanding)
150
- : formatVisibleCurrency(0)}
151
- </p>
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
- <div className="rounded-lg border p-3">
154
- <p className="text-muted-foreground text-xs">
155
- {t('current_activity')}
156
- </p>
157
- <p className="font-semibold text-lg">
158
- {data.currentActivity !== 0
159
- ? formatVisibleCurrency(data.currentActivity)
160
- : t('no_charges')}
161
- </p>
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} ${data.prevCycleEnd}`}
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 { Pencil, Plus } from '@tuturuuu/icons';
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 [showEditDialog, setShowEditDialog] = useState(false);
44
- const [showTransactionDialog, setShowTransactionDialog] = useState(false);
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
- <Button
84
- variant="outline"
85
- size="sm"
86
- onClick={() => setShowTransactionDialog(true)}
87
- >
88
- <Plus className="mr-2 h-4 w-4" />
89
- {t('ws-transactions.singular')}
90
- </Button>
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
- data={{ wallet_id: walletId }}
93
- open={showTransactionDialog}
94
- title={t('ws-transactions.create')}
95
- createDescription={t('ws-transactions.create_description')}
96
- setOpen={setShowTransactionDialog}
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
+ });