@tuturuuu/ui 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -3
  3. package/biome.json +1 -1
  4. package/package.json +8 -8
  5. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  6. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  7. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  8. package/src/components/ui/calendar.test.tsx +24 -0
  9. package/src/components/ui/calendar.tsx +1 -0
  10. package/src/components/ui/date-time-picker.tsx +352 -234
  11. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  12. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  13. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  14. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  15. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  16. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  17. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  18. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  19. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  20. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  21. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  22. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  23. package/src/components/ui/finance/transactions/form.tsx +116 -20
  24. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  25. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  26. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  27. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  28. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  29. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  30. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  31. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  34. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  35. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  36. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  38. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  39. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  40. package/src/components/ui/optional-time-picker.tsx +95 -0
  41. package/src/components/ui/quick-command-center.test.tsx +90 -0
  42. package/src/components/ui/quick-command-center.tsx +190 -0
  43. package/src/components/ui/storefront/cart-summary.tsx +18 -27
  44. package/src/components/ui/storefront/hero-panel.tsx +22 -13
  45. package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
  46. package/src/components/ui/storefront/storefront-surface.tsx +84 -41
  47. package/src/components/ui/storefront/types.ts +2 -0
  48. package/src/components/ui/storefront/utils.ts +21 -0
  49. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  50. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  51. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
