@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/biome.json +1 -1
  3. package/package.json +11 -11
  4. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  5. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  6. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  7. package/src/components/ui/calendar.test.tsx +24 -0
  8. package/src/components/ui/calendar.tsx +1 -0
  9. package/src/components/ui/currency-input.test.tsx +43 -0
  10. package/src/components/ui/currency-input.tsx +1 -1
  11. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  12. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  13. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  14. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  15. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  16. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  17. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  18. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  19. package/src/components/ui/date-time-picker.tsx +352 -234
  20. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  21. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  22. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  23. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  24. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  25. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  26. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  27. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  28. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  29. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  30. package/src/components/ui/finance/transactions/form-types.ts +5 -0
  31. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  32. package/src/components/ui/finance/transactions/form.tsx +116 -20
  33. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  34. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  35. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  36. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  37. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  38. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  39. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  40. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  41. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  42. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  43. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  48. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  49. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  50. package/src/components/ui/money-input.test.tsx +64 -0
  51. package/src/components/ui/money-input.tsx +63 -0
  52. package/src/components/ui/optional-time-picker.tsx +95 -0
  53. package/src/components/ui/quick-command-center.test.tsx +90 -0
  54. package/src/components/ui/quick-command-center.tsx +190 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +126 -50
  56. package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +23 -20
  58. package/src/components/ui/storefront/image-panel.tsx +6 -0
  59. package/src/components/ui/storefront/index.ts +11 -0
  60. package/src/components/ui/storefront/listing-card.tsx +84 -22
  61. package/src/components/ui/storefront/product-detail.tsx +289 -0
  62. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  63. package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
  64. package/src/components/ui/storefront/storefront-surface.tsx +371 -128
  65. package/src/components/ui/storefront/types.ts +25 -1
  66. package/src/components/ui/storefront/utils.ts +118 -13
  67. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  68. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  69. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  70. package/src/components/ui/text-editor/content-migration.ts +41 -18
  71. package/src/components/ui/text-editor/extensions.ts +1 -1
  72. package/src/components/ui/text-editor/image-extension.ts +40 -18
  73. package/src/components/ui/text-editor/video-extension.ts +11 -2
  74. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  75. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  76. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
  77. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  79. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
  80. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
  81. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
  82. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
  83. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
  84. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  85. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  86. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  87. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  88. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  89. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  90. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
  91. package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
  92. package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
  93. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  94. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  95. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  100. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  101. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
  102. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
  103. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  104. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  105. package/src/hooks/useBoardRealtime.ts +6 -3
  106. package/src/hooks/useBoardRealtime.types.ts +11 -0
  107. package/src/hooks/useCursorTracking.ts +91 -27
  108. 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(false);
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(false);
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: () => null,
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({ personal: false });
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 response = await fetch(`/api/v1/calendar/auth?wsId=${wsId}`, {
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
+ }