@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
@@ -1,7 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useQueryClient } from '@tanstack/react-query';
3
+ import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { ChevronDown, Loader2 } from '@tuturuuu/icons';
5
+ import { listInfiniteWallets } from '@tuturuuu/internal-api/finance';
4
6
  import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
7
+ import { Button } from '@tuturuuu/ui/button';
5
8
  import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger';
6
9
  import { DataTable } from '@tuturuuu/ui/custom/tables/data-table';
7
10
  import { walletColumns } from '@tuturuuu/ui/finance/wallets/columns';
@@ -9,32 +12,29 @@ import { WalletForm } from '@tuturuuu/ui/finance/wallets/form';
9
12
  import { useExchangeRates } from '@tuturuuu/ui/hooks/use-exchange-rates';
10
13
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
11
14
  import { useTranslations } from 'next-intl';
12
- import { type ReactNode, useCallback, useState } from 'react';
15
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { useFinanceBalanceMode } from '../shared/use-finance-balance-mode';
17
+
18
+ const WALLET_PAGE_SIZE = 20;
13
19
 
14
20
  interface WalletsDataTableProps {
15
21
  wsId: string;
16
- data: Wallet[];
17
- count: number;
18
- filters?: ReactNode[];
19
22
  canUpdateWallets?: boolean;
20
23
  canDeleteWallets?: boolean;
21
24
  currency?: string;
25
+ financePrefix?: string;
22
26
  isPersonalWorkspace?: boolean;
23
- page?: string;
24
- pageSize?: string;
27
+ query?: string;
25
28
  }
26
29
 
