@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -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 +50 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
  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/wallet-filter.tsx +21 -2
  22. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
  23. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  24. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
  25. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
  28. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  29. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  30. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  31. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  32. package/src/components/ui/finance/wallets/form.tsx +41 -197
  33. package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
  34. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  35. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  36. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  40. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  41. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
  42. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
  43. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  44. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  45. package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
  46. package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
  47. package/src/components/ui/storefront/accent-button.tsx +33 -0
  48. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  49. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  50. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  51. package/src/components/ui/storefront/image-panel.tsx +40 -0
  52. package/src/components/ui/storefront/index.ts +12 -0
  53. package/src/components/ui/storefront/listing-card.tsx +129 -0
  54. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  55. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  56. package/src/components/ui/storefront/types.ts +99 -0
  57. package/src/components/ui/storefront/utils.ts +90 -0
  58. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  59. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  60. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  61. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  62. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  63. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  64. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  65. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
  66. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  67. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  68. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  69. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  70. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  71. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  72. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  73. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  85. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
  86. package/src/hooks/useBoardRealtime.ts +54 -1
  87. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  88. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -0,0 +1,72 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+
5
+ const BALANCE_MODE_COOKIE_NAME = 'finance-balance-mode';
6
+ const BALANCE_MODE_CHANGE_EVENT = 'finance-balance-mode-change';
7
+
8
+ export type FinanceBalanceMode = 'audited' | 'ledger';
9
+
10
+ const isBalanceMode = (value: string | null): value is FinanceBalanceMode =>
11
+ value === 'audited' || value === 'ledger';
12
+
13
+ const getCookie = (name: string): string | null => {
14
+ if (typeof document === 'undefined') return null;
15
+ const nameEQ = `${name}=`;
16
+ const cookies = document.cookie.split(';');
17
+ for (let i = 0; i < cookies.length; i += 1) {
18
+ let cookie = cookies[i];
19
+ if (!cookie) continue;
20
+ while (cookie.charAt(0) === ' ') cookie = cookie.substring(1);
21
+ if (cookie.indexOf(nameEQ) === 0) {
22
+ return cookie.substring(nameEQ.length);
23
+ }
24
+ }
25
+ return null;
26
+ };
27
+
28
+ const setCookie = (name: string, value: string, days = 365) => {
29
+ const expires = new Date();
30
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
31
+ // biome-ignore lint/suspicious/noDocumentCookie: Used for finance balance mode persistence.
32
+ document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
33
+ };
34
+
35
+ export function useFinanceBalanceMode() {
36
+ const [mode, setModeState] = useState<FinanceBalanceMode>('ledger');
37
+
38
+ useEffect(() => {
39
+ const saved = getCookie(BALANCE_MODE_COOKIE_NAME);
40
+ if (isBalanceMode(saved)) {
41
+ setModeState(saved);
42
+ }
43
+
44
+ const handleBalanceModeChange = () => {
45
+ const nextValue = getCookie(BALANCE_MODE_COOKIE_NAME);
46
+ if (isBalanceMode(nextValue)) {
47
+ setModeState(nextValue);
48
+ }
49
+ };
50
+
51
+ window.addEventListener(BALANCE_MODE_CHANGE_EVENT, handleBalanceModeChange);
52
+
53
+ return () => {
54
+ window.removeEventListener(
55
+ BALANCE_MODE_CHANGE_EVENT,
56
+ handleBalanceModeChange
57
+ );
58
+ };
59
+ }, []);
60
+
61
+ const setMode = useCallback((nextMode: FinanceBalanceMode) => {
62
+ setModeState(nextMode);
63
+ setCookie(BALANCE_MODE_COOKIE_NAME, nextMode);
64
+ window.dispatchEvent(new Event(BALANCE_MODE_CHANGE_EVENT));
65
+ }, []);
66
+
67
+ return {
68
+ isAuditedMode: mode === 'audited',
69
+ mode,
70
+ setMode,
71
+ };
72
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getWalletBalanceTone,
4
+ resolveWalletBalanceForMode,
5
+ } from './wallet-balance-mode';
6
+
7
+ describe('wallet balance mode resolver', () => {
8
+ it('uses ledger balance in ledger mode', () => {
9
+ expect(
10
+ resolveWalletBalanceForMode(
11
+ {
12
+ audit_balance: 150,
13
+ audit_status: 'unresolved',
14
+ balance: 100,
15
+ },
16
+ 'ledger'
17
+ )
18
+ ).toMatchObject({
19
+ contextBalance: 150,
20
+ displayBalance: 100,
21
+ hasAuditedBalance: true,
22
+ usesAuditedBalance: false,
23
+ });
24
+ });
25
+
26
+ it('uses audited balance in audited mode when a checkpoint exists', () => {
27
+ expect(
28
+ resolveWalletBalanceForMode(
29
+ {
30
+ audit_balance: -25,
31
+ audit_status: 'unresolved',
32
+ balance: 100,
33
+ },
34
+ 'audited'
35
+ )
36
+ ).toMatchObject({
37
+ contextBalance: 100,
38
+ displayBalance: -25,
39
+ hasAuditedBalance: true,
40
+ usesAuditedBalance: true,
41
+ });
42
+ });
43
+
44
+ it('falls back to ledger balance in audited mode without a checkpoint', () => {
45
+ expect(
46
+ resolveWalletBalanceForMode(
47
+ {
48
+ audit_status: 'no_checkpoint',
49
+ balance: 100,
50
+ },
51
+ 'audited'
52
+ )
53
+ ).toMatchObject({
54
+ contextBalance: null,
55
+ displayBalance: 100,
56
+ hasAuditedBalance: false,
57
+ usesAuditedBalance: false,
58
+ });
59
+ });
60
+
61
+ it('derives tone from the effective balance', () => {
62
+ expect(getWalletBalanceTone(10)).toBe('positive');
63
+ expect(getWalletBalanceTone(-10)).toBe('negative');
64
+ expect(getWalletBalanceTone(0)).toBe('neutral');
65
+ });
66
+ });
@@ -0,0 +1,42 @@
1
+ import type { FinanceBalanceMode } from './use-finance-balance-mode';
2
+
3
+ type WalletBalanceInput = {
4
+ audit_balance?: number | null;
5
+ audit_status?: 'clean' | 'no_checkpoint' | 'unresolved' | null;
6
+ audit_variance?: number | null;
7
+ balance?: number | null;
8
+ };
9
+
10
+ function toFiniteNumber(value: number | null | undefined) {
11
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
12
+ }
13
+
14
+ export function resolveWalletBalanceForMode(
15
+ wallet: WalletBalanceInput,
16
+ mode: FinanceBalanceMode
17
+ ) {
18
+ const ledgerBalance = toFiniteNumber(wallet.balance) ?? 0;
19
+ const auditedBalance = toFiniteNumber(wallet.audit_balance);
20
+ const hasAuditedBalance = auditedBalance !== null;
21
+ const usesAuditedBalance = mode === 'audited' && hasAuditedBalance;
22
+ const displayBalance = usesAuditedBalance ? auditedBalance : ledgerBalance;
23
+ const contextBalance = usesAuditedBalance ? ledgerBalance : auditedBalance;
24
+
25
+ return {
26
+ auditedBalance,
27
+ auditStatus: wallet.audit_status ?? null,
28
+ auditVariance: toFiniteNumber(wallet.audit_variance),
29
+ contextBalance,
30
+ displayBalance,
31
+ hasAuditedBalance,
32
+ isAuditedMode: mode === 'audited',
33
+ ledgerBalance,
34
+ usesAuditedBalance,
35
+ };
36
+ }
37
+
38
+ export function getWalletBalanceTone(balance: number) {
39
+ if (balance > 0) return 'positive';
40
+ if (balance < 0) return 'negative';
41
+ return 'neutral';
42
+ }
@@ -23,6 +23,26 @@ export type TransactionCategory = DbTransactionCategory;
23
23
  export type Wallet = DbWallet;
