@tuturuuu/ui 0.6.1 → 0.7.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 (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -3
  3. package/biome.json +1 -1
  4. package/package.json +8 -8
  5. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  6. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  7. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  8. package/src/components/ui/calendar.test.tsx +24 -0
  9. package/src/components/ui/calendar.tsx +1 -0
  10. package/src/components/ui/date-time-picker.tsx +352 -234
  11. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  12. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  13. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  14. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  15. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  16. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  17. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  18. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  19. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  20. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  21. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  22. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  23. package/src/components/ui/finance/transactions/form.tsx +116 -20
  24. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  25. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  26. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  27. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  28. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  29. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  30. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  31. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  34. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  35. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  36. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  38. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  39. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  40. package/src/components/ui/optional-time-picker.tsx +95 -0
  41. package/src/components/ui/quick-command-center.test.tsx +90 -0
  42. package/src/components/ui/quick-command-center.tsx +190 -0
  43. package/src/components/ui/storefront/cart-summary.tsx +18 -27
  44. package/src/components/ui/storefront/hero-panel.tsx +22 -13
  45. package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
  46. package/src/components/ui/storefront/storefront-surface.tsx +84 -41
  47. package/src/components/ui/storefront/types.ts +2 -0
  48. package/src/components/ui/storefront/utils.ts +21 -0
  49. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  50. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  51. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
@@ -11,6 +11,7 @@ import { Separator } from '@tuturuuu/ui/separator';
11
11
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
12
12
  import { useTranslations } from 'next-intl';
13
13
  import { parseAsString, useQueryState } from 'nuqs';
14
+ import { useEffect } from 'react';
14
15
 
15
16
  interface Props {
16
17
  currency: string;
@@ -30,8 +31,22 @@ export default function CategoriesTagsTabs({
30
31
  shallow: true,
31
32
  })
32
33
  );
34
+ const [create, setCreate] = useQueryState(
35
+ 'create',
36
+ parseAsString.withDefault('').withOptions({
37
+ shallow: true,
38
+ })
39
+ );
33
40
 
34
41
  const activeTab = tab === 'tags' ? 'tags' : 'categories';
42
+ const categoryCreateOpen = create === 'category';
43
+ const tagCreateOpen = create === 'tag';
44
+
45
+ useEffect(() => {
46
+ if (tagCreateOpen && activeTab !== 'tags') {
47
+ setTab('tags');
48
+ }
49
+ }, [activeTab, setTab, tagCreateOpen]);
35
50
 
36
51
  return (
37
52
  <Tabs
@@ -58,6 +73,8 @@ export default function CategoriesTagsTabs({
58
73
  createTitle={t('ws-transaction-categories.create')}
59
74
  createDescription={t('ws-transaction-categories.create_description')}
60
75
  form={<TransactionCategoryForm wsId={wsId} />}
76
+ open={categoryCreateOpen}
77
+ setOpen={(open) => setCreate(open ? 'category' : null)}
61
78
  />
62
79
  <Separator className="my-4" />
63
80
  <CategoryBreakdownChart wsId={wsId} currency={currency} />
@@ -73,7 +90,12 @@ export default function CategoriesTagsTabs({
73
90
  </TabsContent>
74
91
 
75
92
  <TabsContent value="tags">
76
- <TagManager wsId={wsId} currency={currency} />
93
+ <TagManager
94
+ wsId={wsId}
95
+ currency={currency}
96
+ openCreateDialog={tagCreateOpen}
97
+ onOpenCreateDialogChange={(open) => setCreate(open ? 'tag' : null)}
98
+ />
77
99
  </TabsContent>
78
100
  </Tabs>
79
101
  );
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildFinanceCommandActionGroups } from './finance-command-actions';
3
+
4
+ const translate = (key: string) => key;
5
+
6
+ describe('buildFinanceCommandActionGroups', () => {
7
+ it('exposes create and utility commands when permissions allow them', () => {
8
+ const groups = buildFinanceCommandActionGroups({
9
+ permissions: {
10
+ canCreateDebts: true,
11
+ canCreateInvoices: true,
12
+ canCreateRecurringTransactions: true,
13
+ canCreateTransactions: true,
14
+ canCreateWallets: true,
15
+ canExportFinanceData: true,
16
+ canManageFinance: true,
17
+ canUpdateWallets: true,
18
+ },
19
+ tCommand: translate,
20
+ tFinance: translate,
21
+ });
22
+
23
+ const ids = groups.flatMap((group) => group.items.map((item) => item.id));
24
+
25
+ expect(ids).toContain('new-transaction');
26
+ expect(ids).toContain('new-transfer');
27
+ expect(ids).toContain('import-transactions');
28
+ expect(ids).toContain('export-transactions');
29
+ expect(ids).toContain('all-wallet-check');
30
+ });
31
+
32
+ it('hides transaction and wallet commands without permission', () => {
33
+ const groups = buildFinanceCommandActionGroups({
34
+ permissions: {
35
+ canCreateTransactions: false,
36
+ canCreateWallets: false,
37
+ },
38
+ tCommand: translate,
39
+ tFinance: translate,
40
+ });
41
+
42
+ const ids = groups.flatMap((group) => group.items.map((item) => item.id));
43
+
44
+ expect(ids).not.toContain('new-transaction');
45
+ expect(ids).not.toContain('new-transfer');
46
+ expect(ids).not.toContain('new-wallet');
47
+ });
48
+ });
@@ -0,0 +1,200 @@
1
+ import {
2
+ ArrowLeftRight,
3
+ Calculator,
4
+ CreditCard,
5
+ DollarSign,
6
+ Download,
7
+ FileText,
8
+ History,
9
+ Repeat,
10
+ Tag,
11
+ Target,
12
+ TrendingDown,
13
+ TrendingUp,
14
+ Upload,
15
+ Wallet,
16
+ } from '@tuturuuu/icons';
17
+ import type { ComponentType, ReactNode } from 'react';
18
+
19
+ type IconComponent = ComponentType<{ className?: string }>;
20
+ type Translation = (key: any) => string;
21
+
22
+ export interface FinanceCommandActionPermissions {
23
+ canCreateDebts?: boolean;
24
+ canCreateInvoices?: boolean;
25
+ canCreateRecurringTransactions?: boolean;
26
+ canCreateTransactions?: boolean;
27
+ canCreateWallets?: boolean;
28
+ canExportFinanceData?: boolean;
29
+ canManageFinance?: boolean;
30
+ canUpdateWallets?: boolean;
31
+ }
32
+
33
+ export interface FinanceCommandAction {
34
+ description?: string;
35
+ href: string;
36
+ icon: IconComponent;
37
+ id: string;
38
+ title: string;
39
+ }
40
+
41
+ export interface FinanceCommandActionGroup {
42
+ heading: string;
43
+ id: 'create' | 'tools';
44
+ items: FinanceCommandAction[];
45
+ }
46
+
47
+ function iconNode(Icon: IconComponent): ReactNode {
48
+ return <Icon className="h-4 w-4" />;
49
+ }
50
+
51
+ export function renderFinanceCommandActionIcon(action: FinanceCommandAction) {
52
+ return iconNode(action.icon);
53
+ }
54
+
55
+ export function buildFinanceCommandActionGroups({
56
+ permissions,
57
+ tCommand,
58
+ tFinance,
59
+ }: {
60
+ permissions: FinanceCommandActionPermissions;
61
+ tCommand: Translation;
62
+ tFinance: Translation;
63
+ }): FinanceCommandActionGroup[] {
64
+ const canCreateTransactions = permissions.canCreateTransactions ?? true;
65
+ const canCreateWallets = permissions.canCreateWallets ?? true;
66
+ const canManageFinance = permissions.canManageFinance ?? true;
67
+ const canCreateInvoices = permissions.canCreateInvoices ?? true;
68
+ const canCreateDebts = permissions.canCreateDebts ?? true;
69
+ const canCreateRecurringTransactions =
70
+ permissions.canCreateRecurringTransactions ?? true;
71
+
72
+ const groups: FinanceCommandActionGroup[] = [
73
+ {
74
+ heading: tCommand('create_group'),
75
+ id: 'create',
76
+ items: [
77
+ canCreateTransactions && {
78
+ description: tCommand('new_transaction_description'),
79
+ href: '/transactions?create=transaction',
80
+ icon: DollarSign,
81
+ id: 'new-transaction',
82
+ title: tFinance('new_transaction'),
83
+ },
84
+ canCreateTransactions && {
85
+ description: tCommand('new_transfer_description'),
86
+ href: '/transactions?create=transfer',
87
+ icon: ArrowLeftRight,
88
+ id: 'new-transfer',
89
+ title: tFinance('new_transfer'),
90
+ },
91
+ canCreateRecurringTransactions && {
92
+ description: tCommand('new_recurring_transaction_description'),
93
+ href: '/recurring?create=recurring',
94
+ icon: Repeat,
95
+ id: 'new-recurring-transaction',
96
+ title: tFinance('new_recurring_transaction'),
97
+ },
98
+ canCreateWallets && {
99
+ description: tCommand('new_wallet_description'),
100
+ href: '/wallets?create=wallet',
101
+ icon: Wallet,
102
+ id: 'new-wallet',
103
+ title: tFinance('new_wallet'),
104
+ },
105
+ canCreateWallets && {
106
+ description: tCommand('new_credit_card_description'),
107
+ href: '/wallets?create=credit-card',
108
+ icon: CreditCard,
109
+ id: 'new-credit-card',
110
+ title: tFinance('new_credit_card'),
111
+ },
112
+ canManageFinance && {
113
+ description: tCommand('new_budget_description'),
114
+ href: '/budgets?create=budget',
115
+ icon: Target,
116
+ id: 'new-budget',
117
+ title: tFinance('new_budget'),
118
+ },
119
+ canCreateInvoices && {
120
+ description: tCommand('new_invoice_description'),
121
+ href: '/invoices/new',
122
+ icon: FileText,
123
+ id: 'new-invoice',
124
+ title: tFinance('new_invoice'),
125
+ },
126
+ canCreateDebts && {
127
+ description: tCommand('new_debt_description'),
128
+ href: '/debts?create=debt',
129
+ icon: TrendingDown,
130
+ id: 'new-debt',
131
+ title: tFinance('new_debt'),
132
+ },
133
+ canCreateDebts && {
134
+ description: tCommand('new_loan_description'),
135
+ href: '/debts?create=loan',
136
+ icon: TrendingUp,
137
+ id: 'new-loan',
138
+ title: tFinance('new_loan'),
139
+ },
140
+ canManageFinance && {
141
+ description: tCommand('new_category_description'),
142
+ href: '/categories?create=category',
143
+ icon: CreditCard,
144
+ id: 'new-category',
145
+ title: tCommand('new_category'),
146
+ },
147
+ canManageFinance && {
148
+ description: tCommand('new_tag_description'),
149
+ href: '/categories?tab=tags&create=tag',
150
+ icon: Tag,
151
+ id: 'new-tag',
152
+ title: tCommand('new_tag'),
153
+ },
154
+ ].filter(Boolean) as FinanceCommandAction[],
155
+ },
156
+ {
157
+ heading: tCommand('tools_group'),
158
+ id: 'tools',
159
+ items: [
160
+ canCreateTransactions && {
161
+ description: tCommand('import_transactions_description'),
162
+ href: '/transactions?tool=import',
163
+ icon: Download,
164
+ id: 'import-transactions',
165
+ title: tCommand('import_transactions'),
166
+ },
167
+ permissions.canExportFinanceData && {
168
+ description: tCommand('export_transactions_description'),
169
+ href: '/transactions?tool=export',
170
+ icon: Upload,
171
+ id: 'export-transactions',
172
+ title: tCommand('export_transactions'),
173
+ },
174
+ permissions.canUpdateWallets && {
175
+ description: tCommand('all_wallet_check_description'),
176
+ href: '/wallets?tool=all-wallet-check',
177
+ icon: Calculator,
178
+ id: 'all-wallet-check',
179
+ title: tCommand('all_wallet_check'),
180
+ },
181
+ canCreateTransactions && {
182
+ description: tCommand('checkpoint_history_description'),
183
+ href: '/wallets?tool=checkpoint-history',
184
+ icon: History,
185
+ id: 'checkpoint-history',
186
+ title: tCommand('checkpoint_history'),
187
+ },
188
+ canManageFinance && {
189
+ description: tCommand('manage_categories_description'),
190
+ href: '/categories',
191
+ icon: CreditCard,
192
+ id: 'manage-categories',
193
+ title: tFinance('manage_categories'),
194
+ },
195
+ ].filter(Boolean) as FinanceCommandAction[],
196
+ },
197
+ ];
198
+
199
+ return groups.filter((group) => group.items.length > 0);
200
+ }
@@ -0,0 +1,151 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { FinanceRouteProvider } from '../finance-route-context';
5
+ import { FinanceCommandProvider } from './finance-command-provider';
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ listBudgets: vi.fn(),
9
+ listDebtLoans: vi.fn(),
10
+ listFinanceInvoices: vi.fn(),
11
+ listInfiniteTransactionsWithInternalApi: vi.fn(),
12
+ listInfiniteWallets: vi.fn(),
13
+ listRecurringTransactions: vi.fn(),
14
+ listTransactionCategories: vi.fn(),
15
+ listTransactionTags: vi.fn(),
16
+ push: vi.fn(),
17
+ }));
18
+
19
+ vi.mock('next/navigation', () => ({
20
+ useRouter: () => ({
21
+ push: mocks.push,
22
+ }),
23
+ }));
24
+
25
+ vi.mock('next-intl', () => ({
26
+ useLocale: () => 'en',
27
+ useTranslations: () => (key: string) => {
28
+ const labels: Record<string, string> = {
29
+ create_group: 'Create',
30
+ description: 'Finance shortcuts',
31
+ empty: 'No commands',
32
+ finance: 'Finance',
33
+ new_transaction: 'New transaction',
34
+ new_transfer: 'New transfer',
35
+ quick_create_title: 'Quick create',
36
+ quick_placeholder: 'Create anything...',
37
+ recent_transactions: 'Recent transactions',
38
+ search_placeholder: 'Search finance...',
39
+ title: 'Finance command center',
40
+ };
41
+ return labels[key] ?? key;
42
+ },
43
+ }));
44
+
45
+ vi.mock('@tuturuuu/internal-api/finance', () => ({
46
+ listBudgets: (...args: Parameters<typeof mocks.listBudgets>) =>
47
+ mocks.listBudgets(...args),
48
+ listDebtLoans: (...args: Parameters<typeof mocks.listDebtLoans>) =>
49
+ mocks.listDebtLoans(...args),
50
+ listFinanceInvoices: (
51
+ ...args: Parameters<typeof mocks.listFinanceInvoices>
52
+ ) => mocks.listFinanceInvoices(...args),
53
+ listInfiniteWallets: (
54
+ ...args: Parameters<typeof mocks.listInfiniteWallets>
55
+ ) => mocks.listInfiniteWallets(...args),
56
+ listRecurringTransactions: (
57
+ ...args: Parameters<typeof mocks.listRecurringTransactions>
58
+ ) => mocks.listRecurringTransactions(...args),
59
+ listTransactionCategories: (
60
+ ...args: Parameters<typeof mocks.listTransactionCategories>
61
+ ) => mocks.listTransactionCategories(...args),
62
+ listTransactionTags: (
63
+ ...args: Parameters<typeof mocks.listTransactionTags>
64
+ ) => mocks.listTransactionTags(...args),
65
+ }));
66
+
67
+ vi.mock('../transactions/internal-api', () => ({
68
+ listInfiniteTransactionsWithInternalApi: (
69
+ ...args: Parameters<typeof mocks.listInfiniteTransactionsWithInternalApi>
70
+ ) => mocks.listInfiniteTransactionsWithInternalApi(...args),
71
+ }));
72
+
73
+ function renderProvider() {
74
+ const queryClient = new QueryClient({
75
+ defaultOptions: {
76
+ queries: {
77
+ retry: false,
78
+ },
79
+ },
80
+ });
81
+
82
+ return render(
83
+ <QueryClientProvider client={queryClient}>
84
+ <FinanceRouteProvider prefix="/finance">
85
+ <FinanceCommandProvider
86
+ wsId="ws-1"
87
+ workspaceSlug="ws-1"
88
+ canCreateTransactions
89
+ />
90
+ </FinanceRouteProvider>
91
+ </QueryClientProvider>
92
+ );
93
+ }
94
+
95
+ describe('FinanceCommandProvider', () => {
96
+ beforeEach(() => {
97
+ vi.clearAllMocks();
98
+ globalThis.ResizeObserver = class ResizeObserver {
99
+ disconnect() {}
100
+ observe() {}
101
+ unobserve() {}
102
+ };
103
+ Element.prototype.scrollIntoView = vi.fn();
104
+ mocks.listBudgets.mockResolvedValue([]);
105
+ mocks.listDebtLoans.mockResolvedValue([]);
106
+ mocks.listFinanceInvoices.mockResolvedValue({ data: [] });
107
+ mocks.listInfiniteTransactionsWithInternalApi.mockResolvedValue({
108
+ data: [],
109
+ });
110
+ mocks.listInfiniteWallets.mockResolvedValue({ data: [] });
111
+ mocks.listRecurringTransactions.mockResolvedValue([]);
112
+ mocks.listTransactionCategories.mockResolvedValue([]);
113
+ mocks.listTransactionTags.mockResolvedValue([]);
114
+ });
115
+
116
+ it('opens quick create with C and routes digit shortcuts', () => {
117
+ renderProvider();
118
+
119
+ fireEvent.keyDown(window, { key: 'c' });
120
+
121
+ expect(screen.getByText('New transaction')).toBeVisible();
122
+
123
+ fireEvent.keyDown(window, { key: '1' });
124
+
125
+ expect(mocks.push).toHaveBeenCalledWith(
126
+ '/ws-1/finance/transactions?create=transaction'
127
+ );
128
+ });
129
+
130
+ it('opens finance search with command-k and loads recent transactions', async () => {
131
+ mocks.listInfiniteTransactionsWithInternalApi.mockResolvedValue({
132
+ data: [
133
+ {
134
+ amount: -12,
135
+ description: 'Lunch',
136
+ id: 'tx-1',
137
+ taken_at: '2026-06-13T12:00:00.000Z',
138
+ wallet_currency: 'USD',
139
+ wallet_name: 'Cash',
140
+ },
141
+ ],
142
+ });
143
+
144
+ renderProvider();
145
+
146
+ fireEvent.keyDown(window, { key: 'k', metaKey: true });
147
+
148
+ expect(screen.getByPlaceholderText('Search finance...')).toBeVisible();
149
+ expect(await screen.findByText('Lunch')).toBeVisible();
150
+ });
151
+ });
@@ -0,0 +1,250 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import {
5
+ listBudgets,
6
+ listDebtLoans,
7
+ listFinanceInvoices,
8
+ listInfiniteWallets,
9
+ listRecurringTransactions,
10
+ listTransactionCategories,
11
+ listTransactionTags,
12
+ } from '@tuturuuu/internal-api/finance';
13
+ import type { QuickCommandCenterGroup } from '@tuturuuu/ui/quick-command-center';
14
+ import { QuickCommandCenter } from '@tuturuuu/ui/quick-command-center';
15
+ import { useRouter } from 'next/navigation';
16
+ import { useLocale, useTranslations } from 'next-intl';
17
+ import { useCallback, useEffect, useMemo, useState } from 'react';
18
+ import { useFinanceHref } from '../finance-route-context';
19
+ import { listInfiniteTransactionsWithInternalApi } from '../transactions/internal-api';
20
+ import {
21
+ buildFinanceCommandActionGroups,
22
+ type FinanceCommandActionPermissions,
23
+ renderFinanceCommandActionIcon,
24
+ } from './finance-command-actions';
25
+ import { buildFinanceRecentCommandGroups } from './finance-command-results';
26
+
27
+ interface FinanceCommandProviderProps extends FinanceCommandActionPermissions {
28
+ currency?: string;
29
+ workspaceSlug: string;
30
+ wsId: string;
31
+ }
32
+
33
+ type CommandMode = 'quick-create' | 'search';
34
+
35
+ function isEditableTarget(target: EventTarget | null) {
36
+ return (
37
+ target instanceof HTMLElement &&
38
+ !!target.closest(
39
+ 'input, textarea, select, [contenteditable="true"], [role="textbox"]'
40
+ )
41
+ );
42
+ }
43
+
44
+ export function FinanceCommandProvider({
45
+ currency = 'USD',
46
+ workspaceSlug,
47
+ wsId,
48
+ ...permissions
49
+ }: FinanceCommandProviderProps) {
50
+ const router = useRouter();
51
+ const locale = useLocale();
52
+ const tCommand = useTranslations('finance-command-center');
53
+ const tFinance = useTranslations('finance');
54
+ const financeHref = useFinanceHref();
55
+ const [open, setOpen] = useState(false);
56
+ const [mode, setMode] = useState<CommandMode>('quick-create');
57
+ const [search, setSearch] = useState('');
58
+ const trimmedSearch = search.trim();
59
+ const searchEnabled = open && mode === 'search';
60
+
61
+ const pushFinanceHref = useCallback(
62
+ (path: string) => {
63
+ setOpen(false);
64
+ setSearch('');
65
+ router.push(`/${workspaceSlug}${financeHref(path)}`);
66
+ },
67
+ [financeHref, router, workspaceSlug]
68
+ );
69
+
70
+ useEffect(() => {
71
+ const handleFinanceShortcut = (event: KeyboardEvent) => {
72
+ if (event.isComposing || isEditableTarget(event.target)) return;
73
+
74
+ if (
75
+ event.key.toLowerCase() === 'k' &&
76
+ (event.metaKey || event.ctrlKey) &&
77
+ !event.altKey
78
+ ) {
79
+ event.preventDefault();
80
+ event.stopPropagation();
81
+ event.stopImmediatePropagation();
82
+ setMode('search');
83
+ setOpen(true);
84
+ return;
85
+ }
86
+
87
+ if (
88
+ event.key.toLowerCase() === 'c' &&
89
+ !(event.metaKey || event.ctrlKey || event.altKey)
90
+ ) {
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ event.stopImmediatePropagation();
94
+ setMode('quick-create');
95
+ setOpen(true);
96
+ }
97
+ };
98
+
99
+ window.addEventListener('keydown', handleFinanceShortcut, {
100
+ capture: true,
101
+ });
102
+
103
+ return () =>
104
+ window.removeEventListener('keydown', handleFinanceShortcut, {
105
+ capture: true,
106
+ });
107
+ }, []);
108
+
109
+ const transactionsQuery = useQuery({
110
+ enabled: searchEnabled,
111
+ queryFn: () =>
112
+ listInfiniteTransactionsWithInternalApi(wsId, {
113
+ limit: 6,
114
+ q: trimmedSearch || undefined,
115
+ }),
116
+ queryKey: ['finance-command-center', wsId, 'transactions', trimmedSearch],
117
+ });
118
+ const walletsQuery = useQuery({
119
+ enabled: searchEnabled,
120
+ queryFn: () =>
121
+ listInfiniteWallets(wsId, {
122
+ limit: 6,
123
+ q: trimmedSearch || undefined,
124
+ }),
125
+ queryKey: ['finance-command-center', wsId, 'wallets', trimmedSearch],
126
+ });
127
+ const invoicesQuery = useQuery({
128
+ enabled: searchEnabled,
129
+ queryFn: () =>
130
+ listFinanceInvoices(wsId, {
131
+ page: 1,
132
+ pageSize: 6,
133
+ q: trimmedSearch || undefined,
134
+ }),
135
+ queryKey: ['finance-command-center', wsId, 'invoices', trimmedSearch],
136
+ });
137
+ const budgetsQuery = useQuery({
138
+ enabled: searchEnabled,
139
+ queryFn: () => listBudgets(wsId),
140
+ queryKey: ['finance-command-center', wsId, 'budgets'],
141
+ });
142
+ const debtsQuery = useQuery({
143
+ enabled: searchEnabled,
144
+ queryFn: () => listDebtLoans(wsId),
145
+ queryKey: ['finance-command-center', wsId, 'debts'],
146
+ });
147
+ const recurringQuery = useQuery({
148
+ enabled: searchEnabled,
149
+ queryFn: () => listRecurringTransactions(wsId),
150
+ queryKey: ['finance-command-center', wsId, 'recurring'],
151
+ });
152
+ const categoriesQuery = useQuery({
153
+ enabled: searchEnabled,
154
+ queryFn: () => listTransactionCategories(wsId),
155
+ queryKey: ['finance-command-center', wsId, 'categories'],
156
+ });
157
+ const tagsQuery = useQuery({
158
+ enabled: searchEnabled,
159
+ queryFn: () => listTransactionTags(wsId),
160
+ queryKey: ['finance-command-center', wsId, 'tags'],
161
+ });
162
+
163
+ const actionGroups = useMemo<QuickCommandCenterGroup[]>(
164
+ () =>
165
+ buildFinanceCommandActionGroups({
166
+ permissions,
167
+ tCommand,
168
+ tFinance,
169
+ }).map((group) => ({
170
+ heading: group.heading,
171
+ id: group.id,
172
+ items: group.items.map((action) => ({
173
+ description: action.description,
174
+ icon: renderFinanceCommandActionIcon(action),
175
+ id: action.id,
176
+ keywords: [action.href],
177
+ onSelect: () => pushFinanceHref(action.href),
178
+ title: action.title,
179
+ })),
180
+ })),
181
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `permissions` is the rest-spread of stable boolean props; its identity churns each render but the memo body is a cheap synchronous mapping.
182
+ [permissions, pushFinanceHref, tCommand, tFinance]
183
+ );
184
+
185
+ const recentGroups = useMemo<QuickCommandCenterGroup[]>(() => {
186
+ if (mode !== 'search') return [];
187
+
188
+ return buildFinanceRecentCommandGroups({
189
+ budgets: budgetsQuery.data,
190
+ categories: categoriesQuery.data,
191
+ currency,
192
+ debts: debtsQuery.data,
193
+ invoices: invoicesQuery.data?.data,
194
+ locale,
195
+ pushFinanceHref,
196
+ recurring: recurringQuery.data,
197
+ search: trimmedSearch,
198
+ tags: tagsQuery.data,
199
+ tCommand,
200
+ tFinance,
201
+ transactions: transactionsQuery.data?.data,
202
+ wallets: walletsQuery.data?.data,
203
+ });
204
+ }, [
205
+ budgetsQuery.data,
206
+ categoriesQuery.data,
207
+ currency,
208
+ debtsQuery.data,
209
+ invoicesQuery.data,
210
+ locale,
211
+ mode,
212
+ pushFinanceHref,
213
+ recurringQuery.data,
214
+ tagsQuery.data,
215
+ tCommand,
216
+ tFinance,
217
+ transactionsQuery.data,
218
+ trimmedSearch,
219
+ walletsQuery.data,
220
+ ]);
221
+
222
+ const groups =
223
+ mode === 'quick-create' ? actionGroups : [...actionGroups, ...recentGroups];
224
+
225
+ return (
226
+ <QuickCommandCenter
227
+ digitShortcuts={mode === 'quick-create'}
228
+ emptyLabel={tCommand('empty')}
229
+ groups={groups}
230
+ onOpenChange={(nextOpen) => {
231
+ setOpen(nextOpen);
232
+ if (!nextOpen) setSearch('');
233
+ }}
234
+ onSearchValueChange={setSearch}
235
+ open={open}
236
+ placeholder={
237
+ mode === 'quick-create'
238
+ ? tCommand('quick_placeholder')
239
+ : tCommand('search_placeholder')
240
+ }
241
+ searchValue={search}
242
+ title={
243
+ mode === 'quick-create'
244
+ ? tCommand('quick_create_title')
245
+ : tCommand('title')
246
+ }
247
+ description={tCommand('description')}
248
+ />
249
+ );
250
+ }