27
30
  export function WalletsDataTable({
28
31
  wsId,
29
- data,
30
- count,
31
- filters,
32
32
  canUpdateWallets,
33
33
  canDeleteWallets,
34
34
  currency = 'USD',
35
+ financePrefix = '/finance',
35
36
  isPersonalWorkspace,
36
- page,
37
- pageSize,
37
+ query,
38
38
  }: WalletsDataTableProps) {
39
39
  const t = useTranslations();
40
40
  const router = useRouter();
@@ -42,6 +42,32 @@ export function WalletsDataTable({
42
42
  const searchParams = useSearchParams();
43
43
  const queryClient = useQueryClient();
44
44
  const { data: exchangeRatesResponse } = useExchangeRates();
45
+ const { mode: balanceMode } = useFinanceBalanceMode();
46
+ const loadMoreRef = useRef<HTMLDivElement | null>(null);
47
+ const normalizedQuery = query?.trim() ?? '';
48
+ const walletsQuery = useInfiniteQuery({
49
+ queryKey: ['wallets', wsId, 'infinite', normalizedQuery, WALLET_PAGE_SIZE],
50
+ queryFn: ({ pageParam }) =>
51
+ listInfiniteWallets(wsId, {
52
+ limit: WALLET_PAGE_SIZE,
53
+ offset: pageParam,
54
+ q: normalizedQuery,
55
+ }),
56
+ getNextPageParam: (lastPage) => lastPage.nextOffset ?? undefined,
57
+ initialPageParam: 0,
58
+ });
59
+ const wallets = useMemo(
60
+ () =>
61
+ (walletsQuery.data?.pages.flatMap((page) => page.data) ?? []).map(
62
+ (wallet) => ({
63
+ ...wallet,
64
+ href: `/${wsId}${financePrefix}/wallets/${wallet.id}`,
65
+ ws_id: wsId,
66
+ })
67
+ ),
68
+ [financePrefix, walletsQuery.data?.pages, wsId]
69
+ );
70
+ const totalCount = walletsQuery.data?.pages.at(0)?.totalCount ?? 0;
45
71
 
46
72
  // State for edit dialog
47
73
  const [selectedWallet, setSelectedWallet] = useState<Wallet | null>(null);
@@ -63,42 +89,73 @@ export function WalletsDataTable({
63
89
  });
64
90
  }, [queryClient, wsId]);
65
91
 
66
- const handleSetParams = useCallback(
67
- (params: { page?: number; pageSize?: string }) => {
92
+ const updateSearchQuery = useCallback(
93
+ (nextQuery: string) => {
68
94
  const newSearchParams = new URLSearchParams(searchParams?.toString());
95
+ const trimmedQuery = nextQuery.trim();
69
96
 
70
- if (params.page !== undefined) {
71
- newSearchParams.set('page', params.page.toString());
72
- }
73
-
74
- if (params.pageSize !== undefined) {
75
- newSearchParams.set('pageSize', params.pageSize);
97
+ if (trimmedQuery) {
98
+ newSearchParams.set('q', trimmedQuery);
99
+ } else {
100
+ newSearchParams.delete('q');
76
101
  }
102
+ newSearchParams.delete('page');
103
+ newSearchParams.delete('pageSize');
77
104
 
78
- router.push(`${pathname}?${newSearchParams.toString()}`);
105
+ const queryString = newSearchParams.toString();
106
+ router.replace(queryString ? `${pathname}?${queryString}` : pathname);
79
107
  },
80
108
  [pathname, router, searchParams]
81
109
  );
82
110
 
83
- const pageIndex = page ? Number.parseInt(page, 10) - 1 : 0;
84
- const pageSizeValue = pageSize ? Number.parseInt(pageSize, 10) : 10;
111
+ const resetSearchQuery = useCallback(() => {
112
+ updateSearchQuery('');
113
+ }, [updateSearchQuery]);
114
+
115
+ useEffect(() => {
116
+ const target = loadMoreRef.current;
117
+ if (
118
+ !target ||
119
+ !walletsQuery.hasNextPage ||
120
+ walletsQuery.isFetchingNextPage
121
+ ) {
122
+ return;
123
+ }
124
+
125
+ const observer = new IntersectionObserver(
126
+ (entries) => {
127
+ if (entries[0]?.isIntersecting) {
128
+ void walletsQuery.fetchNextPage();
129
+ }
130
+ },
131
+ { rootMargin: '160px' }
132
+ );
133
+
134
+ observer.observe(target);
135
+
136
+ return () => observer.disconnect();
137
+ }, [
138
+ walletsQuery.fetchNextPage,
139
+ walletsQuery.hasNextPage,
140
+ walletsQuery.isFetchingNextPage,
141
+ ]);
85
142
 
86
143
  return (
87
144
  <div className="relative">
88
145
  <DataTable
89
146
  t={t}
90
- data={data}
147
+ data={walletsQuery.isLoading ? undefined : wallets}
91
148
  columnGenerator={walletColumns}
92
149
  extraData={{
150
+ balanceMode,
93
151
  canUpdateWallets,
94
152
  canDeleteWallets,
95
153
  currency,
96
154
  exchangeRates: exchangeRatesResponse?.data,
97
155
  isPersonalWorkspace,
98
156
  }}
99
- filters={filters}
100
157
  namespace="wallet-data-table"
101
- count={count}
158
+ defaultQuery={normalizedQuery}
102
159
  defaultVisibility={{
103
160
  id: false,
104
161
  description: false,
@@ -107,12 +164,58 @@ export function WalletsDataTable({
107
164
  report_opt_in: false,
108
165
  created_at: false,
109
166
  }}
167
+ hidePagination
168
+ isFiltered={normalizedQuery.length > 0}
169
+ onRefresh={() => void walletsQuery.refetch()}
110
170
  onRowClick={canUpdateWallets ? handleRowClick : undefined}
111
- pageIndex={pageIndex}
112
- pageSize={pageSizeValue}
113
- setParams={handleSetParams}
171
+ onSearch={updateSearchQuery}
172
+ resetParams={resetSearchQuery}
114
173
  />
115
174
 
175
+ {!walletsQuery.isLoading && walletsQuery.isError && (
176
+ <div className="rounded-lg border border-dynamic-red/30 bg-dynamic-red/5 p-4 text-dynamic-red text-sm">
177
+ {walletsQuery.error instanceof Error
178
+ ? walletsQuery.error.message
179
+ : t('wallet-data-table.load_error')}
180
+ </div>
181
+ )}
182
+
183
+ {!walletsQuery.isLoading && !walletsQuery.isError && (
184
+ <div
185
+ ref={loadMoreRef}
186
+ className="mt-4 flex flex-col items-center gap-3"
187
+ >
188
+ <div className="text-muted-foreground text-sm">
189
+ {t('wallet-data-table.loaded_count', {
190
+ loaded: wallets.length,
191
+ total: totalCount,
192
+ })}
193
+ </div>
194
+ {walletsQuery.isFetchingNextPage && (
195
+ <div className="flex items-center gap-2 text-muted-foreground text-sm">
196
+ <Loader2 className="h-4 w-4 animate-spin" />
197
+ {t('wallet-data-table.loading_more')}
198
+ </div>
199
+ )}
200
+ {walletsQuery.hasNextPage && !walletsQuery.isFetchingNextPage && (
201
+ <Button
202
+ type="button"
203
+ variant="outline"
204
+ size="sm"
205
+ onClick={() => void walletsQuery.fetchNextPage()}
206
+ >
207
+ <ChevronDown className="mr-2 h-4 w-4" />
208
+ {t('wallet-data-table.load_more')}
209
+ </Button>
210
+ )}
211
+ {!walletsQuery.hasNextPage && wallets.length > WALLET_PAGE_SIZE && (
212
+ <div className="rounded-lg border border-dashed bg-muted/20 px-4 py-2 text-muted-foreground text-sm">
213
+ {t('wallet-data-table.end_of_list')}
214
+ </div>
215
+ )}
216
+ </div>
217
+ )}
218
+
116
219
  {/* Edit Dialog */}
117
220
  {selectedWallet && (
118
221
  <ModifiableDialogTrigger
@@ -1,24 +1,20 @@
1
+ import { render } from '@testing-library/react';
2
+ import type { ReactElement } from 'react';
1
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
4
 
3
5
  const mocks = vi.hoisted(() => ({
6
+ createDialogFeatureSummary: vi.fn((_props: unknown) => null),
4
7
  getPermissions: vi.fn(),
5
8
  getTranslations: vi.fn(),
6
9
  getWorkspace: vi.fn(),
7
10
  getWorkspaceConfig: vi.fn(),
8
- headers: vi.fn(),
9
- listWallets: vi.fn(),
10
11
  notFound: vi.fn(() => {
11
12
  throw new Error('notFound');
12
13
  }),
13
- withForwardedInternalApiAuth: vi.fn(() => ({ headers: {} })),
14
- }));
15
-
16
- vi.mock('@tuturuuu/internal-api', () => ({
17
- listWallets: (...args: Parameters<typeof mocks.listWallets>) =>
18
- mocks.listWallets(...args),
19
- withForwardedInternalApiAuth: (
20
- ...args: Parameters<typeof mocks.withForwardedInternalApiAuth>
21
- ) => mocks.withForwardedInternalApiAuth(...args),
14
+ walletCheckpointHistoryDialog: vi.fn((_props: unknown) => null),
15
+ walletForm: vi.fn((_props: unknown) => null),
16
+ walletTotalCheckDialog: vi.fn((_props: unknown) => null),
17
+ walletsDataTable: vi.fn((_props: unknown) => null),
22
18
  }));
23
19
 
24
20
  vi.mock('@tuturuuu/utils/workspace-helper', () => ({
@@ -30,11 +26,6 @@ vi.mock('@tuturuuu/utils/workspace-helper', () => ({
30
26
  mocks.getWorkspaceConfig(...args),
31
27
  }));
32
28
 
33
- vi.mock('next/headers', () => ({
34
- headers: (...args: Parameters<typeof mocks.headers>) =>
35
- mocks.headers(...args),
36
- }));
37
-
38
29
  vi.mock('next/navigation', () => ({
39
30
  notFound: (...args: Parameters<typeof mocks.notFound>) =>
40
31
  mocks.notFound(...args),
@@ -50,22 +41,45 @@ vi.mock('@tuturuuu/ui/custom/feature-summary', () => ({
50
41
  }));
51
42
 
52
43
  vi.mock('@tuturuuu/ui/finance/shared/create-dialog-feature-summary', () => ({
53
- CreateDialogFeatureSummary: () => null,
44
+ CreateDialogFeatureSummary: (
45
+ ...args: Parameters<typeof mocks.createDialogFeatureSummary>
46
+ ) => mocks.createDialogFeatureSummary(...args),
54
47
  }));
55
48
 
49
+ vi.mock('@tuturuuu/ui/finance/shared/balance-mode-toggle', () => ({
50
+ FinanceBalanceModeToggle: () => null,
51
+ }));
52
+
53
+ vi.mock('@tuturuuu/ui/finance/shared/numbers-visibility-toggle', () => ({
54
+ FinanceNumbersVisibilityToggle: () => null,
55
+ }));
56
+
57
+ vi.mock(
58
+ '@tuturuuu/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog',
59
+ () => ({
60
+ WalletCheckpointHistoryDialog: (
61
+ ...args: Parameters<typeof mocks.walletCheckpointHistoryDialog>
62
+ ) => mocks.walletCheckpointHistoryDialog(...args),
63
+ })
64
+ );
65
+
56
66
  vi.mock(
57
67
  '@tuturuuu/ui/finance/wallets/checkpoints/wallet-total-check-dialog',
58
68
  () => ({
59
- WalletTotalCheckDialog: () => null,
69
+ WalletTotalCheckDialog: (
70
+ ...args: Parameters<typeof mocks.walletTotalCheckDialog>
71
+ ) => mocks.walletTotalCheckDialog(...args),
60
72
  })
61
73
  );
62
74
 
63
75
  vi.mock('@tuturuuu/ui/finance/wallets/form', () => ({
64
- WalletForm: () => null,
76
+ WalletForm: (...args: Parameters<typeof mocks.walletForm>) =>
77
+ mocks.walletForm(...args),
65
78
  }));
66
79
 
67
80
  vi.mock('@tuturuuu/ui/finance/wallets/wallets-data-table', () => ({
68
- WalletsDataTable: () => null,
81
+ WalletsDataTable: (...args: Parameters<typeof mocks.walletsDataTable>) =>
82
+ mocks.walletsDataTable(...args),
69
83
  }));
70
84
 
71
85
  vi.mock('@tuturuuu/ui/separator', () => ({
@@ -79,36 +93,96 @@ describe('wallets page', () => {
79
93
  mocks.getTranslations.mockResolvedValue((key: string) => key);
80
94
  mocks.getPermissions.mockResolvedValue({
81
95
  containsPermission: vi.fn((permission: string) =>
82
- ['create_wallets', 'update_wallets', 'delete_wallets'].includes(
83
- permission
84
- )
96
+ [
97
+ 'create_transactions',
98
+ 'create_wallets',
99
+ 'update_wallets',
100
+ 'delete_wallets',
101
+ ].includes(permission)
85
102
  ),
86
103
  });
87
104
  mocks.getWorkspaceConfig.mockResolvedValue('USD');
88
105
  mocks.getWorkspace.mockResolvedValue({ personal: false });
89
- mocks.headers.mockResolvedValue(new Headers());
90
- mocks.listWallets.mockResolvedValue([
91
- { id: 'wallet-1', name: 'Primary Wallet' },
92
- ]);
93
106
  });
94
107
 
95
- it('loads wallets through the internal API helper', async () => {
108
+ it('passes wallet controls and the search query to the infinite table', async () => {
96
109
  const { default: WalletsPageModule } = await import('./wallets-page.js');
97
110
  const WalletsPage = WalletsPageModule as unknown as (props: {
98
111
  wsId: string;
99
- searchParams: { q: string; page: string; pageSize: string };
100
- page?: string;
101
- pageSize?: string;
112
+ searchParams: { q: string };
102
113
  }) => Promise<unknown>;
103
114
 
104
- await WalletsPage({
115
+ const element = await WalletsPage({
105
116
  wsId: 'ws-1',
106
- searchParams: { q: '', page: '1', pageSize: '10' },
107
- page: '1',
108
- pageSize: '10',
117
+ searchParams: { q: 'bank' },
109
118
  });
110
119
 
111
- expect(mocks.withForwardedInternalApiAuth).toHaveBeenCalled();
112
- expect(mocks.listWallets).toHaveBeenCalledWith('ws-1', { headers: {} });
120
+ render(element as ReactElement);
121
+
122
+ expect(mocks.walletsDataTable).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ canDeleteWallets: true,
125
+ canUpdateWallets: true,
126
+ currency: 'USD',
127
+ financePrefix: '/finance',
128
+ isPersonalWorkspace: false,
129
+ query: 'bank',
130
+ wsId: 'ws-1',
131
+ }),
132
+ undefined
133
+ );
134
+ expect(mocks.walletTotalCheckDialog).toHaveBeenCalledWith(
135
+ expect.objectContaining({
136
+ canUpdateWallets: true,
137
+ currency: 'USD',
138
+ wsId: 'ws-1',
139
+ }),
140
+ undefined
141
+ );
142
+ expect(mocks.walletCheckpointHistoryDialog).toHaveBeenCalledWith(
143
+ expect.objectContaining({
144
+ canCreateTransactions: true,
145
+ financePrefix: '/finance',
146
+ wsId: 'ws-1',
147
+ }),
148
+ undefined
149
+ );
150
+ });
151
+
152
+ it('opens wallet creation in credit-card mode from search params', async () => {
153
+ const { default: WalletsPageModule } = await import('./wallets-page.js');
154
+ const WalletsPage = WalletsPageModule as unknown as (props: {
155
+ wsId: string;
156
+ searchParams: {
157
+ create?: string;
158
+ q: string;
159
+ };
160
+ }) => Promise<ReactElement>;
161
+
162
+ const element = await WalletsPage({
163
+ wsId: 'ws-1',
164
+ searchParams: {
165
+ create: 'credit-card',
166
+ q: '',
167
+ },
168
+ });
169
+
170
+ render(element);
171
+
172
+ const createDialogProps =
173
+ mocks.createDialogFeatureSummary.mock.calls[0]?.[0];
174
+
175
+ expect(createDialogProps).toEqual(
176
+ expect.objectContaining({
177
+ defaultOpen: true,
178
+ })
179
+ );
180
+ const form = (createDialogProps as { form?: ReactElement }).form;
181
+
182
+ expect(form?.props).toEqual(
183
+ expect.objectContaining({
184
+ defaultType: 'CREDIT',
185
+ })
186
+ );
113
187
  });
114
188
  });
@@ -1,10 +1,7 @@
1
- import {
2
- type InternalApiClientOptions,
3
- listWallets,
4
- withForwardedInternalApiAuth,
5
- } from '@tuturuuu/internal-api';
6
- import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
1
+ import { FinanceBalanceModeToggle } from '@tuturuuu/ui/finance/shared/balance-mode-toggle';
7
2
  import { CreateDialogFeatureSummary } from '@tuturuuu/ui/finance/shared/create-dialog-feature-summary';
3
+ import { FinanceNumbersVisibilityToggle } from '@tuturuuu/ui/finance/shared/numbers-visibility-toggle';
4
+ import { WalletCheckpointHistoryDialog } from '@tuturuuu/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog';
8
5
  import { WalletTotalCheckDialog } from '@tuturuuu/ui/finance/wallets/checkpoints/wallet-total-check-dialog';
9
6
  import { WalletForm } from '@tuturuuu/ui/finance/wallets/form';
10
7
  import { WalletsDataTable } from '@tuturuuu/ui/finance/wallets/wallets-data-table';
@@ -15,7 +12,6 @@ import {
15
12
  getWorkspaceConfig,
16
13
  type PermissionsResult,
17
14
  } from '@tuturuuu/utils/workspace-helper';
18
- import { headers } from 'next/headers';
19
15
  import { notFound } from 'next/navigation';
20
16
  import { getTranslations } from 'next-intl/server';
21
17
 
@@ -23,15 +19,10 @@ interface Props {
23
19
  wsId: string;
24
20
  searchParams: {
25
21
  create?: string;
26
- q: string;
27
- page: string;
28
- pageSize: string;
22
+ q?: string;
29
23
  };
30
- page?: string;
31
- pageSize?: string;
32
24
  currency?: string;
33
25
  financePrefix?: string;
34
- internalApiOptions?: InternalApiClientOptions;
35
26
  openCreateDialog?: boolean;
36
27
  permissions?: PermissionsResult;
37
28
  workspace?: {
@@ -42,11 +33,8 @@ interface Props {
42
33
  export default async function WalletsPage({
43
34
  wsId,
44
35
  searchParams,
45
- page,
46
- pageSize,
47
36
  currency,
48
37
  financePrefix = '/finance',
49
- internalApiOptions,
50
38
  openCreateDialog = false,
51
39
  permissions,
52
40
  workspace,
@@ -66,19 +54,8 @@ export default async function WalletsPage({
66
54
  const canCreateWallets = containsPermission('create_wallets');
67
55
  const canUpdateWallets = containsPermission('update_wallets');
68
56
  const canDeleteWallets = containsPermission('delete_wallets');
69
- const resolvedInternalApiOptions =
70
- internalApiOptions ?? withForwardedInternalApiAuth(await headers());
71
- const {
72
- data: rawData,
73
- count,
74
- allWallets,
75
- } = await getData(wsId, searchParams, resolvedInternalApiOptions);
76
-
77
- const data = rawData.map((d) => ({
78
- ...d,
79
- href: `/${wsId}${financePrefix}/wallets/${d.id}`,
80
- ws_id: wsId,
81
- }));
57
+ const canCreateTransactions = containsPermission('create_transactions');
58
+ const isCreditCardCreate = searchParams.create === 'credit-card';
82
59
 
83
60
  return (
84
61
  <>
@@ -88,65 +65,48 @@ export default async function WalletsPage({
88
65
  description={t('ws-wallets.description')}
89
66
  createTitle={t('ws-wallets.create')}
90
67
  createDescription={t('ws-wallets.create_description')}
91
- defaultOpen={openCreateDialog || searchParams.create === 'wallet'}
92
- form={canCreateWallets ? <WalletForm wsId={wsId} /> : undefined}
68
+ defaultOpen={
69
+ openCreateDialog ||
70
+ searchParams.create === 'wallet' ||
71
+ isCreditCardCreate
72
+ }
73
+ form={
74
+ canCreateWallets ? (
75
+ <WalletForm
76
+ wsId={wsId}
77
+ defaultType={isCreditCardCreate ? 'CREDIT' : 'STANDARD'}
78
+ />
79
+ ) : undefined
80
+ }
93
81
  />
94
82
  <Separator className="my-4" />
95
- <div className="mb-4 flex justify-end">
96
- <WalletTotalCheckDialog
97
- wsId={wsId}
98
- wallets={allWallets
99
- .filter((wallet) => !!wallet.id)
100
- .map((wallet) => ({
101
- balance: wallet.balance,
102
- currency: wallet.currency || resolvedCurrency || 'USD',
103
- id: wallet.id as string,
104
- name: wallet.name,
105
- }))}
106
- canUpdateWallets={canUpdateWallets}
107
- />
83
+ <div className="mb-4 flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
84
+ <div className="flex flex-wrap gap-2">
85
+ <FinanceBalanceModeToggle />
86
+ <FinanceNumbersVisibilityToggle />
87
+ </div>
88
+ <div className="flex flex-wrap gap-2 lg:justify-end">
89
+ <WalletCheckpointHistoryDialog
90
+ wsId={wsId}
91
+ financePrefix={financePrefix}
92
+ canCreateTransactions={canCreateTransactions}
93
+ />
94
+ <WalletTotalCheckDialog
95
+ wsId={wsId}
96
+ currency={resolvedCurrency ?? 'USD'}
97
+ canUpdateWallets={canUpdateWallets}
98
+ />
99
+ </div>
108
100
  </div>
109
101
  <WalletsDataTable
110
102
  wsId={wsId}
111
- data={data}
112
- count={count}
113
103
  canUpdateWallets={canUpdateWallets}
114
104
  canDeleteWallets={canDeleteWallets}
115
105
  currency={resolvedCurrency ?? 'USD'}
106
+ financePrefix={financePrefix}
116
107
  isPersonalWorkspace={!!resolvedWorkspace?.personal}
117
- page={page}
118
- pageSize={pageSize}
108
+ query={searchParams.q}
119
109
  />
120
110
  </>
121
111
  );
122
112
  }
123
-
124
- async function getData(
125
- wsId: string,
126
- {
127
- q,
128
- page = '1',
129
- pageSize = '10',
130
- }: { q?: string; page?: string; pageSize?: string },
131
- internalApiOptions: Parameters<typeof listWallets>[1]
132
- ) {
133
- const wallets = await listWallets(wsId, internalApiOptions);
134
- const normalizedQuery = q?.trim().toLowerCase();
135
- const filteredWallets = wallets
136
- .filter((wallet) =>
137
- normalizedQuery
138
- ? wallet.name?.toLowerCase().includes(normalizedQuery)
139
- : true
140
- )
141
- .sort((a, b) => (a.name || '').localeCompare(b.name || ''));
142
-
143
- const parsedPage = parseInt(page, 10);
144
- const parsedPageSize = parseInt(pageSize, 10);
145
- const start = (parsedPage - 1) * parsedPageSize;
146
-
147
- return {
148
- data: filteredWallets.slice(start, start + parsedPageSize) as Wallet[],
149
- allWallets: wallets as Wallet[],
150
- count: filteredWallets.length,
151
- };
152
- }
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@tuturuuu/utils/format';
4
+ import type { ReactNode } from 'react';
5
+
6
+ export function AccentButton({
7
+ children,
8
+ disabled,
9
+ onClick,
10
+ radius,
11
+ }: {
12
+ children: ReactNode;
13
+ disabled?: boolean;
14
+ onClick?: () => void;
15
+ radius: string;
16
+ }) {
17
+ return (
18
+ <button
19
+ className={cn(
20
+ 'inline-flex h-9 items-center justify-center gap-2 px-3 font-medium text-sm transition disabled:pointer-events-none disabled:opacity-50',
21
+ 'bg-primary text-primary-foreground hover:bg-primary/90',
22
+ '[--accent-bg:var(--storefront-accent,var(--primary))] [--accent-fg:var(--storefront-accent-foreground,var(--primary-foreground))]',
23
+ 'bg-[var(--accent-bg)] text-[var(--accent-fg)] hover:opacity-90',
24
+ radius
25
+ )}
26
+ disabled={disabled}
27
+ onClick={onClick}
28
+ type={onClick ? 'button' : 'submit'}
29
+ >
30
+ {children}
31
+ </button>
32
+ );
33
+ }