@@ -0,0 +1,262 @@
1
+ 'use client';
2
+
3
+ import {
4
+ CreditCard,
5
+ DollarSign,
6
+ FileText,
7
+ Repeat,
8
+ Tag,
9
+ Target,
10
+ TrendingDown,
11
+ Wallet as WalletIcon,
12
+ } from '@tuturuuu/icons';
13
+ import type {
14
+ RecurringTransactionRecord,
15
+ TransactionTagRecord,
16
+ } from '@tuturuuu/internal-api/finance';
17
+ import type { FinanceBudget } from '@tuturuuu/types';
18
+ import type { DebtLoanWithBalance } from '@tuturuuu/types/primitives/DebtLoan';
19
+ import type { Invoice } from '@tuturuuu/types/primitives/Invoice';
20
+ import type { Transaction } from '@tuturuuu/types/primitives/Transaction';
21
+ import type { TransactionCategoryWithStats } from '@tuturuuu/types/primitives/TransactionCategory';
22
+ import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
23
+ import type { QuickCommandCenterGroup } from '@tuturuuu/ui/quick-command-center';
24
+ import { formatCurrency } from '@tuturuuu/utils/format';
25
+
26
+ type Translation = (key: any) => string;
27
+
28
+ interface FinanceRecentCommandGroupsOptions {
29
+ budgets?: FinanceBudget[];
30
+ categories?: TransactionCategoryWithStats[];
31
+ currency: string;
32
+ debts?: DebtLoanWithBalance[];
33
+ invoices?: Invoice[];
34
+ locale: string;
35
+ pushFinanceHref: (path: string) => void;
36
+ recurring?: RecurringTransactionRecord[];
37
+ search: string;
38
+ tags?: TransactionTagRecord[];
39
+ tCommand: Translation;
40
+ tFinance: Translation;
41
+ transactions?: Transaction[];
42
+ wallets?: Wallet[];
43
+ }
44
+
45
+ function formatDate(value: string | undefined, locale: string) {
46
+ if (!value) return null;
47
+ const date = new Date(value);
48
+ if (Number.isNaN(date.getTime())) return null;
49
+
50
+ return new Intl.DateTimeFormat(locale, {
51
+ dateStyle: 'medium',
52
+ timeStyle: 'short',
53
+ }).format(date);
54
+ }
55
+
56
+ function matchesQuery(
57
+ value: string | null | undefined,
58
+ query: string | null | undefined
59
+ ) {
60
+ if (!query) return true;
61
+ return (value ?? '').toLowerCase().includes(query.toLowerCase());
62
+ }
63
+
64
+ export function buildFinanceRecentCommandGroups({
65
+ budgets,
66
+ categories,
67
+ currency,
68
+ debts,
69
+ invoices,
70
+ locale,
71
+ pushFinanceHref,
72
+ recurring,
73
+ search,
74
+ tags,
75
+ tCommand,
76
+ tFinance,
77
+ transactions,
78
+ wallets,
79
+ }: FinanceRecentCommandGroupsOptions): QuickCommandCenterGroup[] {
80
+ const groups: QuickCommandCenterGroup[] = [];
81
+ const txItems = (transactions ?? [])
82
+ .filter((transaction) => !!transaction.id)
83
+ .map((transaction) => {
84
+ const amount =
85
+ typeof transaction.amount === 'number'
86
+ ? formatCurrency(
87
+ transaction.amount,
88
+ transaction.wallet_currency ?? currency,
89
+ locale,
90
+ { signDisplay: 'always' }
91
+ )
92
+ : null;
93
+ return {
94
+ description: [
95
+ amount,
96
+ transaction.wallet_name,
97
+ formatDate(transaction.taken_at, locale),
98
+ ]
99
+ .filter(Boolean)
100
+ .join(' · '),
101
+ icon: <DollarSign className="h-4 w-4" />,
102
+ id: `transaction-${transaction.id}`,
103
+ onSelect: () => pushFinanceHref(`/transactions/${transaction.id}`),
104
+ title:
105
+ transaction.description ||
106
+ transaction.category_name ||
107
+ tCommand('transaction_fallback'),
108
+ };
109
+ });
110
+
111
+ if (txItems.length > 0) {
112
+ groups.push({
113
+ heading: tCommand('recent_transactions'),
114
+ id: 'recent-transactions',
115
+ items: txItems,
116
+ });
117
+ }
118
+
119
+ const walletItems = (wallets ?? []).map((wallet) => ({
120
+ description: [wallet.type, wallet.currency].filter(Boolean).join(' · '),
121
+ icon: <WalletIcon className="h-4 w-4" />,
122
+ id: `wallet-${wallet.id}`,
123
+ onSelect: () => pushFinanceHref(`/wallets/${wallet.id}`),
124
+ title: wallet.name || tCommand('wallet_fallback'),
125
+ }));
126
+
127
+ if (walletItems.length > 0) {
128
+ groups.push({
129
+ heading: tCommand('recent_wallets'),
130
+ id: 'recent-wallets',
131
+ items: walletItems,
132
+ });
133
+ }
134
+
135
+ const invoiceItems = (invoices ?? []).map((invoice) => ({
136
+ description: [
137
+ invoice.customer?.full_name || invoice.customer?.display_name,
138
+ typeof invoice.price === 'number'
139
+ ? formatCurrency(invoice.price, currency, locale)
140
+ : null,
141
+ formatDate(invoice.created_at, locale),
142
+ ]
143
+ .filter(Boolean)
144
+ .join(' · '),
145
+ icon: <FileText className="h-4 w-4" />,
146
+ id: `invoice-${invoice.id}`,
147
+ onSelect: () => pushFinanceHref(`/invoices/${invoice.id}`),
148
+ title:
149
+ invoice.notice ||
150
+ invoice.note ||
151
+ `${tCommand('invoice_fallback')} ${invoice.id.slice(0, 8)}`,
152
+ }));
153
+
154
+ if (invoiceItems.length > 0) {
155
+ groups.push({
156
+ heading: tCommand('recent_invoices'),
157
+ id: 'recent-invoices',
158
+ items: invoiceItems,
159
+ });
160
+ }
161
+
162
+ const budgetItems = (budgets ?? [])
163
+ .filter((budget) => matchesQuery(budget.name, search))
164
+ .slice(0, 5)
165
+ .map((budget) => ({
166
+ description:
167
+ typeof budget.amount === 'number'
168
+ ? formatCurrency(budget.amount, currency, locale)
169
+ : undefined,
170
+ icon: <Target className="h-4 w-4" />,
171
+ id: `budget-${budget.id}`,
172
+ onSelect: () => pushFinanceHref('/budgets'),
173
+ title: budget.name,
174
+ }));
175
+
176
+ if (budgetItems.length > 0) {
177
+ groups.push({
178
+ heading: tCommand('recent_budgets'),
179
+ id: 'recent-budgets',
180
+ items: budgetItems,
181
+ });
182
+ }
183
+
184
+ const recurringItems = (recurring ?? [])
185
+ .filter((item) => matchesQuery(item.name, search))
186
+ .slice(0, 5)
187
+ .map((item) => ({
188
+ description: item.frequency,
189
+ icon: <Repeat className="h-4 w-4" />,
190
+ id: `recurring-${item.id}`,
191
+ onSelect: () => pushFinanceHref('/recurring'),
192
+ title: item.name,
193
+ }));
194
+
195
+ if (recurringItems.length > 0) {
196
+ groups.push({
197
+ heading: tCommand('recent_recurring'),
198
+ id: 'recent-recurring',
199
+ items: recurringItems,
200
+ });
201
+ }
202
+
203
+ const debtItems = (debts ?? [])
204
+ .filter((item) => matchesQuery(item.name, search))
205
+ .slice(0, 5)
206
+ .map((item) => ({
207
+ description: item.type,
208
+ icon: <TrendingDown className="h-4 w-4" />,
209
+ id: `debt-${item.id}`,
210
+ onSelect: () => pushFinanceHref(`/debts/${item.id}`),
211
+ title: item.name,
212
+ }));
213
+
214
+ if (debtItems.length > 0) {
215
+ groups.push({
216
+ heading: tCommand('recent_debts'),
217
+ id: 'recent-debts',
218
+ items: debtItems,
219
+ });
220
+ }
221
+
222
+ const categoryItems = (categories ?? [])
223
+ .filter((item) => matchesQuery(item.name, search))
224
+ .slice(0, 5)
225
+ .map((item) => ({
226
+ description:
227
+ item.is_expense === false ? tFinance('income') : tFinance('expense'),
228
+ icon: <CreditCard className="h-4 w-4" />,
229
+ id: `category-${item.id}`,
230
+ onSelect: () => pushFinanceHref('/categories'),
231
+ title: item.name ?? tCommand('category_fallback'),
232
+ }));
233
+
234
+ if (categoryItems.length > 0) {
235
+ groups.push({
236
+ heading: tCommand('recent_categories'),
237
+ id: 'recent-categories',
238
+ items: categoryItems,
239
+ });
240
+ }
241
+
242
+ const tagItems = (tags ?? [])
243
+ .filter((item) => matchesQuery(item.name, search))
244
+ .slice(0, 5)
245
+ .map((item) => ({
246
+ description: item.description ?? undefined,
247
+ icon: <Tag className="h-4 w-4" />,
248
+ id: `tag-${item.id}`,
249
+ onSelect: () => pushFinanceHref(`/transactions?tagIds=${item.id}`),
250
+ title: item.name,
251
+ }));
252
+
253
+ if (tagItems.length > 0) {
254
+ groups.push({
255
+ heading: tCommand('recent_tags'),
256
+ id: 'recent-tags',
257
+ items: tagItems,
258
+ });
259
+ }
260
+
261
+ return groups;
262
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useQueryClient } from '@tanstack/react-query';
4
4
  import { Loader2 } from '@tuturuuu/icons';