24
24
  export type TagDraft = Pick<DbTag, 'name'> & Partial<Pick<DbTag, 'color'>>;
25
25
 
26
+ export type TransactionFormInitialMode = 'transaction' | 'transfer';
27
+
28
+ export interface TransactionFormInitialTransaction {
29
+ amount?: number;
30
+ category_id?: string;
31
+ categoryKind?: 'expense' | 'income';
32
+ description?: string;
33
+ origin_wallet_id?: string;
34
+ taken_at?: Date;
35
+ }
36
+
37
+ export interface TransactionFormInitialTransfer {
38
+ amount?: number;
39
+ description?: string;
40
+ destination_amount?: number;
41
+ destination_wallet_id?: string;
42
+ origin_wallet_id?: string;
43
+ taken_at?: Date;
44
+ }
45
+
26
46
  export interface TransactionFormProps {
27
47
  wsId: string;
28
48
  data?: Partial<Transaction>;
@@ -33,6 +53,9 @@ export interface TransactionFormProps {
33
53
  canUpdateConfidentialTransactions?: boolean;
34
54
  canChangeFinanceWallets?: boolean;
35
55
  canSetFinanceWalletsOnCreate?: boolean;
56
+ initialMode?: TransactionFormInitialMode;
57
+ initialTransaction?: TransactionFormInitialTransaction;
58
+ initialTransfer?: TransactionFormInitialTransfer;
36
59
  permissionRequestUser?: FinancePermissionRequestUser | null;
37
60
  }
38
61
 
@@ -73,6 +73,9 @@ export function TransactionForm({
73
73
  canUpdateConfidentialTransactions,
74
74
  canChangeFinanceWallets = true,
75
75
  canSetFinanceWalletsOnCreate = true,
76
+ initialMode = 'transaction',
77
+ initialTransaction,
78
+ initialTransfer,
76
79
  permissionRequestUser,
77
80
  }: TransactionFormProps) {
78
81
  const t = useTranslations();
@@ -83,7 +86,8 @@ export function TransactionForm({
83
86
  const [attachments, setAttachments] = useState<TransactionAttachmentDraft[]>(
84
87
  []
85
88
  );
86
- const [isTransfer, setIsTransfer] = useState(!!data?.transfer);
89
+ const initialIsTransfer = initialMode === 'transfer' || !!data?.transfer;
90
+ const [isTransfer, setIsTransfer] = useState(initialIsTransfer);
87
91
  // Start in override mode when editing an existing transfer (preserve stored amounts).
88
92
  // Start in auto mode for new transfers so the exchange rate pre-fills destination.
89
93
  const [isDestinationOverridden, setIsDestinationOverridden] = useState(
@@ -144,18 +148,39 @@ export function TransactionForm({
144
148
  resolver: zodResolver(TransactionFormSchema),
145
149
  defaultValues: {
146
150
  id: data?.id,
147
- description: data?.description || '',
148
- amount: data?.amount ? Math.abs(data.amount) : undefined,
149
- origin_wallet_id: data?.wallet_id || '',
150
- destination_wallet_id: data?.transfer?.linked_wallet_id || '',
151
+ description:
152
+ data?.description ||
153
+ (initialIsTransfer
154
+ ? initialTransfer?.description
155
+ : initialTransaction?.description) ||
156
+ '',
157
+ amount: data?.amount
158
+ ? Math.abs(data.amount)
159
+ : initialIsTransfer
160
+ ? initialTransfer?.amount
161
+ : initialTransaction?.amount,
162
+ origin_wallet_id:
163
+ data?.wallet_id ||
164
+ (initialIsTransfer
165
+ ? initialTransfer?.origin_wallet_id
166
+ : initialTransaction?.origin_wallet_id) ||
167
+ '',
168
+ destination_wallet_id:
169
+ data?.transfer?.linked_wallet_id ||
170
+ initialTransfer?.destination_wallet_id ||
171
+ '',
151
172
  destination_amount: data?.transfer?.linked_amount
152
173
  ? Math.abs(data.transfer.linked_amount)
153
- : undefined,
154
- category_id: data?.category_id || '',
155
- taken_at: data?.taken_at ? new Date(data.taken_at) : new Date(),
174
+ : initialTransfer?.destination_amount,
175
+ category_id: data?.category_id || initialTransaction?.category_id || '',
176
+ taken_at: data?.taken_at
177
+ ? new Date(data.taken_at)
178
+ : initialIsTransfer
179
+ ? (initialTransfer?.taken_at ?? new Date())
180
+ : (initialTransaction?.taken_at ?? new Date()),
156
181
  report_opt_in: data?.report_opt_in ?? true,
157
182
  tag_ids: [] as string[],
158
- is_transfer: !!data?.transfer,
183
+ is_transfer: initialIsTransfer,
159
184
  is_amount_confidential:
160
185
  (data as Record<string, unknown>)?.is_amount_confidential === true,
161
186
  is_description_confidential:
@@ -189,7 +214,12 @@ export function TransactionForm({
189
214
  const isUserEdited =
190
215
  originWalletState.isDirty || originWalletState.isTouched;
191
216
  const currentWalletId = form.getValues('origin_wallet_id');
192
- const contextualWalletId = data?.wallet_id || '';
217
+ const contextualWalletId =
218
+ data?.wallet_id ||
219
+ (initialIsTransfer
220
+ ? initialTransfer?.origin_wallet_id
221
+ : initialTransaction?.origin_wallet_id) ||
222
+ '';
193
223
 
194
224
  if (data?.id || isUserEdited) return;
195
225
  if (!wallets || wallets.length === 0) return;
@@ -241,6 +271,9 @@ export function TransactionForm({
241
271
  data?.wallet_id,
242
272
  defaultWalletId,
243
273
  form,
274
+ initialIsTransfer,
275
+ initialTransaction?.origin_wallet_id,
276
+ initialTransfer?.origin_wallet_id,
244
277
  isLastSelectionsInitialized,
245
278
  isLoadingRememberLastSelections,
246
279
  lastSelections.walletId,
@@ -253,7 +286,22 @@ export function TransactionForm({
253
286
  const categoryState = form.getFieldState('category_id');
254
287
  const isUserEdited = categoryState.isDirty || categoryState.isTouched;
255
288
  const currentCategoryId = form.getValues('category_id');
256
- const contextualCategoryId = data?.category_id || '';
289
+ const contextualCategoryId =
290
+ data?.category_id || initialTransaction?.category_id || '';
291
+ const categoryKind = initialTransaction?.categoryKind;
292
+ const matchesCategoryKind = (category: { is_expense?: boolean | null }) => {
293
+ if (!categoryKind) return true;
294
+ return categoryKind === 'income'
295
+ ? category.is_expense === false
296
+ : category.is_expense !== false;
297
+ };
298
+ const hasSelectableCategory = (categoryId?: string | null) =>
299
+ !!categoryId &&
300
+ (categories ?? []).some(
301
+ (category) =>
302
+ category.id === categoryId &&
303
+ (!categoryKind || matchesCategoryKind(category))
304
+ );
257
305
 
258
306
  if (data?.id || isUserEdited) return;
259
307
  if (!categories || categories.length === 0) return;
@@ -263,24 +311,33 @@ export function TransactionForm({
263
311
  const rememberedCategoryId =
264
312
  rememberLastSelections &&
265
313
  lastSelections.categoryId &&
266
- categories.some((category) => category.id === lastSelections.categoryId)
314
+ hasSelectableCategory(lastSelections.categoryId)
267
315
  ? lastSelections.categoryId
268
316
  : '';
317
+ const contextualCategorySelection = hasSelectableCategory(
318
+ contextualCategoryId
319
+ )
320
+ ? contextualCategoryId
321
+ : '';
322
+ const defaultCategorySelection = hasSelectableCategory(defaultCategoryId)
323
+ ? defaultCategoryId
324
+ : '';
325
+ const intentCategorySelection =
326
+ categoryKind && categories.find(matchesCategoryKind)?.id
327
+ ? categories.find(matchesCategoryKind)?.id
328
+ : '';
269
329
  const nextCategorySelection =
270
330
  rememberedCategoryId ||
271
- (contextualCategoryId &&
272
- categories.some((category) => category.id === contextualCategoryId)
273
- ? contextualCategoryId
274
- : '') ||
275
- (defaultCategoryId &&
276
- categories.some((category) => category.id === defaultCategoryId)
277
- ? defaultCategoryId
278
- : '');
331
+ contextualCategorySelection ||
332
+ defaultCategorySelection ||
333
+ intentCategorySelection;
279
334
  const sourceLabel = rememberedCategoryId
280
335
  ? t('transaction-data-table.prefill_source_last_used')
281
- : contextualCategoryId && nextCategorySelection === contextualCategoryId
336
+ : contextualCategorySelection &&
337
+ nextCategorySelection === contextualCategorySelection
282
338
  ? t('transaction-data-table.prefill_source_current_context')
283
- : defaultCategoryId && nextCategorySelection === defaultCategoryId
339
+ : defaultCategorySelection &&
340
+ nextCategorySelection === defaultCategorySelection
284
341
  ? t('transaction-data-table.prefill_source_workspace_default')
285
342
  : '';
286
343
 
@@ -304,6 +361,8 @@ export function TransactionForm({
304
361
  data?.id,
305
362
  defaultCategoryId,
306
363
  form,
364
+ initialTransaction?.categoryKind,
365
+ initialTransaction?.category_id,
307
366
  isLastSelectionsInitialized,
308
367
  isLoadingRememberLastSelections,
309
368
  lastSelections.categoryId,
@@ -20,10 +20,12 @@ import { getCurrencyLocale } from '@tuturuuu/utils/currencies';
20
20
  import { cn } from '@tuturuuu/utils/format';
21
21
  import { useTranslations } from 'next-intl';
22
22
  import { useState } from 'react';
23
+ import { useFinanceBalanceMode } from '../shared/use-finance-balance-mode';
23
24
  import {
24
25
  FINANCE_HIDDEN_AMOUNT,
25
26
  useFinanceConfidentialVisibility,
26
27
  } from '../shared/use-finance-confidential-visibility';
28
+ import { resolveWalletBalanceForMode } from '../shared/wallet-balance-mode';
27
29
 
28
30
  interface WalletFilterProps {
29
31
  wsId: string;
@@ -45,9 +47,11 @@ export function WalletFilter({
45
47
  className,
46
48
  }: WalletFilterProps) {
47
49
  const t = useTranslations();
50
+ const checkpointT = useTranslations('wallet-checkpoints');
48
51
  const [isOpen, setIsOpen] = useState(false);
49
52
  const { isConfidential: areNumbersHidden } =
50
53
  useFinanceConfidentialVisibility();
54
+ const { isAuditedMode } = useFinanceBalanceMode();
51
55
 
52
56
  const hasActiveFilters = selectedWalletIds.length > 0;
53
57
 
@@ -135,6 +139,14 @@ export function WalletFilter({
135
139
  })
136
140
  .map((wallet) => {
137
141
  const isSelected = selectedWalletIds.includes(wallet.id);
142
+ const { displayBalance, usesAuditedBalance } =
143
+ resolveWalletBalanceForMode(
144
+ wallet,
145
+ isAuditedMode ? 'audited' : 'ledger'
146
+ );
147
+ const balanceLabel = usesAuditedBalance
148
+ ? checkpointT('audited')
149
+ : checkpointT('ledger');
138
150
 
139
151
  return (
140
152
  <CommandItem
@@ -161,7 +173,7 @@ export function WalletFilter({
161
173
  <span className="text-muted-foreground text-xs">
162
174
  {areNumbersHidden
163
175
  ? FINANCE_HIDDEN_AMOUNT
164
- : Intl.NumberFormat(
176
+ : `${balanceLabel}: ${Intl.NumberFormat(
165
177
  getCurrencyLocale(
166
178
  wallet.currency || 'USD'
167
179
  ),
@@ -171,7 +183,14 @@ export function WalletFilter({
171
183
  minimumFractionDigits: 0,
172
184
  maximumFractionDigits: 0,
173
185
  }
174
- ).format(wallet.balance || 0)}
186
+ ).format(displayBalance)}`}
187
+ {!areNumbersHidden &&
188
+ isAuditedMode &&
189
+ wallet.audit_status === 'no_checkpoint' && (
190
+ <span className="ml-1">
191
+ {checkpointT('no_checkpoint_short')}
192
+ </span>
193
+ )}
175
194
  </span>
176
195
  </div>
177
196
  </div>
@@ -2,9 +2,10 @@
2
2
 
3
3
  import { useMutation, useQuery } from '@tanstack/react-query';
4
4
  import {
5
- createTransaction,
5
+ createWalletCheckpointReconciliation,
6
6
  listTransactionCategories,
7
7
  } from '@tuturuuu/internal-api/finance';
8
+ import { FINANCE_DEFAULT_RECONCILIATION_CATEGORY_CONFIG_ID } from '@tuturuuu/internal-api/workspace-configs';
8
9
  import { Button } from '@tuturuuu/ui/button';
9
10
  import {
10
11
  Dialog,
@@ -14,6 +15,7 @@ import {
14
15
  DialogHeader,
15
16
  DialogTitle,
16
17
  } from '@tuturuuu/ui/dialog';
18
+ import { useWorkspaceConfig } from '@tuturuuu/ui/hooks/use-workspace-config';
17
19
  import { Input } from '@tuturuuu/ui/input';
18
20
  import { Label } from '@tuturuuu/ui/label';
19
21
  import {
@@ -27,12 +29,13 @@ import { toast } from '@tuturuuu/ui/sonner';
27
29
  import { Textarea } from '@tuturuuu/ui/textarea';
28
30
  import { formatCurrency } from '@tuturuuu/utils/format';
29
31
  import { useTranslations } from 'next-intl';
30
- import { useMemo, useState } from 'react';
32
+ import { useEffect, useMemo, useState } from 'react';
31
33
 
32
34
  const NO_CATEGORY = 'none';
33
35
 
34
36
  export function WalletCheckpointAdjustmentDialog({
35
37
  checkedAt,
38
+ checkpointId,
36
39
  currency,
37
40
  onCreated,
38
41
  onOpenChange,
@@ -43,6 +46,7 @@ export function WalletCheckpointAdjustmentDialog({
43
46
  wsId,
44
47
  }: {
45
48
  checkedAt: string;
49
+ checkpointId: string;
46
50
  currency: string;
47
51
  onCreated: () => void;
48
52
  onOpenChange: (open: boolean) => void;
@@ -55,21 +59,59 @@ export function WalletCheckpointAdjustmentDialog({
55
59
  const t = useTranslations('wallet-checkpoints');
56
60
  const [categoryId, setCategoryId] = useState(NO_CATEGORY);
57
61
  const [description, setDescription] = useState(
58
- t('adjustment_description', {
62
+ t('reconciliation_description', {
59
63
  date: new Date(checkedAt).toLocaleDateString(),
60
64
  wallet: walletName,
61
65
  })
62
66
  );
63
- const [takenAt, setTakenAt] = useState(() => {
64
- const date = new Date(checkedAt);
65
- const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
66
- return local.toISOString().slice(0, 16);
67
- });
67
+ const [categoryInitialized, setCategoryInitialized] = useState(false);
68
68
  const categoriesQuery = useQuery({
69
69
  queryKey: ['transaction-categories', wsId],
70
70
  queryFn: () => listTransactionCategories(wsId),
71
71
  enabled: open,
72
72
  });
73
+ const {
74
+ data: defaultReconciliationCategoryId,
75
+ isLoading: isLoadingDefaultReconciliationCategory,
76
+ } = useWorkspaceConfig<string>(
77
+ wsId,
78
+ FINANCE_DEFAULT_RECONCILIATION_CATEGORY_CONFIG_ID,
79
+ ''
80
+ );
81
+
82
+ useEffect(() => {
83
+ if (!open) {
84
+ setCategoryId(NO_CATEGORY);
85
+ setCategoryInitialized(false);
86
+ return;
87
+ }
88
+
89
+ if (
90
+ categoryInitialized ||
91
+ categoriesQuery.isLoading ||
92
+ isLoadingDefaultReconciliationCategory
93
+ ) {
94
+ return;
95
+ }
96
+
97
+ const defaultCategoryExists = (categoriesQuery.data ?? []).some(
98
+ (category) => category.id === defaultReconciliationCategoryId
99
+ );
100
+
101
+ setCategoryId(
102
+ defaultReconciliationCategoryId && defaultCategoryExists
103
+ ? defaultReconciliationCategoryId
104
+ : NO_CATEGORY
105
+ );
106
+ setCategoryInitialized(true);
107
+ }, [
108
+ categoriesQuery.data,
109
+ categoriesQuery.isLoading,
110
+ categoryInitialized,
111
+ defaultReconciliationCategoryId,
112
+ isLoadingDefaultReconciliationCategory,
113
+ open,
114
+ ]);
73
115
  const amountText = useMemo(
74
116
  () =>
75
117
  formatCurrency(variance, currency, undefined, {
@@ -78,24 +120,30 @@ export function WalletCheckpointAdjustmentDialog({
78
120
  }),
79
121
  [currency, variance]
80
122
  );
123
+ const checkedAtText = useMemo(
124
+ () =>
125
+ new Intl.DateTimeFormat(undefined, {
126
+ dateStyle: 'medium',
127
+ timeStyle: 'short',
128
+ }).format(new Date(checkedAt)),
129
+ [checkedAt]
130
+ );
81
131
  const mutation = useMutation({
82
132
  mutationFn: () =>
83
- createTransaction(wsId, {
84
- amount: variance,
133
+ createWalletCheckpointReconciliation(wsId, walletId, checkpointId, {
85
134
  category_id: categoryId === NO_CATEGORY ? undefined : categoryId,
86
135
  description,
87
- origin_wallet_id: walletId,
88
- report_opt_in: false,
89
- taken_at: new Date(takenAt).toISOString(),
90
136
  }),
91
- onSuccess: () => {
92
- toast.success(t('adjustment_created'));
137
+ onSuccess: (result) => {
138
+ toast.success(
139
+ result.created ? t('reconciliation_created') : t('reconciliation_clean')
140
+ );
93
141
  onCreated();
94
142
  onOpenChange(false);
95
143
  },
96
144
  onError: (error) => {
97
145
  toast.error(
98
- error instanceof Error ? error.message : t('adjustment_create_error')
146
+ error instanceof Error ? error.message : t('reconciliation_error')
99
147
  );
100
148
  },
101
149
  });
@@ -104,23 +152,22 @@ export function WalletCheckpointAdjustmentDialog({
104
152
  <Dialog open={open} onOpenChange={onOpenChange}>
105
153
  <DialogContent>
106
154
  <DialogHeader>
107
- <DialogTitle>{t('create_adjustment')}</DialogTitle>
155
+ <DialogTitle>{t('create_reconciliation_transaction')}</DialogTitle>
108
156
  <DialogDescription>
109
- {t('create_adjustment_description')}
157
+ {t('create_reconciliation_description')}
110
158
  </DialogDescription>
111
159
  </DialogHeader>
112
160
  <div className="grid gap-4 py-2">
113
161
  <div className="grid gap-2">
114
- <Label>{t('adjustment_amount')}</Label>
162
+ <Label>{t('offset_amount_preview')}</Label>
115
163
  <Input value={amountText} readOnly />
116
164
  </div>
117
165
  <div className="grid gap-2">
118
- <Label htmlFor="checkpoint-adjustment-date">{t('date')}</Label>
166
+ <Label htmlFor="checkpoint-reconciliation-date">{t('date')}</Label>
119
167
  <Input
120
- id="checkpoint-adjustment-date"
121
- type="datetime-local"
122
- value={takenAt}
123
- onChange={(event) => setTakenAt(event.target.value)}
168
+ id="checkpoint-reconciliation-date"
169
+ value={checkedAtText}
170
+ readOnly
124
171
  />
125
172
  </div>
126
173
  <div className="grid gap-2">
@@ -160,10 +207,10 @@ export function WalletCheckpointAdjustmentDialog({
160
207
  {t('cancel')}
161
208
  </Button>
162
209
  <Button
163
- disabled={mutation.isPending || variance === 0}
210
+ disabled={mutation.isPending || !categoryInitialized}
164
211
  onClick={() => mutation.mutate()}
165
212
  >
166
- {mutation.isPending ? t('creating') : t('create_adjustment')}
213
+ {mutation.isPending ? t('creating') : t('reconcile')}
167
214
  </Button>
168
215
  </DialogFooter>
169
216
  </DialogContent>