@tuturuuu/ui 0.6.2 → 0.8.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 +66 -0
- package/biome.json +1 -1
- package/package.json +11 -11
- 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/currency-input.test.tsx +43 -0
- package/src/components/ui/currency-input.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
- 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 +5 -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-card.tsx +21 -9
- 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/money-input.test.tsx +64 -0
- package/src/components/ui/money-input.tsx +63 -0
- 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 +126 -50
- package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
- package/src/components/ui/storefront/hero-panel.tsx +23 -20
- package/src/components/ui/storefront/image-panel.tsx +6 -0
- package/src/components/ui/storefront/index.ts +11 -0
- package/src/components/ui/storefront/listing-card.tsx +84 -22
- package/src/components/ui/storefront/product-detail.tsx +289 -0
- package/src/components/ui/storefront/product-dialog.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
- package/src/components/ui/storefront/storefront-surface.tsx +371 -128
- package/src/components/ui/storefront/types.ts +25 -1
- package/src/components/ui/storefront/utils.ts +118 -13
- package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
- package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
- package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
- package/src/components/ui/text-editor/content-migration.ts +41 -18
- package/src/components/ui/text-editor/extensions.ts +1 -1
- package/src/components/ui/text-editor/image-extension.ts +40 -18
- package/src/components/ui/text-editor/video-extension.ts +11 -2
- package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
- package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
- package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
- package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
- 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
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
- package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
- package/src/hooks/useBoardRealtime.ts +6 -3
- package/src/hooks/useBoardRealtime.types.ts +11 -0
- package/src/hooks/useCursorTracking.ts +91 -27
- package/src/hooks/useTaskUserRealtime.ts +5 -3
|
@@ -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
|
) : (
|
|
@@ -82,6 +82,14 @@ export function TransactionsInfinitePage({
|
|
|
82
82
|
shallow: true,
|
|
83
83
|
})
|
|
84
84
|
);
|
|
85
|
+
const [tool, setTool] = useQueryState(
|
|
86
|
+
'tool',
|
|
87
|
+
parseAsString.withDefault('').withOptions({
|
|
88
|
+
shallow: true,
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
const importOpen = tool === 'import';
|
|
92
|
+
const exportOpen = tool === 'export';
|
|
85
93
|
|
|
86
94
|
const handleSearch = async (query: string) => {
|
|
87
95
|
await setQ(query || '');
|
|
@@ -143,7 +151,12 @@ export function TransactionsInfinitePage({
|
|
|
143
151
|
|
|
144
152
|
<div className="flex w-full flex-col gap-2 sm:flex-row md:w-auto">
|
|
145
153
|
{/* Import button */}
|
|
146
|
-
<Dialog
|
|
154
|
+
<Dialog
|
|
155
|
+
open={importOpen}
|
|
156
|
+
onOpenChange={(nextOpen) =>
|
|
157
|
+
setTool(nextOpen ? 'import' : tool === 'import' ? null : tool)
|
|
158
|
+
}
|
|
159
|
+
>
|
|
147
160
|
<DialogTrigger asChild>
|
|
148
161
|
<Button
|
|
149
162
|
variant="outline"
|
|
@@ -161,7 +174,12 @@ export function TransactionsInfinitePage({
|
|
|
161
174
|
|
|
162
175
|
{/* Export button */}
|
|
163
176
|
{canExport && exportContent && (
|
|
164
|
-
<Dialog
|
|
177
|
+
<Dialog
|
|
178
|
+
open={exportOpen}
|
|
179
|
+
onOpenChange={(nextOpen) =>
|
|
180
|
+
setTool(nextOpen ? 'export' : tool === 'export' ? null : tool)
|
|
181
|
+
}
|
|
182
|
+
>
|
|
165
183
|
<DialogTrigger asChild>
|
|
166
184
|
<Button
|
|
167
185
|
variant="outline"
|
|
@@ -26,6 +26,7 @@ interface Props {
|
|
|
26
26
|
};
|
|
27
27
|
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
28
28
|
openCreateDialog?: boolean;
|
|
29
|
+
initialCreateMode?: 'transaction' | 'transfer';
|
|
29
30
|
showTransactionTypeFilter?: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -36,6 +37,7 @@ export default async function TransactionsPage({
|
|
|
36
37
|
workspace,
|
|
37
38
|
permissionRequestUser,
|
|
38
39
|
openCreateDialog = false,
|
|
40
|
+
initialCreateMode = 'transaction',
|
|
39
41
|
showTransactionTypeFilter = false,
|
|
40
42
|
}: Props) {
|
|
41
43
|
const [t, resolvedWorkspace, resolvedPermissions, resolvedCurrency] =
|
|
@@ -102,11 +104,13 @@ export default async function TransactionsPage({
|
|
|
102
104
|
createTitle={t('ws-transactions.create')}
|
|
103
105
|
createDescription={t('ws-transactions.create_description')}
|
|
104
106
|
defaultOpen={openCreateDialog}
|
|
107
|
+
initialMode={initialCreateMode}
|
|
105
108
|
wsId={wsId}
|
|
106
109
|
canCreateTransactions={canCreateTransactions}
|
|
107
110
|
canChangeFinanceWallets={canChangeFinanceWallets}
|
|
108
111
|
canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
|
|
109
112
|
canCreateConfidentialTransactions={canCreateConfidentialTransactions}
|
|
113
|
+
timezone={resolvedWorkspace.timezone}
|
|
110
114
|
permissionRequestUser={permissionRequestUser}
|
|
111
115
|
/>
|
|
112
116
|
<Separator className="my-4" />
|
|
@@ -30,7 +30,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
|
|
|
30
30
|
import { cn } from '@tuturuuu/utils/format';
|
|
31
31
|
import Link from 'next/link';
|
|
32
32
|
import { useLocale, useTranslations } from 'next-intl';
|
|
33
|
-
import { useMemo, useState } from 'react';
|
|
33
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
34
34
|
import { invalidateWalletMutationQueries } from '../query-invalidation';
|
|
35
35
|
import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
|
|
36
36
|
import { WalletCheckpointAmount } from './wallet-checkpoint-amount';
|
|
@@ -63,17 +63,19 @@ type WindowRow =
|
|
|
63
63
|
|
|
64
64
|
export function WalletCheckpointHistoryDialog({
|
|
65
65
|
canCreateTransactions,
|
|
66
|
+
defaultOpen = false,
|
|
66
67
|
financePrefix = '/finance',
|
|
67
68
|
wsId,
|
|
68
69
|
}: {
|
|
69
70
|
canCreateTransactions: boolean;
|
|
71
|
+
defaultOpen?: boolean;
|
|
70
72
|
financePrefix?: string;
|
|
71
73
|
wsId: string;
|
|
72
74
|
}) {
|
|
73
75
|
const t = useTranslations('wallet-checkpoints');
|
|
74
76
|
const locale = useLocale();
|
|
75
77
|
const queryClient = useQueryClient();
|
|
76
|
-
const [open, setOpen] = useState(
|
|
78
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
77
79
|
const [search, setSearch] = useState('');
|
|
78
80
|
const [currency, setCurrency] = useState(ALL);
|
|
79
81
|
const [status, setStatus] = useState<typeof ALL | CheckpointStatus>(ALL);
|
|
@@ -84,6 +86,9 @@ export function WalletCheckpointHistoryDialog({
|
|
|
84
86
|
queryFn: () => getWalletCheckpointHistory(wsId, { limit: 100 }),
|
|
85
87
|
enabled: open,
|
|
86
88
|
});
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (defaultOpen) setOpen(true);
|
|
91
|
+
}, [defaultOpen]);
|
|
87
92
|
const formatDate = (value: string) =>
|
|
88
93
|
new Intl.DateTimeFormat(locale, {
|
|
89
94
|
dateStyle: 'medium',
|
|
@@ -29,7 +29,7 @@ import { toast } from '@tuturuuu/ui/sonner';
|
|
|
29
29
|
import { cn } from '@tuturuuu/utils/format';
|
|
30
30
|
import { useTranslations } from 'next-intl';
|
|
31
31
|
import type { ReactNode } from 'react';
|
|
32
|
-
import { useMemo, useState } from 'react';
|
|
32
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
33
33
|
import { useFinanceBalanceMode } from '../../shared/use-finance-balance-mode';
|
|
34
34
|
import {
|
|
35
35
|
getWalletBalanceTone,
|
|
@@ -51,22 +51,27 @@ type WalletInput = {
|
|
|
51
51
|
export function WalletTotalCheckDialog({
|
|
52
52
|
canUpdateWallets,
|
|
53
53
|
currency,
|
|
54
|
+
defaultOpen = false,
|
|
54
55
|
wsId,
|
|
55
56
|
}: {
|
|
56
57
|
canUpdateWallets: boolean;
|
|
57
58
|
currency: string;
|
|
59
|
+
defaultOpen?: boolean;
|
|
58
60
|
wsId: string;
|
|
59
61
|
}) {
|
|
60
62
|
const t = useTranslations('wallet-checkpoints');
|
|
61
63
|
const queryClient = useQueryClient();
|
|
62
64
|
const { isAuditedMode } = useFinanceBalanceMode();
|
|
63
|
-
const [open, setOpen] = useState(
|
|
65
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
64
66
|
const [values, setValues] = useState<Record<string, string>>({});
|
|
65
67
|
const walletsQuery = useQuery({
|
|
66
68
|
enabled: open && canUpdateWallets,
|
|
67
69
|
queryFn: () => listWallets(wsId),
|
|
68
70
|
queryKey: ['wallets', wsId, 'all-wallet-check'],
|
|
69
71
|
});
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (defaultOpen) setOpen(true);
|
|
74
|
+
}, [defaultOpen]);
|
|
70
75
|
const wallets = useMemo<WalletInput[]>(
|
|
71
76
|
() =>
|
|
72
77
|
(walletsQuery.data ?? []).flatMap((wallet) => {
|
|
@@ -50,7 +50,7 @@ describe('WalletDetailsActions', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('prefills card payments as transfers into the credit wallet', () => {
|
|
53
|
-
render(<WalletDetailsActions {...baseProps} />);
|
|
53
|
+
render(<WalletDetailsActions {...baseProps} timezone="Asia/Ho_Chi_Minh" />);
|
|
54
54
|
|
|
55
55
|
fireEvent.click(screen.getByText('wallet-data-table.credit_payment'));
|
|
56
56
|
|
|
@@ -63,6 +63,9 @@ describe('WalletDetailsActions', () => {
|
|
|
63
63
|
initialTransfer: expect.objectContaining({
|
|
64
64
|
destination_wallet_id: 'wallet-1',
|
|
65
65
|
}),
|
|
66
|
+
preferInitialWalletSelection: false,
|
|
67
|
+
refreshPageOnFinish: true,
|
|
68
|
+
timezone: 'Asia/Ho_Chi_Minh',
|
|
66
69
|
})
|
|
67
70
|
);
|
|
68
71
|
});
|
|
@@ -81,6 +84,8 @@ describe('WalletDetailsActions', () => {
|
|
|
81
84
|
categoryKind: 'expense',
|
|
82
85
|
origin_wallet_id: 'wallet-1',
|
|
83
86
|
}),
|
|
87
|
+
preferInitialWalletSelection: true,
|
|
88
|
+
refreshPageOnFinish: true,
|
|
84
89
|
})
|
|
85
90
|
);
|
|
86
91
|
});
|
|
@@ -99,6 +104,38 @@ describe('WalletDetailsActions', () => {
|
|
|
99
104
|
categoryKind: 'income',
|
|
100
105
|
origin_wallet_id: 'wallet-1',
|
|
101
106
|
}),
|
|
107
|
+
preferInitialWalletSelection: true,
|
|
108
|
+
refreshPageOnFinish: true,
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('prefills standard wallet transactions as source-wallet transactions', () => {
|
|
114
|
+
render(
|
|
115
|
+
<WalletDetailsActions
|
|
116
|
+
{...baseProps}
|
|
117
|
+
wallet={
|
|
118
|
+
{
|
|
119
|
+
id: 'wallet-1',
|
|
120
|
+
name: 'Cash',
|
|
121
|
+
type: 'STANDARD',
|
|
122
|
+
} as never
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
fireEvent.click(screen.getByText('ws-transactions.singular'));
|
|
128
|
+
|
|
129
|
+
const props = mocks.transactionForm.mock.calls.at(-1)?.[0];
|
|
130
|
+
|
|
131
|
+
expect(props).toEqual(
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
initialMode: 'transaction',
|
|
134
|
+
initialTransaction: expect.objectContaining({
|
|
135
|
+
origin_wallet_id: 'wallet-1',
|
|
136
|
+
}),
|
|
137
|
+
preferInitialWalletSelection: true,
|
|
138
|
+
refreshPageOnFinish: true,
|
|
102
139
|
})
|
|
103
140
|
);
|
|
104
141
|
});
|
|
@@ -35,6 +35,7 @@ interface WalletDetailsActionsProps {
|
|
|
35
35
|
canSetFinanceWalletsOnCreate?: boolean;
|
|
36
36
|
canDeleteWallets: boolean;
|
|
37
37
|
isPersonalWorkspace: boolean;
|
|
38
|
+
timezone?: string | null;
|
|
38
39
|
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -50,6 +51,7 @@ export function WalletDetailsActions({
|
|
|
50
51
|
canSetFinanceWalletsOnCreate,
|
|
51
52
|
canDeleteWallets,
|
|
52
53
|
isPersonalWorkspace,
|
|
54
|
+
timezone,
|
|
53
55
|
permissionRequestUser,
|
|
54
56
|
}: WalletDetailsActionsProps) {
|
|
55
57
|
const t = useTranslations();
|
|
@@ -208,6 +210,9 @@ export function WalletDetailsActions({
|
|
|
208
210
|
canCreateConfidentialTransactions={
|
|
209
211
|
canCreateConfidentialTransactions
|
|
210
212
|
}
|
|
213
|
+
timezone={timezone}
|
|
214
|
+
preferInitialWalletSelection={transactionAction !== 'payment'}
|
|
215
|
+
refreshPageOnFinish
|
|
211
216
|
permissionRequestUser={permissionRequestUser}
|
|
212
217
|
/>
|
|
213
218
|
}
|
|
@@ -30,6 +30,7 @@ const mocks = vi.hoisted(() => {
|
|
|
30
30
|
getWorkspace: vi.fn(),
|
|
31
31
|
getWorkspaceConfig: vi.fn(),
|
|
32
32
|
headers: vi.fn(),
|
|
33
|
+
infiniteTransactionsList: vi.fn((_props: unknown) => null),
|
|
33
34
|
notFound: vi.fn(() => {
|
|
34
35
|
throw new Error('notFound');
|
|
35
36
|
}),
|
|
@@ -81,7 +82,9 @@ vi.mock('@tuturuuu/ui/custom/feature-summary', () => ({
|
|
|
81
82
|
}));
|
|
82
83
|
|
|
83
84
|
vi.mock('@tuturuuu/ui/finance/transactions/infinite-transactions-list', () => ({
|
|
84
|
-
InfiniteTransactionsList: (
|
|
85
|
+
InfiniteTransactionsList: (
|
|
86
|
+
...args: Parameters<typeof mocks.infiniteTransactionsList>
|
|
87
|
+
) => mocks.infiniteTransactionsList(...args),
|
|
85
88
|
}));
|
|
86
89
|
|
|
87
90
|
vi.mock('@tuturuuu/ui/separator', () => ({
|
|
@@ -129,7 +132,10 @@ describe('wallet details page', () => {
|
|
|
129
132
|
vi.resetModules();
|
|
130
133
|
vi.clearAllMocks();
|
|
131
134
|
mocks.getTranslations.mockResolvedValue((key: string) => key);
|
|
132
|
-
mocks.getWorkspace.mockResolvedValue({
|
|
135
|
+
mocks.getWorkspace.mockResolvedValue({
|
|
136
|
+
personal: false,
|
|
137
|
+
timezone: 'Asia/Ho_Chi_Minh',
|
|
138
|
+
});
|
|
133
139
|
mocks.getWorkspaceConfig.mockResolvedValue('USD');
|
|
134
140
|
mocks.getPermissions.mockResolvedValue({
|
|
135
141
|
withoutPermission: vi.fn(() => false),
|
|
@@ -208,6 +214,16 @@ describe('wallet details page', () => {
|
|
|
208
214
|
expect(actionProps).toEqual(
|
|
209
215
|
expect.objectContaining({
|
|
210
216
|
initialAction: 'payment',
|
|
217
|
+
timezone: 'Asia/Ho_Chi_Minh',
|
|
218
|
+
walletId: 'wallet-1',
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
expect(mocks.infiniteTransactionsList).toHaveBeenCalled();
|
|
222
|
+
const listProps = mocks.infiniteTransactionsList.mock.calls[0]?.[0];
|
|
223
|
+
|
|
224
|
+
expect(listProps).toEqual(
|
|
225
|
+
expect.objectContaining({
|
|
226
|
+
timezone: 'Asia/Ho_Chi_Minh',
|
|
211
227
|
walletId: 'wallet-1',
|
|
212
228
|
})
|
|
213
229
|
);
|
|
@@ -51,6 +51,7 @@ interface Props {
|
|
|
51
51
|
permissions?: PermissionsResult;
|
|
52
52
|
workspace?: {
|
|
53
53
|
personal?: boolean | null;
|
|
54
|
+
timezone?: string | null;
|
|
54
55
|
};
|
|
55
56
|
permissionRequestUser?: FinancePermissionRequestUser | null;
|
|
56
57
|
}
|
|
@@ -152,6 +153,7 @@ export default async function WalletDetailsPage({
|
|
|
152
153
|
canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
|
|
153
154
|
canDeleteWallets={canDeleteWallets}
|
|
154
155
|
isPersonalWorkspace={!!resolvedWorkspace.personal}
|
|
156
|
+
timezone={resolvedWorkspace.timezone}
|
|
155
157
|
permissionRequestUser={permissionRequestUser}
|
|
156
158
|
/>
|
|
157
159
|
</div>
|
|
@@ -314,6 +316,7 @@ export default async function WalletDetailsPage({
|
|
|
314
316
|
wsId={wsId}
|
|
315
317
|
walletId={walletId}
|
|
316
318
|
currency={currency}
|
|
319
|
+
timezone={resolvedWorkspace.timezone}
|
|
317
320
|
canCreateTransactions={canCreateTransactions}
|
|
318
321
|
canCreateConfidentialTransactions={canCreateConfidentialTransactions}
|
|
319
322
|
canUpdateTransactions={canUpdateTransactions}
|
|
@@ -20,6 +20,7 @@ interface Props {
|
|
|
20
20
|
searchParams: {
|
|
21
21
|
create?: string;
|
|
22
22
|
q?: string;
|
|
23
|
+
tool?: string;
|
|
23
24
|
};
|
|
24
25
|
currency?: string;
|
|
25
26
|
financePrefix?: string;
|
|
@@ -90,11 +91,13 @@ export default async function WalletsPage({
|
|
|
90
91
|
wsId={wsId}
|
|
91
92
|
financePrefix={financePrefix}
|
|
92
93
|
canCreateTransactions={canCreateTransactions}
|
|
94
|
+
defaultOpen={searchParams.tool === 'checkpoint-history'}
|
|
93
95
|
/>
|
|
94
96
|
<WalletTotalCheckDialog
|
|
95
97
|
wsId={wsId}
|
|
96
98
|
currency={resolvedCurrency ?? 'USD'}
|
|
97
99
|
canUpdateWallets={canUpdateWallets}
|
|
100
|
+
defaultOpen={searchParams.tool === 'all-wallet-check'}
|
|
98
101
|
/>
|
|
99
102
|
</div>
|
|
100
103
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Check, ExternalLink, Link, Loader2, RefreshCw } from '@tuturuuu/icons';
|
|
4
|
+
import { getGoogleCalendarAuthUrl } from '@tuturuuu/internal-api';
|
|
4
5
|
import { createClient } from '@tuturuuu/supabase/next/client';
|
|
5
6
|
import type { WorkspaceCalendarGoogleTokenClient } from '@tuturuuu/types';
|
|
6
7
|
import { Alert, AlertDescription } from '@tuturuuu/ui/alert';
|
|
@@ -119,15 +120,7 @@ export function GoogleCalendarSettings({
|
|
|
119
120
|
|
|
120
121
|
setIsGoogleAuthenticating(true);
|
|
121
122
|
try {
|
|
122
|
-
const
|
|
123
|
-
method: 'GET',
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const { authUrl } = await response.json();
|
|
123
|
+
const { authUrl } = await getGoogleCalendarAuthUrl(wsId);
|
|
131
124
|
window.location.href = authUrl;
|
|
132
125
|
} catch (error) {
|
|
133
126
|
console.error('Error initiating Google auth:', error);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { MoneyInput } from './money-input';
|
|
4
|
+
|
|
5
|
+
describe('MoneyInput', () => {
|
|
6
|
+
it('renders a minor-unit USD value as major units', () => {
|
|
7
|
+
render(
|
|
8
|
+
<MoneyInput
|
|
9
|
+
aria-label="Price"
|
|
10
|
+
currency="USD"
|
|
11
|
+
hideHelpers
|
|
12
|
+
onChange={vi.fn()}
|
|
13
|
+
value={10000}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const input = screen.getByLabelText('Price') as HTMLInputElement;
|
|
18
|
+
expect(input).toHaveValue('100');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('emits minor units (cents) for a USD entry', () => {
|
|
22
|
+
const onChange = vi.fn();
|
|
23
|
+
render(
|
|
24
|
+
<MoneyInput
|
|
25
|
+
aria-label="Price"
|
|
26
|
+
currency="USD"
|
|
27
|
+
hideHelpers
|
|
28
|
+
onChange={onChange}
|
|
29
|
+
value={undefined}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const input = screen.getByLabelText('Price') as HTMLInputElement;
|
|
34
|
+
fireEvent.focus(input);
|
|
35
|
+
fireEvent.change(input, {
|
|
36
|
+
target: { selectionStart: 4, value: '9.99' },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(onChange).toHaveBeenLastCalledWith(999);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('treats zero-decimal currencies 1:1', () => {
|
|
43
|
+
const onChange = vi.fn();
|
|
44
|
+
render(
|
|
45
|
+
<MoneyInput
|
|
46
|
+
aria-label="Price"
|
|
47
|
+
currency="VND"
|
|
48
|
+
hideHelpers
|
|
49
|
+
onChange={onChange}
|
|
50
|
+
value={25000}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const input = screen.getByLabelText('Price') as HTMLInputElement;
|
|
55
|
+
// 25000 minor units == 25000 major units for VND (0 fraction digits).
|
|
56
|
+
expect(input.value.replace(/[^\d]/g, '')).toBe('25000');
|
|
57
|
+
|
|
58
|
+
fireEvent.focus(input);
|
|
59
|
+
fireEvent.change(input, {
|
|
60
|
+
target: { selectionStart: 3, value: '500' },
|
|
61
|
+
});
|
|
62
|
+
expect(onChange).toHaveBeenLastCalledWith(500);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { getCurrencyLocale } from '@tuturuuu/utils/format';
|
|
4
|
+
import {
|
|
5
|
+
getCurrencyFractionDigits,
|
|
6
|
+
majorToMinor,
|
|
7
|
+
minorToMajor,
|
|
8
|
+
} from '@tuturuuu/utils/money';
|
|
9
|
+
import { useMemo } from 'react';
|
|
10
|
+
import { CurrencyInput, type CurrencyInputProps } from './currency-input';
|
|
11
|
+
|
|
12
|
+
export interface MoneyInputProps
|
|
13
|
+
extends Omit<
|
|
14
|
+
CurrencyInputProps,
|
|
15
|
+
'value' | 'onChange' | 'locale' | 'maximumFractionDigits' | 'currencySuffix'
|
|
16
|
+
> {
|
|
17
|
+
/** ISO currency code (e.g. 'USD', 'VND'). Drives precision + locale. */
|
|
18
|
+
currency: string;
|
|
19
|
+
/** Amount in integer minor units (cents for USD, whole units for JPY/VND). */
|
|
20
|
+
value: number | undefined;
|
|
21
|
+
/** Emits the amount in integer minor units. */
|
|
22
|
+
onChange: (minorValue: number) => void;
|
|
23
|
+
/** Show the currency code as a muted suffix (defaults to true). */
|
|
24
|
+
showCurrencySuffix?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Currency-aware money input. The `value`/`onChange` boundary is always in
|
|
29
|
+
* integer **minor units** (the canonical storage format); the field renders and
|
|
30
|
+
* edits in localized major units with the correct precision for the currency.
|
|
31
|
+
*
|
|
32
|
+
* Wraps the shared {@link CurrencyInput} so all money entry across the platform
|
|
33
|
+
* shares one polished input (cursor preservation, quick-action helpers) while
|
|
34
|
+
* keeping the minor-unit conversion centralized in one place.
|
|
35
|
+
*/
|
|
36
|
+
export function MoneyInput({
|
|
37
|
+
currency,
|
|
38
|
+
value,
|
|
39
|
+
onChange,
|
|
40
|
+
showCurrencySuffix = true,
|
|
41
|
+
...props
|
|
42
|
+
}: MoneyInputProps) {
|
|
43
|
+
const fractionDigits = useMemo(
|
|
44
|
+
() => getCurrencyFractionDigits(currency),
|
|
45
|
+
[currency]
|
|
46
|
+
);
|
|
47
|
+
const locale = useMemo(() => getCurrencyLocale(currency), [currency]);
|
|
48
|
+
const majorValue = useMemo(
|
|
49
|
+
() => (value === undefined ? undefined : minorToMajor(value, currency)),
|
|
50
|
+
[currency, value]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<CurrencyInput
|
|
55
|
+
{...props}
|
|
56
|
+
value={majorValue}
|
|
57
|
+
onChange={(major) => onChange(majorToMinor(major, currency))}
|
|
58
|
+
locale={locale}
|
|
59
|
+
maximumFractionDigits={fractionDigits}
|
|
60
|
+
currencySuffix={showCurrencySuffix ? currency.toUpperCase() : undefined}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { DateTimePicker } from '@tuturuuu/ui/date-time-picker';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import {
|
|
6
|
+
buildDateInTimezone,
|
|
7
|
+
getDatePartsInTimezone,
|
|
8
|
+
} from '@tuturuuu/utils/task-date-timezone';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
|
|
11
|
+
export interface OptionalTimePickerProps {
|
|
12
|
+
date?: Date;
|
|
13
|
+
setDate: (date: Date | undefined) => void;
|
|
14
|
+
includeTime: boolean;
|
|
15
|
+
setIncludeTime: (includeTime: boolean) => void;
|
|
16
|
+
includeTimeLabel: ReactNode;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
allowClear?: boolean;
|
|
19
|
+
showFooterControls?: boolean;
|
|
20
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
21
|
+
align?: 'start' | 'center' | 'end';
|
|
22
|
+
collisionPadding?: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
preferences?: {
|
|
25
|
+
weekStartsOn?: 0 | 1 | 6;
|
|
26
|
+
timezone?: string;
|
|
27
|
+
timeFormat?: '12h' | '24h';
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function startOfDay(date: Date, timezone?: string) {
|
|
32
|
+
if (!timezone) {
|
|
33
|
+
const next = new Date(date);
|
|
34
|
+
next.setHours(0, 0, 0, 0);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parts = getDatePartsInTimezone(date, timezone);
|
|
39
|
+
return buildDateInTimezone(
|
|
40
|
+
parts.year,
|
|
41
|
+
parts.month,
|
|
42
|
+
parts.day,
|
|
43
|
+
0,
|
|
44
|
+
0,
|
|
45
|
+
timezone
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function OptionalTimePicker({
|
|
50
|
+
date,
|
|
51
|
+
setDate,
|
|
52
|
+
includeTime,
|
|
53
|
+
setIncludeTime,
|
|
54
|
+
includeTimeLabel,
|
|
55
|
+
disabled = false,
|
|
56
|
+
allowClear = true,
|
|
57
|
+
showFooterControls = true,
|
|
58
|
+
side = 'bottom',
|
|
59
|
+
align = 'start',
|
|
60
|
+
collisionPadding = 16,
|
|
61
|
+
className,
|
|
62
|
+
preferences,
|
|
63
|
+
}: OptionalTimePickerProps) {
|
|
64
|
+
const handleDateChange = (nextDate: Date | undefined) => {
|
|
65
|
+
if (!nextDate || includeTime) {
|
|
66
|
+
setDate(nextDate);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setDate(startOfDay(nextDate, preferences?.timezone));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={cn('w-full', className)}>
|
|
75
|
+
<DateTimePicker
|
|
76
|
+
date={date}
|
|
77
|
+
setDate={handleDateChange}
|
|
78
|
+
showTimeSelect={includeTime}
|
|
79
|
+
allowClear={allowClear}
|
|
80
|
+
showFooterControls={showFooterControls}
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
side={side}
|
|
83
|
+
align={align}
|
|
84
|
+
collisionPadding={collisionPadding}
|
|
85
|
+
preferences={preferences}
|
|
86
|
+
timeToggle={{
|
|
87
|
+
checked: includeTime,
|
|
88
|
+
disabled,
|
|
89
|
+
label: includeTimeLabel,
|
|
90
|
+
onCheckedChange: setIncludeTime,
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|