5
+ import { Button } from '@tuturuuu/ui/button';
5
6
  import { DataTable } from '@tuturuuu/ui/custom/tables/data-table';
6
7
  import { Skeleton } from '@tuturuuu/ui/skeleton';
7
8
  import { useTranslations } from 'next-intl';
@@ -86,14 +87,17 @@ export function PendingInvoicesTable({
86
87
  setPageSize(null);
87
88
  }, [setQ, setUserIds, setPage, setPageSize]);
88
89
 
89
- const { data, isLoading, isFetching, error } = usePendingInvoices(wsId, {
90
- page,
91
- pageSize,
92
- q,
93
- userIds,
94
- groupByUser,
95
- enabled: !isConfigLoading,
96
- });
90
+ const { data, isLoading, isFetching, error, refetch } = usePendingInvoices(
91
+ wsId,
92
+ {
93
+ page,
94
+ pageSize,
95
+ q,
96
+ userIds,
97
+ groupByUser,
98
+ enabled: !isConfigLoading,
99
+ }
100
+ );
97
101
 
98
102
  const columns = useMemo(
99
103
  () =>
@@ -170,10 +174,19 @@ export function PendingInvoicesTable({
170
174
 
171
175
  if (error) {
172
176
  return (
173
- <div className="flex items-center justify-center py-8">
177
+ <div className="flex flex-col items-center justify-center gap-3 py-8">
174
178
  <p className="text-destructive text-sm">
175
179
  {t('ws-invoices.error_loading')}
176
180
  </p>
181
+ <Button
182
+ variant="outline"
183
+ size="sm"
184
+ onClick={() => refetch()}
185
+ disabled={isFetching}
186
+ >
187
+ {isFetching && <Loader2 className="h-4 w-4 animate-spin" />}
188
+ {t('common.retry')}
189
+ </Button>
177
190
  </div>
178
191
  );
179
192
  }
@@ -1,16 +1,6 @@
1
1
  'use client';
2
2
 
3
- import {
4
- CreditCard,
5
- DollarSign,
6
- FileText,
7
- Plus,
8
- Repeat,
9
- Target,
10
- TrendingDown,
11
- TrendingUp,
12
- Wallet,
13
- } from '@tuturuuu/icons';
3
+ import { Plus } from '@tuturuuu/icons';
14
4
  import { Button } from '@tuturuuu/ui/button';
15
5
  import {
16
6
  DropdownMenu,
@@ -22,6 +12,11 @@ import {
22
12
  } from '@tuturuuu/ui/dropdown-menu';
23
13
  import { useRouter } from 'next/navigation';
24
14
  import { useTranslations } from 'next-intl';
15
+ import { Fragment } from 'react';
16
+ import {
17
+ buildFinanceCommandActionGroups,
18
+ renderFinanceCommandActionIcon,
19
+ } from '../command/finance-command-actions';
25
20
  import { useFinanceHref } from '../finance-route-context';
26
21
 
27
22
  interface QuickActionsProps {
@@ -45,14 +40,21 @@ export function QuickActions({
45
40
  }: QuickActionsProps) {
46
41
  const router = useRouter();
47
42
  const t = useTranslations('finance');
43
+ const commandT = useTranslations('finance-command-center');
48
44
  const financeHref = useFinanceHref();
49
- const hasVisibleActions =
50
- canCreateDebts ||
51
- canCreateRecurringTransactions ||
52
- canCreateTransactions ||
53
- canCreateWallets ||
54
- canManageFinance ||
55
- canCreateInvoices;
45
+ const groups = buildFinanceCommandActionGroups({
46
+ permissions: {
47
+ canCreateDebts,
48
+ canCreateInvoices,
49
+ canCreateRecurringTransactions,
50
+ canCreateTransactions,
51
+ canCreateWallets,
52
+ canManageFinance,
53
+ },
54
+ tCommand: commandT,
55
+ tFinance: t,
56
+ });
57
+ const hasVisibleActions = groups.some((group) => group.items.length > 0);
56
58
 
57
59
  if (!hasVisibleActions) return null;
58
60
 
@@ -73,78 +75,25 @@ export function QuickActions({
73
75
  </DropdownMenuTrigger>
74
76
  <DropdownMenuContent align="end" className="w-56">
75
77
  <DropdownMenuLabel>{t('quick_actions')}</DropdownMenuLabel>
76
- <DropdownMenuSeparator />
77
- {canCreateTransactions && (
78
- <DropdownMenuItem
79
- onClick={() => pushFinanceHref('/transactions?create=transaction')}
80
- >
81
- <DollarSign className="mr-2 h-4 w-4" />
82
- <span>{t('new_transaction')}</span>
83
- </DropdownMenuItem>
84
- )}
85
- {canCreateRecurringTransactions && (
86
- <DropdownMenuItem
87
- onClick={() => pushFinanceHref('/recurring?create=recurring')}
88
- >
89
- <Repeat className="mr-2 h-4 w-4" />
90
- <span>{t('new_recurring_transaction')}</span>
91
- </DropdownMenuItem>
92
- )}
93
- {canCreateWallets && (
94
- <>
95
- <DropdownMenuItem
96
- onClick={() => pushFinanceHref('/wallets?create=wallet')}
97
- >
98
- <Wallet className="mr-2 h-4 w-4" />
99
- <span>{t('new_wallet')}</span>
100
- </DropdownMenuItem>
101
- <DropdownMenuItem
102
- onClick={() => pushFinanceHref('/wallets?create=credit-card')}
103
- >
104
- <CreditCard className="mr-2 h-4 w-4" />
105
- <span>{t('new_credit_card')}</span>
106
- </DropdownMenuItem>
107
- </>
108
- )}
109
- {canManageFinance && (
110
- <DropdownMenuItem
111
- onClick={() => pushFinanceHref('/budgets?create=budget')}
112
- >
113
- <Target className="mr-2 h-4 w-4" />
114
- <span>{t('new_budget')}</span>
115
- </DropdownMenuItem>
116
- )}
117
- {(canCreateInvoices || canCreateDebts || canManageFinance) && (
118
- <DropdownMenuSeparator />
119
- )}
120
- {canCreateInvoices && (
121
- <DropdownMenuItem onClick={() => pushFinanceHref('/invoices/new')}>
122
- <FileText className="mr-2 h-4 w-4" />
123
- <span>{t('new_invoice')}</span>
124
- </DropdownMenuItem>
125
- )}
126
- {canCreateDebts && (
127
- <DropdownMenuItem
128
- onClick={() => pushFinanceHref('/debts?create=debt')}
129
- >
130
- <TrendingDown className="mr-2 h-4 w-4" />
131
- <span>{t('new_debt')}</span>
132
- </DropdownMenuItem>
133
- )}
134
- {canCreateDebts && (
135
- <DropdownMenuItem
136
- onClick={() => pushFinanceHref('/debts?create=loan')}
137
- >
138
- <TrendingUp className="mr-2 h-4 w-4" />
139
- <span>{t('new_loan')}</span>
140
- </DropdownMenuItem>
141
- )}
142
- {canManageFinance && (
143
- <DropdownMenuItem onClick={() => pushFinanceHref('/categories')}>
144
- <CreditCard className="mr-2 h-4 w-4" />
145
- <span>{t('manage_categories')}</span>
146
- </DropdownMenuItem>
147
- )}
78
+ {groups.map((group, groupIndex) => (
79
+ <Fragment key={group.id}>
80
+ <DropdownMenuSeparator />
81
+ {groupIndex > 0 && (
82
+ <DropdownMenuLabel>{group.heading}</DropdownMenuLabel>
83
+ )}
84
+ {group.items.map((action) => (
85
+ <DropdownMenuItem
86
+ key={action.id}
87
+ onClick={() => pushFinanceHref(action.href)}
88
+ >
89
+ <span className="mr-2">
90
+ {renderFinanceCommandActionIcon(action)}
91
+ </span>
92
+ <span>{action.title}</span>
93
+ </DropdownMenuItem>
94
+ ))}
95
+ </Fragment>
96
+ ))}
148
97
  </DropdownMenuContent>
149
98
  </DropdownMenu>
150
99
  );
@@ -62,7 +62,7 @@ import { Textarea } from '@tuturuuu/ui/textarea';
62
62
  import { cn, formatCurrency } from '@tuturuuu/utils/format';
63
63
  import { useRouter } from 'next/navigation';
64
64
  import { useLocale, useTranslations } from 'next-intl';
65
- import { useMemo, useState } from 'react';
65
+ import { useCallback, useEffect, useMemo, useState } from 'react';
66
66
  import { useForm } from 'react-hook-form';
67
67
  import * as z from 'zod';
68
68
  import { useFinanceHref } from '../finance-route-context';
@@ -73,6 +73,8 @@ import {
73
73
 
74
74
  interface TagManagerProps {
75
75
  currency: string;
76
+ onOpenCreateDialogChange?: (open: boolean) => void;
77
+ openCreateDialog?: boolean;
76
78
  wsId: string;
77
79
  }
78
80
 
@@ -99,7 +101,12 @@ const PRESET_COLORS = [
99
101
  '#ec4899', // pink
100
102
  ];
101
103
 
102
- export function TagManager({ currency, wsId }: TagManagerProps) {
104
+ export function TagManager({
105
+ currency,
106
+ onOpenCreateDialogChange,
107
+ openCreateDialog,
108
+ wsId,
109
+ }: TagManagerProps) {
103
110
  const t = useTranslations();
104
111
  const locale = useLocale();
105
112
  const { isConfidential: areNumbersHidden } =
@@ -221,7 +228,7 @@ export function TagManager({ currency, wsId }: TagManagerProps) {
221
228
  router.push(`/${wsId}${financeHref('/transactions')}?tagIds=${tagId}`);
222
229
  };
223
230
 
224
- const handleOpenCreate = () => {
231
+ const handleOpenCreate = useCallback(() => {
225
232
  setEditingTag(null);
226
233
  form.reset({
227
234
  name: '',
@@ -229,7 +236,11 @@ export function TagManager({ currency, wsId }: TagManagerProps) {
229
236
  description: '',
230
237
  });
231
238
  setIsDialogOpen(true);
232
- };
239
+ }, [form]);
240
+
241
+ useEffect(() => {
242
+ if (openCreateDialog) handleOpenCreate();
243
+ }, [handleOpenCreate, openCreateDialog]);
233
244
 
234
245
  const handleOpenEdit = (tag: TransactionTag) => {
235
246
  setEditingTag(tag);
@@ -475,7 +486,15 @@ export function TagManager({ currency, wsId }: TagManagerProps) {
475
486
  )}
476
487
 
477
488
  {/* Create/Edit Dialog */}
478
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
489
+ <Dialog
490
+ open={isDialogOpen}
491
+ onOpenChange={(open) => {
492
+ setIsDialogOpen(open);
493
+ if (!open && openCreateDialog) {
494
+ onOpenCreateDialogChange?.(false);
495
+ }
496
+ }}
497
+ >
479
498
  <DialogContent>
480
499
  <DialogHeader>
481
500
  <DialogTitle>
@@ -1,10 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { CalendarIcon, PlusIcon } from '@tuturuuu/icons';
3
+ import { PlusIcon } from '@tuturuuu/icons';
4
4
  import type { TransactionCategory } from '@tuturuuu/types/primitives/TransactionCategory';
5
5
  import type { Wallet as WalletType } from '@tuturuuu/types/primitives/Wallet';
6
- import { Button } from '@tuturuuu/ui/button';
7
- import { Calendar } from '@tuturuuu/ui/calendar';
8
6
  import { CurrencyInput } from '@tuturuuu/ui/currency-input';
9
7
  import { Combobox } from '@tuturuuu/ui/custom/combobox';
10
8
  import { getIconComponentByKey } from '@tuturuuu/ui/custom/icon-picker';
@@ -22,10 +20,8 @@ import {
22
20
  FormMessage,
23
21
  } from '@tuturuuu/ui/form';
24
22
  import { Input } from '@tuturuuu/ui/input';
25
- import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
23
+ import { OptionalTimePicker } from '@tuturuuu/ui/optional-time-picker';
26
24
  import { computeAccessibleLabelStyles } from '@tuturuuu/utils/label-colors';
27
- import { format } from 'date-fns';
28
- import { enUS, vi } from 'date-fns/locale';
29
25
  import { useTranslations } from 'next-intl';
30
26
  import type { ReactNode } from 'react';
31
27
  import type { UseFormReturn } from 'react-hook-form';
@@ -52,6 +48,9 @@ interface FormBasicTabProps {
52
48
  suggestedExchangeRate: number | null;
53
49
  isDestinationOverridden: boolean;
54
50
  setIsDestinationOverridden: (value: boolean) => void;
51
+ includeTakenAtTime: boolean;
52
+ setIncludeTakenAtTime: (value: boolean) => void;
53
+ timezone?: string | null;
55
54
  setNewContentType: (value: NewContentType) => void;
56
55
  setNewContent: (value: NewContent) => void;
57
56
  walletPrefillMeta?: {
@@ -83,6 +82,9 @@ export function FormBasicTab({
83
82
  suggestedExchangeRate,
84
83
  isDestinationOverridden,
85
84
  setIsDestinationOverridden,
85
+ includeTakenAtTime,
86
+ setIncludeTakenAtTime,
87
+ timezone,
86
88
  setNewContentType,
87
89
  setNewContent,
88
90
  walletPrefillMeta,
@@ -320,49 +322,31 @@ export function FormBasicTab({
320
322
  )}
321
323
  />
322
324
 
323
- <Popover>
324
- <FormField
325
- control={form.control}
326
- name="taken_at"
327
- render={({ field }) => (
328
- <FormItem className="flex flex-col">
329
- <FormLabel>{t('transaction-data-table.taken_at')}</FormLabel>
330
- <PopoverTrigger asChild>
331
- <FormControl>
332
- <Button
333
- variant="outline"
334
- className="pl-3 text-left font-normal"
335
- disabled={!hasFormPermission}
336
- >
337
- {field.value ? (
338
- format(
339
- field.value,
340
- locale === 'vi' ? 'dd/MM/yyyy, ppp' : 'PPP',
341
- {
342
- locale: locale === 'vi' ? vi : enUS,
343
- }
344
- )
345
- ) : (
346
- <span>{t('transaction-data-table.taken_at')}</span>
347
- )}
348
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
349
- </Button>
350
- </FormControl>
351
- </PopoverTrigger>
352
- <PopoverContent className="w-auto p-0" align="center">
353
- <Calendar
354
- mode="single"
355
- selected={field.value}
356
- onSelect={field.onChange}
357
- onSubmit={field.onChange}
358
- initialFocus
359
- />
360
- </PopoverContent>
361
- <FormMessage />
362
- </FormItem>
363
- )}
364
- />
365
- </Popover>
325
+ <FormField
326
+ control={form.control}
327
+ name="taken_at"
328
+ render={({ field }) => (
329
+ <FormItem className="flex flex-col">
330
+ <FormLabel>{t('transaction-data-table.taken_at')}</FormLabel>
331
+ <FormControl>
332
+ <OptionalTimePicker
333
+ date={field.value}
334
+ setDate={field.onChange}
335
+ includeTime={includeTakenAtTime}
336
+ setIncludeTime={setIncludeTakenAtTime}
337
+ includeTimeLabel={t('transaction-data-table.include_time')}
338
+ disabled={loading || !hasFormPermission}
339
+ allowClear={false}
340
+ preferences={{
341
+ timezone: timezone || 'auto',
342
+ timeFormat: locale === 'vi' ? '24h' : '12h',
343
+ }}
344
+ />
345
+ </FormControl>
346
+ <FormMessage />
347
+ </FormItem>
348
+ )}
349
+ />
366
350
  </div>
367
351
  );
368
352
  }
@@ -56,6 +56,9 @@ export interface TransactionFormProps {
56
56
  initialMode?: TransactionFormInitialMode;
57
57
  initialTransaction?: TransactionFormInitialTransaction;
58
58
  initialTransfer?: TransactionFormInitialTransfer;
59
+ timezone?: string | null;
60
+ preferInitialWalletSelection?: boolean;
61
+ refreshPageOnFinish?: boolean;
59
62
  permissionRequestUser?: FinancePermissionRequestUser | null;
60
63
  }
61
64