@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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -3
- package/biome.json +1 -1
- package/package.json +8 -8
- package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
- package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
- package/src/components/ui/calendar.test.tsx +24 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/date-time-picker.tsx +352 -234
- package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
- package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
- package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
- package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
- package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
- package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
- package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
- package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
- package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
- package/src/components/ui/finance/transactions/form-types.ts +3 -0
- package/src/components/ui/finance/transactions/form.test.tsx +105 -22
- package/src/components/ui/finance/transactions/form.tsx +116 -20
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
- package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
- package/src/components/ui/optional-time-picker.tsx +95 -0
- package/src/components/ui/quick-command-center.test.tsx +90 -0
- package/src/components/ui/quick-command-center.tsx +190 -0
- package/src/components/ui/storefront/cart-summary.tsx +18 -27
- package/src/components/ui/storefront/hero-panel.tsx +22 -13
- package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
- package/src/components/ui/storefront/storefront-surface.tsx +84 -41
- package/src/components/ui/storefront/types.ts +2 -0
- package/src/components/ui/storefront/utils.ts +21 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import {
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { CreateDialogFeatureSummary } from '../shared/create-dialog-feature-summary';
|
|
5
6
|
import { TransactionForm } from './form';
|
|
6
7
|
import { TransactionsCreateSummary } from './transactions-create-summary';
|
|
7
8
|
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
createTransaction: vi.fn(),
|
|
11
|
+
createTransfer: vi.fn(),
|
|
12
|
+
deleteWorkspaceStorageObjects: vi.fn(),
|
|
13
|
+
listTransactionCategories: vi.fn(),
|
|
14
|
+
listTransactionTagLinks: vi.fn(),
|
|
15
|
+
listTransactionTags: vi.fn(),
|
|
16
|
+
listWallets: vi.fn(),
|
|
17
|
+
listWorkspaceStorageObjects: vi.fn(),
|
|
18
|
+
preferences: {
|
|
19
|
+
lastSelections: {} as { categoryId?: string; walletId?: string },
|
|
20
|
+
rememberLastSelections: true,
|
|
21
|
+
},
|
|
22
|
+
saveLastSelections: vi.fn(),
|
|
23
|
+
updateTransaction: vi.fn(),
|
|
24
|
+
updateTransfer: vi.fn(),
|
|
25
|
+
uploadWorkspaceStorageFile: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
8
28
|
vi.mock('next/navigation', () => ({
|
|
9
29
|
useRouter: () => ({
|
|
10
30
|
refresh: vi.fn(),
|
|
@@ -27,9 +47,9 @@ vi.mock('@tuturuuu/ui/hooks/use-finance-transaction-preferences', () => ({
|
|
|
27
47
|
useFinanceTransactionPreferences: () => ({
|
|
28
48
|
isLastSelectionsInitialized: true,
|
|
29
49
|
isLoadingRememberLastSelections: false,
|
|
30
|
-
lastSelections:
|
|
31
|
-
rememberLastSelections:
|
|
32
|
-
saveLastSelections:
|
|
50
|
+
lastSelections: mocks.preferences.lastSelections,
|
|
51
|
+
rememberLastSelections: mocks.preferences.rememberLastSelections,
|
|
52
|
+
saveLastSelections: mocks.saveLastSelections,
|
|
33
53
|
}),
|
|
34
54
|
}));
|
|
35
55
|
|
|
@@ -38,21 +58,34 @@ vi.mock('@tuturuuu/ui/hooks/use-workspace-config', () => ({
|
|
|
38
58
|
}));
|
|
39
59
|
|
|
40
60
|
vi.mock('@tuturuuu/internal-api', () => ({
|
|
41
|
-
createTransaction:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
createTransaction: (...args: Parameters<typeof mocks.createTransaction>) =>
|
|
62
|
+
mocks.createTransaction(...args),
|
|
63
|
+
createTransfer: (...args: Parameters<typeof mocks.createTransfer>) =>
|
|
64
|
+
mocks.createTransfer(...args),
|
|
65
|
+
deleteWorkspaceStorageObjects: (
|
|
66
|
+
...args: Parameters<typeof mocks.deleteWorkspaceStorageObjects>
|
|
67
|
+
) => mocks.deleteWorkspaceStorageObjects(...args),
|
|
68
|
+
listTransactionCategories: (
|
|
69
|
+
...args: Parameters<typeof mocks.listTransactionCategories>
|
|
70
|
+
) => mocks.listTransactionCategories(...args),
|
|
71
|
+
listTransactionTagLinks: (
|
|
72
|
+
...args: Parameters<typeof mocks.listTransactionTagLinks>
|
|
73
|
+
) => mocks.listTransactionTagLinks(...args),
|
|
74
|
+
listTransactionTags: (
|
|
75
|
+
...args: Parameters<typeof mocks.listTransactionTags>
|
|
76
|
+
) => mocks.listTransactionTags(...args),
|
|
77
|
+
listWallets: (...args: Parameters<typeof mocks.listWallets>) =>
|
|
78
|
+
mocks.listWallets(...args),
|
|
79
|
+
listWorkspaceStorageObjects: (
|
|
80
|
+
...args: Parameters<typeof mocks.listWorkspaceStorageObjects>
|
|
81
|
+
) => mocks.listWorkspaceStorageObjects(...args),
|
|
82
|
+
updateTransaction: (...args: Parameters<typeof mocks.updateTransaction>) =>
|
|
83
|
+
mocks.updateTransaction(...args),
|
|
84
|
+
updateTransfer: (...args: Parameters<typeof mocks.updateTransfer>) =>
|
|
85
|
+
mocks.updateTransfer(...args),
|
|
86
|
+
uploadWorkspaceStorageFile: (
|
|
87
|
+
...args: Parameters<typeof mocks.uploadWorkspaceStorageFile>
|
|
88
|
+
) => mocks.uploadWorkspaceStorageFile(...args),
|
|
56
89
|
}));
|
|
57
90
|
|
|
58
91
|
vi.mock('@tuturuuu/internal-api/finance', () => ({
|
|
@@ -70,7 +103,9 @@ vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
|
70
103
|
},
|
|
71
104
|
}));
|
|
72
105
|
|
|
73
|
-
function renderTransactionForm(
|
|
106
|
+
function renderTransactionForm(
|
|
107
|
+
props: Partial<ComponentProps<typeof TransactionForm>> = {}
|
|
108
|
+
) {
|
|
74
109
|
const queryClient = new QueryClient({
|
|
75
110
|
defaultOptions: {
|
|
76
111
|
queries: {
|
|
@@ -81,18 +116,34 @@ function renderTransactionForm() {
|
|
|
81
116
|
|
|
82
117
|
return render(
|
|
83
118
|
<QueryClientProvider client={queryClient}>
|
|
84
|
-
<TransactionForm wsId="ws-1" canCreateTransactions />
|
|
119
|
+
<TransactionForm wsId="ws-1" canCreateTransactions {...props} />
|
|
85
120
|
</QueryClientProvider>
|
|
86
121
|
);
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
describe('TransactionForm', () => {
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
vi.useRealTimers();
|
|
127
|
+
});
|
|
128
|
+
|
|
90
129
|
beforeEach(() => {
|
|
130
|
+
vi.clearAllMocks();
|
|
91
131
|
globalThis.ResizeObserver = class ResizeObserver {
|
|
92
132
|
disconnect() {}
|
|
93
133
|
observe() {}
|
|
94
134
|
unobserve() {}
|
|
95
135
|
};
|
|
136
|
+
mocks.preferences.lastSelections = {};
|
|
137
|
+
mocks.preferences.rememberLastSelections = true;
|
|
138
|
+
mocks.listTransactionCategories.mockResolvedValue([]);
|
|
139
|
+
mocks.listTransactionTagLinks.mockResolvedValue([]);
|
|
140
|
+
mocks.listTransactionTags.mockResolvedValue([]);
|
|
141
|
+
mocks.listWallets.mockResolvedValue([]);
|
|
142
|
+
mocks.listWorkspaceStorageObjects.mockResolvedValue({
|
|
143
|
+
data: [],
|
|
144
|
+
hasMore: false,
|
|
145
|
+
total: 0,
|
|
146
|
+
});
|
|
96
147
|
});
|
|
97
148
|
|
|
98
149
|
it('renders the create transaction form without invalid component errors', () => {
|
|
@@ -102,6 +153,38 @@ describe('TransactionForm', () => {
|
|
|
102
153
|
expect(screen.getByText('ws-transactions.create')).toBeVisible();
|
|
103
154
|
});
|
|
104
155
|
|
|
156
|
+
it('renders the optional time control on by default for new transactions', () => {
|
|
157
|
+
vi.useFakeTimers();
|
|
158
|
+
vi.setSystemTime(new Date('2026-06-13T12:34:00.000Z'));
|
|
159
|
+
|
|
160
|
+
renderTransactionForm({ timezone: 'UTC' });
|
|
161
|
+
|
|
162
|
+
const includeTimeSwitch = screen.getByRole('switch', {
|
|
163
|
+
name: 'transaction-data-table.include_time',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(includeTimeSwitch).toHaveAttribute('data-state', 'checked');
|
|
167
|
+
expect(screen.getByText('12:34 PM')).toBeVisible();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps the contextual wallet selected when wallet context is preferred', async () => {
|
|
171
|
+
mocks.preferences.lastSelections = { walletId: 'wallet-remembered' };
|
|
172
|
+
mocks.listWallets.mockResolvedValue([
|
|
173
|
+
{ id: 'wallet-remembered', name: 'Remembered Wallet' },
|
|
174
|
+
{ id: 'wallet-current', name: 'Current Wallet' },
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
renderTransactionForm({
|
|
178
|
+
initialTransaction: { origin_wallet_id: 'wallet-current' },
|
|
179
|
+
preferInitialWalletSelection: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(screen.getByText('Current Wallet')).toBeVisible();
|
|
184
|
+
});
|
|
185
|
+
expect(screen.queryByText('Remembered Wallet')).not.toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
105
188
|
it('renders inside the create dialog summary when opened from query state', () => {
|
|
106
189
|
const queryClient = new QueryClient({
|
|
107
190
|
defaultOptions: {
|
|
@@ -39,6 +39,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
|
|
|
39
39
|
import { convertCurrency } from '@tuturuuu/utils/exchange-rates';
|
|
40
40
|
import { shouldLockFinanceWalletSelectionOnCreate } from '@tuturuuu/utils/finance';
|
|
41
41
|
import { joinPath } from '@tuturuuu/utils/path-helper';
|
|
42
|
+
import {
|
|
43
|
+
buildDateInTimezone,
|
|
44
|
+
getDatePartsInTimezone,
|
|
45
|
+
} from '@tuturuuu/utils/task-date-timezone';
|
|
42
46
|
import { useRouter } from 'next/navigation';
|
|
43
47
|
import { useLocale, useTranslations } from 'next-intl';
|
|
44
48
|
import { useEffect, useMemo, useState } from 'react';
|
|
@@ -62,6 +66,67 @@ import {
|
|
|
62
66
|
} from './transaction-attachments-field';
|
|
63
67
|
|
|
64
68
|
const TRANSACTION_ATTACHMENT_PAGE_SIZE = 100;
|
|
69
|
+
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
70
|
+
|
|
71
|
+
function startOfDayInTimezone(date: Date, timezone?: string | null) {
|
|
72
|
+
const resolvedTimezone = timezone || 'auto';
|
|
73
|
+
const parts = getDatePartsInTimezone(date, resolvedTimezone);
|
|
74
|
+
|
|
75
|
+
return buildDateInTimezone(
|
|
76
|
+
parts.year,
|
|
77
|
+
parts.month,
|
|
78
|
+
parts.day,
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
resolvedTimezone
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseDateOnlyInTimezone(value: string, timezone?: string | null) {
|
|
86
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
87
|
+
|
|
88
|
+
if (!year || !month || !day)
|
|
89
|
+
return startOfDayInTimezone(new Date(), timezone);
|
|
90
|
+
|
|
91
|
+
return buildDateInTimezone(year, month, day, 0, 0, timezone || 'auto');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveInitialTakenAt({
|
|
95
|
+
data,
|
|
96
|
+
initialIsTransfer,
|
|
97
|
+
initialTransaction,
|
|
98
|
+
initialTransfer,
|
|
99
|
+
timezone,
|
|
100
|
+
}: Pick<
|
|
101
|
+
TransactionFormProps,
|
|
102
|
+
'data' | 'initialTransaction' | 'initialTransfer' | 'timezone'
|
|
103
|
+
> & {
|
|
104
|
+
initialIsTransfer: boolean;
|
|
105
|
+
}) {
|
|
106
|
+
const existingTakenAt = data?.taken_at;
|
|
107
|
+
|
|
108
|
+
if (existingTakenAt) {
|
|
109
|
+
const isDateOnly =
|
|
110
|
+
typeof existingTakenAt === 'string' &&
|
|
111
|
+
DATE_ONLY_PATTERN.test(existingTakenAt);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
date: isDateOnly
|
|
115
|
+
? parseDateOnlyInTimezone(existingTakenAt, timezone)
|
|
116
|
+
: new Date(existingTakenAt),
|
|
117
|
+
includeTime: !!data?.id,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const initialTakenAt = initialIsTransfer
|
|
122
|
+
? initialTransfer?.taken_at
|
|
123
|
+
: initialTransaction?.taken_at;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
date: initialTakenAt ?? new Date(),
|
|
127
|
+
includeTime: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
65
130
|
|
|
66
131
|
export function TransactionForm({
|
|
67
132
|
wsId,
|
|
@@ -76,6 +141,9 @@ export function TransactionForm({
|
|
|
76
141
|
initialMode = 'transaction',
|
|
77
142
|
initialTransaction,
|
|
78
143
|
initialTransfer,
|
|
144
|
+
timezone,
|
|
145
|
+
preferInitialWalletSelection = false,
|
|
146
|
+
refreshPageOnFinish = false,
|
|
79
147
|
permissionRequestUser,
|
|
80
148
|
}: TransactionFormProps) {
|
|
81
149
|
const t = useTranslations();
|
|
@@ -87,6 +155,20 @@ export function TransactionForm({
|
|
|
87
155
|
[]
|
|
88
156
|
);
|
|
89
157
|
const initialIsTransfer = initialMode === 'transfer' || !!data?.transfer;
|
|
158
|
+
const initialTakenAt = useMemo(
|
|
159
|
+
() =>
|
|
160
|
+
resolveInitialTakenAt({
|
|
161
|
+
data,
|
|
162
|
+
initialIsTransfer,
|
|
163
|
+
initialTransaction,
|
|
164
|
+
initialTransfer,
|
|
165
|
+
timezone,
|
|
166
|
+
}),
|
|
167
|
+
[data, initialIsTransfer, initialTransaction, initialTransfer, timezone]
|
|
168
|
+
);
|
|
169
|
+
const [includeTakenAtTime, setIncludeTakenAtTime] = useState(
|
|
170
|
+
initialTakenAt.includeTime
|
|
171
|
+
);
|
|
90
172
|
const [isTransfer, setIsTransfer] = useState(initialIsTransfer);
|
|
91
173
|
// Start in override mode when editing an existing transfer (preserve stored amounts).
|
|
92
174
|
// Start in auto mode for new transfers so the exchange rate pre-fills destination.
|
|
@@ -173,11 +255,7 @@ export function TransactionForm({
|
|
|
173
255
|
? Math.abs(data.transfer.linked_amount)
|
|
174
256
|
: initialTransfer?.destination_amount,
|
|
175
257
|
category_id: data?.category_id || initialTransaction?.category_id || '',
|
|
176
|
-
taken_at:
|
|
177
|
-
? new Date(data.taken_at)
|
|
178
|
-
: initialIsTransfer
|
|
179
|
-
? (initialTransfer?.taken_at ?? new Date())
|
|
180
|
-
: (initialTransaction?.taken_at ?? new Date()),
|
|
258
|
+
taken_at: initialTakenAt.date,
|
|
181
259
|
report_opt_in: data?.report_opt_in ?? true,
|
|
182
260
|
tag_ids: [] as string[],
|
|
183
261
|
is_transfer: initialIsTransfer,
|
|
@@ -190,6 +268,10 @@ export function TransactionForm({
|
|
|
190
268
|
},
|
|
191
269
|
});
|
|
192
270
|
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
setIncludeTakenAtTime(initialTakenAt.includeTime);
|
|
273
|
+
}, [initialTakenAt.includeTime]);
|
|
274
|
+
|
|
193
275
|
// Keep is_transfer in sync with local state
|
|
194
276
|
useEffect(() => {
|
|
195
277
|
if (form.getValues('is_transfer') !== isTransfer) {
|
|
@@ -232,25 +314,35 @@ export function TransactionForm({
|
|
|
232
314
|
wallets.some((wallet) => wallet.id === lastSelections.walletId)
|
|
233
315
|
? lastSelections.walletId
|
|
234
316
|
: '';
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
(contextualWalletId &&
|
|
317
|
+
const contextualWalletSelection =
|
|
318
|
+
contextualWalletId &&
|
|
238
319
|
wallets.some((wallet) => wallet.id === contextualWalletId)
|
|
239
320
|
? contextualWalletId
|
|
240
|
-
: ''
|
|
241
|
-
|
|
242
|
-
wallets.some((wallet) => wallet.id === defaultWalletId)
|
|
321
|
+
: '';
|
|
322
|
+
const defaultWalletSelection =
|
|
323
|
+
defaultWalletId && wallets.some((wallet) => wallet.id === defaultWalletId)
|
|
243
324
|
? defaultWalletId
|
|
244
|
-
: ''
|
|
325
|
+
: '';
|
|
326
|
+
const nextWalletSelection =
|
|
327
|
+
(preferInitialWalletSelection
|
|
328
|
+
? contextualWalletSelection ||
|
|
329
|
+
rememberedWalletId ||
|
|
330
|
+
defaultWalletSelection
|
|
331
|
+
: rememberedWalletId ||
|
|
332
|
+
contextualWalletSelection ||
|
|
333
|
+
defaultWalletSelection) ||
|
|
245
334
|
wallets[0]?.id ||
|
|
246
335
|
'';
|
|
247
|
-
const sourceLabel =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
? t('transaction-data-table.
|
|
253
|
-
:
|
|
336
|
+
const sourceLabel =
|
|
337
|
+
rememberedWalletId && nextWalletSelection === rememberedWalletId
|
|
338
|
+
? t('transaction-data-table.prefill_source_last_used')
|
|
339
|
+
: contextualWalletSelection &&
|
|
340
|
+
nextWalletSelection === contextualWalletSelection
|
|
341
|
+
? t('transaction-data-table.prefill_source_current_context')
|
|
342
|
+
: defaultWalletSelection &&
|
|
343
|
+
nextWalletSelection === defaultWalletSelection
|
|
344
|
+
? t('transaction-data-table.prefill_source_workspace_default')
|
|
345
|
+
: '';
|
|
254
346
|
|
|
255
347
|
if (!nextWalletSelection) return;
|
|
256
348
|
|
|
@@ -277,6 +369,7 @@ export function TransactionForm({
|
|
|
277
369
|
isLastSelectionsInitialized,
|
|
278
370
|
isLoadingRememberLastSelections,
|
|
279
371
|
lastSelections.walletId,
|
|
372
|
+
preferInitialWalletSelection,
|
|
280
373
|
rememberLastSelections,
|
|
281
374
|
t,
|
|
282
375
|
wallets,
|
|
@@ -430,7 +523,7 @@ export function TransactionForm({
|
|
|
430
523
|
const refreshTransactions = async () => {
|
|
431
524
|
await invalidateTransactionMutationQueries(queryClient, wsId);
|
|
432
525
|
|
|
433
|
-
if (!onFinish) {
|
|
526
|
+
if (refreshPageOnFinish || !onFinish) {
|
|
434
527
|
router.refresh();
|
|
435
528
|
}
|
|
436
529
|
};
|
|
@@ -981,6 +1074,9 @@ export function TransactionForm({
|
|
|
981
1074
|
suggestedExchangeRate={suggestedExchangeRate}
|
|
982
1075
|
isDestinationOverridden={isDestinationOverridden}
|
|
983
1076
|
setIsDestinationOverridden={setIsDestinationOverridden}
|
|
1077
|
+
includeTakenAtTime={includeTakenAtTime}
|
|
1078
|
+
setIncludeTakenAtTime={setIncludeTakenAtTime}
|
|
1079
|
+
timezone={timezone}
|
|
984
1080
|
setNewContentType={setNewContentType}
|
|
985
1081
|
setNewContent={setNewContent}
|
|
986
1082
|
walletPrefillMeta={walletPrefillMeta}
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
type FinancePermissionRequestUser,
|
|
56
56
|
FinancePermissionWarningDialog,
|
|
57
57
|
} from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
|
|
58
|
+
import { useRouter } from 'next/navigation';
|
|
58
59
|
import { useLocale, useTranslations } from 'next-intl';
|
|
59
60
|
import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs';
|
|
60
61
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -71,6 +72,7 @@ import {
|
|
|
71
72
|
listTransactionPeriodsWithInternalApi,
|
|
72
73
|
} from './internal-api';
|
|
73
74
|
import { PeriodBreakdownPanel } from './period-charts';
|
|
75
|
+
import { invalidateTransactionMutationQueries } from './query-invalidation';
|
|
74
76
|
import { TransactionCard } from './transaction-card';
|
|
75
77
|
import { TransactionStatistics } from './transaction-statistics';
|
|
76
78
|
import { mergeLinkedTransferTransactions } from './transfer-merge';
|
|
@@ -144,6 +146,7 @@ export function InfiniteTransactionsList({
|
|
|
144
146
|
const t = useTranslations();
|
|
145
147
|
const locale = useLocale();
|
|
146
148
|
const queryClient = useQueryClient();
|
|
149
|
+
const router = useRouter();
|
|
147
150
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
148
151
|
const { data: exchangeRatesData } = useExchangeRates();
|
|
149
152
|
const exchangeRates = exchangeRatesData?.data ?? [];
|
|
@@ -181,12 +184,11 @@ export function InfiniteTransactionsList({
|
|
|
181
184
|
};
|
|
182
185
|
|
|
183
186
|
const handleTransactionUpdate = () => {
|
|
184
|
-
queryClient
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
187
|
+
void invalidateTransactionMutationQueries(queryClient, wsId);
|
|
188
|
+
|
|
189
|
+
if (walletId) {
|
|
190
|
+
router.refresh();
|
|
191
|
+
}
|
|
190
192
|
};
|
|
191
193
|
|
|
192
194
|
const deleteMutation = useMutation({
|
|
@@ -1065,6 +1067,8 @@ export function InfiniteTransactionsList({
|
|
|
1065
1067
|
canUpdateConfidentialTransactions={
|
|
1066
1068
|
canUpdateConfidentialTransactions
|
|
1067
1069
|
}
|
|
1070
|
+
timezone={resolvedTimezone}
|
|
1071
|
+
refreshPageOnFinish={!!walletId}
|
|
1068
1072
|
permissionRequestUser={permissionRequestUser}
|
|
1069
1073
|
onFinish={() => {
|
|
1070
1074
|
handleTransactionUpdate();
|
|
@@ -1098,6 +1102,9 @@ export function InfiniteTransactionsList({
|
|
|
1098
1102
|
canCreateConfidentialTransactions={
|
|
1099
1103
|
canCreateConfidentialTransactions
|
|
1100
1104
|
}
|
|
1105
|
+
timezone={resolvedTimezone}
|
|
1106
|
+
preferInitialWalletSelection={!!walletId}
|
|
1107
|
+
refreshPageOnFinish={!!walletId}
|
|
1101
1108
|
permissionRequestUser={permissionRequestUser}
|
|
1102
1109
|
onFinish={() => {
|
|
1103
1110
|
handleTransactionUpdate();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { TransactionEditDialog } from './transaction-edit-dialog';
|
|
5
6
|
|
|
@@ -40,7 +41,9 @@ vi.mock('next/image', () => ({
|
|
|
40
41
|
default: () => null,
|
|
41
42
|
}));
|
|
42
43
|
|
|
43
|
-
function renderDialog(
|
|
44
|
+
function renderDialog(
|
|
45
|
+
props: Partial<ComponentProps<typeof TransactionEditDialog>> = {}
|
|
46
|
+
) {
|
|
44
47
|
const queryClient = new QueryClient({
|
|
45
48
|
defaultOptions: {
|
|
46
49
|
queries: {
|
|
@@ -68,6 +71,7 @@ function renderDialog() {
|
|
|
68
71
|
} as never
|
|
69
72
|
}
|
|
70
73
|
wsId="ws-1"
|
|
74
|
+
{...props}
|
|
71
75
|
/>
|
|
72
76
|
</QueryClientProvider>
|
|
73
77
|
);
|
|
@@ -98,4 +102,24 @@ describe('TransactionEditDialog', () => {
|
|
|
98
102
|
expect(await screen.findByText('•••••')).toBeVisible();
|
|
99
103
|
expect(screen.queryByText('+$123')).not.toBeInTheDocument();
|
|
100
104
|
});
|
|
105
|
+
|
|
106
|
+
it('enables the optional time control for existing transactions', async () => {
|
|
107
|
+
renderDialog();
|
|
108
|
+
|
|
109
|
+
expect(
|
|
110
|
+
await screen.findByRole('switch', {
|
|
111
|
+
name: 'transaction-data-table.include_time',
|
|
112
|
+
})
|
|
113
|
+
).toHaveAttribute('data-state', 'checked');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('disables the optional time control when update access is missing', async () => {
|
|
117
|
+
renderDialog({ canUpdateTransactions: false });
|
|
118
|
+
|
|
119
|
+
expect(
|
|
120
|
+
await screen.findByRole('switch', {
|
|
121
|
+
name: 'transaction-data-table.include_time',
|
|
122
|
+
})
|
|
123
|
+
).toBeDisabled();
|
|
124
|
+
});
|
|
101
125
|
});
|
|
@@ -50,7 +50,6 @@ import {
|
|
|
50
50
|
getIconComponentByKey,
|
|
51
51
|
type PlatformIconKey,
|
|
52
52
|
} from '@tuturuuu/ui/custom/icon-picker';
|
|
53
|
-
import { DateTimePicker } from '@tuturuuu/ui/date-time-picker';
|
|
54
53
|
import {
|
|
55
54
|
Dialog,
|
|
56
55
|
DialogContent,
|
|
@@ -58,6 +57,7 @@ import {
|
|
|
58
57
|
DialogTitle,
|
|
59
58
|
} from '@tuturuuu/ui/dialog';
|
|
60
59
|
import { Label } from '@tuturuuu/ui/label';
|
|
60
|
+
import { OptionalTimePicker } from '@tuturuuu/ui/optional-time-picker';
|
|
61
61
|
import { Separator } from '@tuturuuu/ui/separator';
|
|
62
62
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
63
63
|
import { Switch } from '@tuturuuu/ui/switch';
|
|
@@ -148,6 +148,7 @@ interface TransactionEditDialogProps {
|
|
|
148
148
|
canViewConfidentialAmount?: boolean;
|
|
149
149
|
canViewConfidentialDescription?: boolean;
|
|
150
150
|
canViewConfidentialCategory?: boolean;
|
|
151
|
+
timezone?: string | null;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
export function TransactionEditDialog({
|
|
@@ -164,6 +165,7 @@ export function TransactionEditDialog({
|
|
|
164
165
|
canViewConfidentialAmount,
|
|
165
166
|
canViewConfidentialDescription,
|
|
166
167
|
canViewConfidentialCategory,
|
|
168
|
+
timezone,
|
|
167
169
|
}: TransactionEditDialogProps) {
|
|
168
170
|
const t = useTranslations();
|
|
169
171
|
const locale = useLocale();
|
|
@@ -212,6 +214,9 @@ export function TransactionEditDialog({
|
|
|
212
214
|
const [takenAt, setTakenAt] = useState<Date | undefined>(
|
|
213
215
|
transaction?.taken_at ? new Date(transaction.taken_at) : new Date()
|
|
214
216
|
);
|
|
217
|
+
const [includeTakenAtTime, setIncludeTakenAtTime] = useState(
|
|
218
|
+
!!transaction?.taken_at
|
|
219
|
+
);
|
|
215
220
|
const [reportOptIn, setReportOptIn] = useState(
|
|
216
221
|
transaction?.report_opt_in ?? true
|
|
217
222
|
);
|
|
@@ -292,6 +297,7 @@ export function TransactionEditDialog({
|
|
|
292
297
|
setTakenAt(
|
|
293
298
|
transaction.taken_at ? new Date(transaction.taken_at) : new Date()
|
|
294
299
|
);
|
|
300
|
+
setIncludeTakenAtTime(!!transaction.taken_at);
|
|
295
301
|
setReportOptIn(transaction.report_opt_in ?? true);
|
|
296
302
|
setIsAmountConfidential(
|
|
297
303
|
(transaction as any)?.is_amount_confidential ?? false
|
|
@@ -309,6 +315,7 @@ export function TransactionEditDialog({
|
|
|
309
315
|
setWalletId('');
|
|
310
316
|
setCategoryId('');
|
|
311
317
|
setTakenAt(new Date());
|
|
318
|
+
setIncludeTakenAtTime(true);
|
|
312
319
|
setReportOptIn(true);
|
|
313
320
|
setIsAmountConfidential(false);
|
|
314
321
|
setIsDescriptionConfidential(false);
|
|
@@ -748,13 +755,19 @@ export function TransactionEditDialog({
|
|
|
748
755
|
</div>
|
|
749
756
|
{t('transaction-data-table.taken_at')}
|
|
750
757
|
</Label>
|
|
751
|
-
<
|
|
758
|
+
<OptionalTimePicker
|
|
752
759
|
date={takenAt}
|
|
753
760
|
setDate={setTakenAt}
|
|
754
|
-
|
|
761
|
+
includeTime={includeTakenAtTime}
|
|
762
|
+
setIncludeTime={setIncludeTakenAtTime}
|
|
763
|
+
includeTimeLabel={t('transaction-data-table.include_time')}
|
|
755
764
|
allowClear={false}
|
|
756
765
|
showFooterControls={true}
|
|
757
766
|
disabled={isDisabled || !canUpdateTransactions}
|
|
767
|
+
preferences={{
|
|
768
|
+
timezone: timezone || 'auto',
|
|
769
|
+
timeFormat: locale === 'vi' ? '24h' : '12h',
|
|
770
|
+
}}
|
|
758
771
|
/>
|
|
759
772
|
</div>
|
|
760
773
|
|
package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx
CHANGED
|
@@ -34,6 +34,7 @@ interface Props {
|
|
|
34
34
|
currency?: string;
|
|
35
35
|
transaction: any;
|
|
36
36
|
tags: Array<{ id: string; name: string; color: string }>;
|
|
37
|
+
timezone?: string | null;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function TransactionDetailsClientPage({
|
|
@@ -41,6 +42,7 @@ export function TransactionDetailsClientPage({
|
|
|
41
42
|
currency = 'USD',
|
|
42
43
|
transaction,
|
|
43
44
|
tags,
|
|
45
|
+
timezone,
|
|
44
46
|
}: Props) {
|
|
45
47
|
const t = useTranslations();
|
|
46
48
|
const financeHref = useFinanceHref();
|
|
@@ -289,6 +291,7 @@ export function TransactionDetailsClientPage({
|
|
|
289
291
|
wsId={wsId}
|
|
290
292
|
isOpen={isEditOpen}
|
|
291
293
|
onClose={() => setIsEditOpen(false)}
|
|
294
|
+
timezone={timezone}
|
|
292
295
|
/>
|
|
293
296
|
</>
|
|
294
297
|
);
|
|
@@ -15,6 +15,7 @@ interface Props {
|
|
|
15
15
|
locale: string;
|
|
16
16
|
}>;
|
|
17
17
|
internalApiOptions?: InternalApiClientOptions;
|
|
18
|
+
timezone?: string | null;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
type TransactionDetailsData = {
|
|
@@ -26,6 +27,7 @@ type TransactionDetailsData = {
|
|
|
26
27
|
export default async function TransactionDetailsPage({
|
|
27
28
|
params,
|
|
28
29
|
internalApiOptions,
|
|
30
|
+
timezone,
|
|
29
31
|
}: Props) {
|
|
30
32
|
const { wsId, transactionId } = await params;
|
|
31
33
|
|
|
@@ -55,6 +57,7 @@ export default async function TransactionDetailsPage({
|
|
|
55
57
|
wsId={wsId}
|
|
56
58
|
transaction={transaction}
|
|
57
59
|
tags={tags}
|
|
60
|
+
timezone={timezone}
|
|
58
61
|
/>
|
|
59
62
|
</div>
|
|
60
63
|
);
|
|
@@ -16,9 +16,11 @@ interface TransactionsCreateSummaryProps {
|
|
|
16
16
|
createTitle: string;
|
|
17
17
|
defaultOpen?: boolean;
|
|
18
18
|
description: string;
|
|
19
|
+
initialMode?: 'transaction' | 'transfer';
|
|
19
20
|
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
20
21
|
pluralTitle: string;
|
|
21
22
|
singularTitle: string;
|
|
23
|
+
timezone?: string | null;
|
|
22
24
|
wsId: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -31,9 +33,11 @@ export function TransactionsCreateSummary({
|
|
|
31
33
|
createTitle,
|
|
32
34
|
defaultOpen = false,
|
|
33
35
|
description,
|
|
36
|
+
initialMode = 'transaction',
|
|
34
37
|
permissionRequestUser,
|
|
35
38
|
pluralTitle,
|
|
36
39
|
singularTitle,
|
|
40
|
+
timezone,
|
|
37
41
|
wsId,
|
|
38
42
|
}: TransactionsCreateSummaryProps) {
|
|
39
43
|
return (
|
|
@@ -54,6 +58,8 @@ export function TransactionsCreateSummary({
|
|
|
54
58
|
canCreateConfidentialTransactions={
|
|
55
59
|
canCreateConfidentialTransactions
|
|
56
60
|
}
|
|
61
|
+
initialMode={initialMode}
|
|
62
|
+
timezone={timezone}
|
|
57
63
|
permissionRequestUser={permissionRequestUser}
|
|
58
64
|
/>
|
|
59
65
|
) : (
|