@tuturuuu/ui 0.22.0 → 0.24.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 (66) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/biome.json +1 -1
  3. package/package.json +78 -78
  4. package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.test.tsx +121 -0
  5. package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.tsx +37 -4
  6. package/src/components/ui/chat/ai-message-markdown.tsx +6 -1
  7. package/src/components/ui/chat/chat-sidebar-groups.test.ts +33 -0
  8. package/src/components/ui/chat/chat-sidebar-items.tsx +10 -2
  9. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +29 -1
  10. package/src/components/ui/chat/chat-sidebar-panel.tsx +4 -3
  11. package/src/components/ui/chat/chat-sidebar-sections.ts +24 -10
  12. package/src/components/ui/chat/chat-utils.test.ts +107 -0
  13. package/src/components/ui/chat/chat-workspace-header.tsx +11 -5
  14. package/src/components/ui/chat/chat-workspace.tsx +66 -24
  15. package/src/components/ui/chat/hooks-conversations.ts +9 -1
  16. package/src/components/ui/chat/hooks-messages.ts +1 -0
  17. package/src/components/ui/chat/message-bubble.tsx +49 -4
  18. package/src/components/ui/chat/message-composer.tsx +29 -23
  19. package/src/components/ui/chat/message-markdown.tsx +4 -1
  20. package/src/components/ui/chat/query-keys.ts +7 -1
  21. package/src/components/ui/chat/utils.ts +85 -3
  22. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +72 -0
  23. package/src/components/ui/custom/onboarding-settings-panel.tsx +131 -0
  24. package/src/components/ui/custom/settings-dialog-shell.tsx +55 -18
  25. package/src/components/ui/custom/workspace-select-helpers.ts +50 -0
  26. package/src/components/ui/custom/workspace-select.tsx +37 -38
  27. package/src/components/ui/finance/analytics/analytics-page.tsx +10 -0
  28. package/src/components/ui/finance/analytics/inventory-provider-summary.tsx +110 -0
  29. package/src/components/ui/finance/finance-overview-metrics.tsx +49 -0
  30. package/src/components/ui/finance/transactions/inventory-reconciliation-entry-row.tsx +122 -0
  31. package/src/components/ui/finance/transactions/inventory-reconciliation-panel.tsx +407 -0
  32. package/src/components/ui/finance/transactions/transaction-card.tsx +15 -0
  33. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +31 -0
  34. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +26 -4
  35. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +22 -0
  36. package/src/components/ui/finance/transactions/transactions-page.tsx +8 -0
  37. package/src/components/ui/finance/wallets/walletId/inventory-wallet-contribution.tsx +84 -0
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +4 -0
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +9 -0
  40. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +27 -10
  41. package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +90 -26
  42. package/src/components/ui/legacy/meet/form.tsx +25 -1
  43. package/src/components/ui/legacy/meet/lib/meet-ics.test.ts +40 -0
  44. package/src/components/ui/legacy/meet/lib/meet-ics.ts +50 -0
  45. package/src/components/ui/legacy/meet/lib/meet-insights.test.ts +76 -0
  46. package/src/components/ui/legacy/meet/lib/meet-insights.ts +219 -0
  47. package/src/components/ui/legacy/meet/lib/meet-timezone.test.ts +53 -0
  48. package/src/components/ui/legacy/meet/lib/meet-timezone.ts +45 -0
  49. package/src/components/ui/legacy/meet/page.tsx +1 -0
  50. package/src/components/ui/legacy/meet/planId/agenda-details.tsx +46 -23
  51. package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +24 -197
  52. package/src/components/ui/legacy/meet/planId/date-planner.tsx +3 -28
  53. package/src/components/ui/legacy/meet/planId/meet-insights-panel.tsx +135 -0
  54. package/src/components/ui/legacy/meet/planId/meet-plan-live-root.tsx +41 -0
  55. package/src/components/ui/legacy/meet/planId/meet-query.ts +26 -0
  56. package/src/components/ui/legacy/meet/planId/meet-suggestions-panel.tsx +463 -0
  57. package/src/components/ui/legacy/meet/planId/page.tsx +58 -18
  58. package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +154 -140
  59. package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +88 -56
  60. package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +3 -5
  61. package/src/components/ui/legacy/meet/planId/unified-availability.tsx +36 -13
  62. package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +3 -24
  63. package/src/components/ui/legacy/meet/plans-grid.tsx +1 -1
  64. package/src/components/ui/legacy/meet/plans-list-view.tsx +1 -1
  65. package/src/components/ui/nuqs-adapter.tsx +1 -0
  66. package/src/hooks/time-blocking-provider.tsx +127 -211
@@ -0,0 +1,407 @@
1
+ 'use client';
2
+
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { AlertTriangle, Link2, RefreshCw, Unlink } from '@tuturuuu/icons';
5
+ import {
6
+ bulkLinkInventoryFinanceEntries,
7
+ bulkUnlinkInventoryFinanceEntries,
8
+ getInventoryFinanceMappings,
9
+ type InventoryFinanceEntryStatus,
10
+ type InventoryFinanceProvider,
11
+ listInventoryFinanceEntries,
12
+ syncInventoryFinanceProvider,
13
+ } from '@tuturuuu/internal-api';
14
+ import {
15
+ listTransactionCategories,
16
+ listWallets,
17
+ } from '@tuturuuu/internal-api/finance';
18
+ import { Badge } from '@tuturuuu/ui/badge';
19
+ import { Button } from '@tuturuuu/ui/button';
20
+ import { Checkbox } from '@tuturuuu/ui/checkbox';
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@tuturuuu/ui/select';
28
+ import { toast } from '@tuturuuu/ui/sonner';
29
+ import { useTranslations } from 'next-intl';
30
+ import { useMemo, useState } from 'react';
31
+ import {
32
+ formatInventoryReconciliationAmount,
33
+ InventoryReconciliationEntryRow,
34
+ inventoryProviderTranslationKey,
35
+ } from './inventory-reconciliation-entry-row';
36
+
37
+ const ALL = 'all';
38
+ const NONE = 'none';
39
+ const providerValues: Array<InventoryFinanceProvider | typeof ALL> = [
40
+ ALL,
41
+ 'cash',
42
+ 'polar',
43
+ 'square_pos',
44
+ 'square_terminal',
45
+ ];
46
+
47
+ export function InventoryReconciliationPanel({
48
+ defaultCurrency,
49
+ wsId,
50
+ }: {
51
+ defaultCurrency: string;
52
+ wsId: string;
53
+ }) {
54
+ const t = useTranslations('inventory-finance-reconciliation');
55
+ const queryClient = useQueryClient();
56
+ const [provider, setProvider] = useState<
57
+ InventoryFinanceProvider | typeof ALL
58
+ >(ALL);
59
+ const [status, setStatus] = useState<InventoryFinanceEntryStatus>('pending');
60
+ const [currency, setCurrency] = useState(defaultCurrency);
61
+ const [selectedIds, setSelectedIds] = useState<string[]>([]);
62
+ const [walletId, setWalletId] = useState(NONE);
63
+ const [categoryId, setCategoryId] = useState(NONE);
64
+
65
+ const reconciliationQuery = useQuery({
66
+ queryKey: [
67
+ 'inventory-finance-reconciliation',
68
+ wsId,
69
+ provider,
70
+ status,
71
+ currency,
72
+ ],
73
+ queryFn: () =>
74
+ listInventoryFinanceEntries(wsId, {
75
+ currency: currency || undefined,
76
+ limit: 100,
77
+ provider: provider === ALL ? undefined : provider,
78
+ status,
79
+ }),
80
+ });
81
+ const mappingsQuery = useQuery({
82
+ queryKey: ['inventory-finance-mappings', wsId],
83
+ queryFn: () => getInventoryFinanceMappings(wsId),
84
+ });
85
+ const walletsQuery = useQuery({
86
+ queryKey: ['workspace-wallets', wsId],
87
+ queryFn: () => listWallets(wsId),
88
+ });
89
+ const categoriesQuery = useQuery({
90
+ queryKey: ['transaction-categories', wsId],
91
+ queryFn: () => listTransactionCategories(wsId),
92
+ });
93
+ const entries = reconciliationQuery.data?.data ?? [];
94
+ const selectedEntries = entries.filter((entry) =>
95
+ selectedIds.includes(entry.id)
96
+ );
97
+ const selectedCurrencies = new Set(
98
+ selectedEntries.map((entry) => entry.currency)
99
+ );
100
+ const compatibleWallets = (walletsQuery.data ?? []).filter(
101
+ (wallet) =>
102
+ wallet.id &&
103
+ (selectedCurrencies.size !== 1 ||
104
+ selectedCurrencies.has((wallet.currency ?? '').toUpperCase()))
105
+ );
106
+
107
+ const invalidate = async () => {
108
+ setSelectedIds([]);
109
+ await Promise.all([
110
+ queryClient.invalidateQueries({
111
+ queryKey: ['inventory-finance-reconciliation', wsId],
112
+ }),
113
+ queryClient.invalidateQueries({ queryKey: ['transactions'] }),
114
+ queryClient.invalidateQueries({ queryKey: ['workspace-wallets', wsId] }),
115
+ ]);
116
+ };
117
+ const linkMutation = useMutation({
118
+ mutationFn: () =>
119
+ bulkLinkInventoryFinanceEntries(wsId, {
120
+ categoryId: categoryId === NONE ? null : categoryId,
121
+ entryIds: selectedIds,
122
+ walletId,
123
+ }),
124
+ onSuccess: async () => {
125
+ toast.success(t('bulk_link_success'));
126
+ await invalidate();
127
+ },
128
+ onError: () => toast.error(t('bulk_action_error')),
129
+ });
130
+ const unlinkMutation = useMutation({
131
+ mutationFn: () =>
132
+ bulkUnlinkInventoryFinanceEntries(wsId, { entryIds: selectedIds }),
133
+ onSuccess: async () => {
134
+ toast.success(t('bulk_unlink_success'));
135
+ await invalidate();
136
+ },
137
+ onError: () => toast.error(t('bulk_action_error')),
138
+ });
139
+ const applyDefaultsMutation = useMutation({
140
+ mutationFn: async () => {
141
+ const mappings = mappingsQuery.data?.mappings ?? [];
142
+ const groups = new Map<
143
+ string,
144
+ { categoryId: string | null; entryIds: string[]; walletId: string }
145
+ >();
146
+ for (const entry of selectedEntries) {
147
+ const mapping = mappings.find(
148
+ (item) =>
149
+ item.provider === entry.provider &&
150
+ item.currency === entry.currency &&
151
+ item.walletId
152
+ );
153
+ if (!mapping?.walletId) throw new Error('missing-mapping');
154
+ const key = `${mapping.walletId}:${mapping.categoryId ?? ''}`;
155
+ const group = groups.get(key) ?? {
156
+ categoryId: mapping.categoryId,
157
+ entryIds: [],
158
+ walletId: mapping.walletId,
159
+ };
160
+ group.entryIds.push(entry.id);
161
+ groups.set(key, group);
162
+ }
163
+ return Promise.all(
164
+ [...groups.values()].map((group) =>
165
+ bulkLinkInventoryFinanceEntries(wsId, group)
166
+ )
167
+ );
168
+ },
169
+ onSuccess: async () => {
170
+ toast.success(t('apply_defaults_success'));
171
+ await invalidate();
172
+ },
173
+ onError: () => toast.error(t('missing_mapping_error')),
174
+ });
175
+ const syncMutation = useMutation({
176
+ mutationFn: async () => {
177
+ const selectedProvider =
178
+ provider === 'polar' ? 'polar' : provider === ALL ? null : 'square';
179
+ const providers: ReadonlyArray<'polar' | 'square'> = selectedProvider
180
+ ? [selectedProvider]
181
+ : (['polar', 'square'] as const);
182
+ return Promise.all(
183
+ providers.map((item) =>
184
+ syncInventoryFinanceProvider(wsId, {
185
+ environment: 'production',
186
+ limit: 100,
187
+ provider: item,
188
+ })
189
+ )
190
+ );
191
+ },
192
+ onSuccess: async () => {
193
+ toast.success(t('sync_success'));
194
+ await invalidate();
195
+ },
196
+ onError: () => toast.error(t('sync_error')),
197
+ });
198
+
199
+ const pending = reconciliationQuery.data?.summary.pending ?? [];
200
+ const pendingLabel = useMemo(
201
+ () =>
202
+ pending.length
203
+ ? pending
204
+ .map(
205
+ (item) =>
206
+ `${item.count} · ${formatInventoryReconciliationAmount(item.amount, item.currency)}`
207
+ )
208
+ .join(' / ')
209
+ : t('nothing_pending'),
210
+ [pending, t]
211
+ );
212
+ const allSelected =
213
+ entries.length > 0 &&
214
+ entries.every((entry) => selectedIds.includes(entry.id));
215
+ const isMutating =
216
+ linkMutation.isPending ||
217
+ unlinkMutation.isPending ||
218
+ applyDefaultsMutation.isPending;
219
+
220
+ return (
221
+ <section className="mb-4 overflow-hidden rounded-2xl border bg-card shadow-sm">
222
+ <div className="flex flex-col gap-4 border-b bg-muted/30 p-4 lg:flex-row lg:items-center lg:justify-between">
223
+ <div className="flex gap-3">
224
+ <AlertTriangle className="mt-0.5 h-5 w-5 text-dynamic-orange" />
225
+ <div>
226
+ <h2 className="font-semibold">{t('title')}</h2>
227
+ <p className="text-muted-foreground text-sm">{t('description')}</p>
228
+ <p className="mt-1 font-medium text-sm">{pendingLabel}</p>
229
+ </div>
230
+ </div>
231
+ <Button
232
+ disabled={syncMutation.isPending || provider === 'cash'}
233
+ onClick={() => syncMutation.mutate()}
234
+ size="sm"
235
+ variant="outline"
236
+ >
237
+ <RefreshCw
238
+ className={syncMutation.isPending ? 'animate-spin' : undefined}
239
+ />
240
+ {t('sync_provider_history')}
241
+ </Button>
242
+ </div>
243
+
244
+ <div className="flex flex-wrap gap-2 border-b p-3">
245
+ <Select
246
+ value={status}
247
+ onValueChange={(value) => {
248
+ setStatus(value as InventoryFinanceEntryStatus);
249
+ setSelectedIds([]);
250
+ }}
251
+ >
252
+ <SelectTrigger className="w-44">
253
+ <SelectValue />
254
+ </SelectTrigger>
255
+ <SelectContent>
256
+ <SelectItem value="pending">{t('view_needs_wallet')}</SelectItem>
257
+ <SelectItem value="linked">{t('view_linked')}</SelectItem>
258
+ <SelectItem value="error">{t('view_errors')}</SelectItem>
259
+ </SelectContent>
260
+ </Select>
261
+ <Select
262
+ value={provider}
263
+ onValueChange={(value) => {
264
+ setProvider(value as InventoryFinanceProvider | typeof ALL);
265
+ setSelectedIds([]);
266
+ }}
267
+ >
268
+ <SelectTrigger className="w-48">
269
+ <SelectValue />
270
+ </SelectTrigger>
271
+ <SelectContent>
272
+ {providerValues.map((value) => (
273
+ <SelectItem key={value} value={value}>
274
+ {value === ALL
275
+ ? t('all_providers')
276
+ : t(inventoryProviderTranslationKey(value))}
277
+ </SelectItem>
278
+ ))}
279
+ </SelectContent>
280
+ </Select>
281
+ <Select value={currency} onValueChange={setCurrency}>
282
+ <SelectTrigger className="w-28">
283
+ <SelectValue />
284
+ </SelectTrigger>
285
+ <SelectContent>
286
+ {[...new Set([defaultCurrency, 'USD', 'VND'])].map((value) => (
287
+ <SelectItem key={value} value={value}>
288
+ {value}
289
+ </SelectItem>
290
+ ))}
291
+ </SelectContent>
292
+ </Select>
293
+ </div>
294
+
295
+ {entries.length > 0 && (
296
+ <div className="flex items-center gap-2 border-b px-4 py-2 text-sm">
297
+ <Checkbox
298
+ checked={allSelected}
299
+ onCheckedChange={(checked) =>
300
+ setSelectedIds(
301
+ checked === true ? entries.map((entry) => entry.id) : []
302
+ )
303
+ }
304
+ />
305
+ {t('select_all', { count: entries.length })}
306
+ </div>
307
+ )}
308
+ <div>
309
+ {entries.map((entry) => (
310
+ <InventoryReconciliationEntryRow
311
+ checked={selectedIds.includes(entry.id)}
312
+ entry={entry}
313
+ key={entry.id}
314
+ onCheckedChange={(checked) =>
315
+ setSelectedIds((current) =>
316
+ checked
317
+ ? [...new Set([...current, entry.id])]
318
+ : current.filter((id) => id !== entry.id)
319
+ )
320
+ }
321
+ />
322
+ ))}
323
+ {!reconciliationQuery.isLoading && entries.length === 0 && (
324
+ <p className="p-8 text-center text-muted-foreground text-sm">
325
+ {t('empty_view')}
326
+ </p>
327
+ )}
328
+ </div>
329
+
330
+ {selectedIds.length > 0 && (
331
+ <div className="sticky bottom-3 m-3 flex flex-col gap-2 rounded-xl border bg-background/95 p-3 shadow-lg backdrop-blur sm:flex-row sm:items-center">
332
+ <Badge variant="secondary">
333
+ {t('selected_count', { count: selectedIds.length })}
334
+ </Badge>
335
+ {status === 'linked' ? (
336
+ <Button
337
+ disabled={isMutating}
338
+ onClick={() => unlinkMutation.mutate()}
339
+ size="sm"
340
+ variant="destructive"
341
+ >
342
+ <Unlink />
343
+ {t('unlink')}
344
+ </Button>
345
+ ) : (
346
+ <>
347
+ <Select value={walletId} onValueChange={setWalletId}>
348
+ <SelectTrigger className="min-w-48">
349
+ <SelectValue placeholder={t('assign_wallet')} />
350
+ </SelectTrigger>
351
+ <SelectContent>
352
+ <SelectItem disabled value={NONE}>
353
+ {t('assign_wallet')}
354
+ </SelectItem>
355
+ {compatibleWallets.map((wallet) => (
356
+ <SelectItem key={wallet.id} value={wallet.id as string}>
357
+ {wallet.name} · {wallet.currency}
358
+ </SelectItem>
359
+ ))}
360
+ </SelectContent>
361
+ </Select>
362
+ <Select value={categoryId} onValueChange={setCategoryId}>
363
+ <SelectTrigger className="min-w-44">
364
+ <SelectValue placeholder={t('set_category')} />
365
+ </SelectTrigger>
366
+ <SelectContent>
367
+ <SelectItem value={NONE}>{t('uncategorized')}</SelectItem>
368
+ {(categoriesQuery.data ?? [])
369
+ .filter((category) => category.id)
370
+ .map((category) => (
371
+ <SelectItem
372
+ key={category.id}
373
+ value={category.id as string}
374
+ >
375
+ {category.name}
376
+ </SelectItem>
377
+ ))}
378
+ </SelectContent>
379
+ </Select>
380
+ <Button
381
+ disabled={
382
+ isMutating ||
383
+ walletId === NONE ||
384
+ selectedCurrencies.size !== 1
385
+ }
386
+ onClick={() => linkMutation.mutate()}
387
+ size="sm"
388
+ >
389
+ <Link2 />
390
+ {t('assign_or_move')}
391
+ </Button>
392
+ <Button
393
+ disabled={isMutating}
394
+ onClick={() => applyDefaultsMutation.mutate()}
395
+ size="sm"
396
+ variant="outline"
397
+ >
398
+ <RefreshCw />
399
+ {t('apply_defaults')}
400
+ </Button>
401
+ </>
402
+ )}
403
+ </div>
404
+ )}
405
+ </section>
406
+ );
407
+ }
@@ -78,6 +78,7 @@ export function TransactionCard({
78
78
  isDaily = false,
79
79
  }: TransactionCardProps) {
80
80
  const t = useTranslations('workspace-finance-transactions');
81
+ const sourceT = useTranslations('inventory-finance-reconciliation');
81
82
  const commonT = useTranslations('common');
82
83
  const financeHref = useFinanceHref();
83
84
  const [isHovered, setIsHovered] = useState(false);
@@ -439,6 +440,20 @@ export function TransactionCard({
439
440
  {t('confidential')}
440
441
  </Badge>
441
442
  )}
443
+ {transaction.source && (
444
+ <Link
445
+ href={transaction.source.inventoryHref}
446
+ onClick={(event) => event.stopPropagation()}
447
+ >
448
+ <Badge
449
+ variant="outline"
450
+ className="border-dynamic-blue/40 bg-dynamic-blue/10 text-dynamic-blue"
451
+ >
452
+ {sourceT(`provider_${transaction.source.provider}`)} ·{' '}
453
+ {sourceT(`kind_${transaction.source.kind}`)}
454
+ </Badge>
455
+ </Link>
456
+ )}
442
457
  </div>
443
458
 
444
459
  {/* Amount + Actions */}
@@ -122,4 +122,35 @@ describe('TransactionEditDialog', () => {
122
122
  })
123
123
  ).toBeDisabled();
124
124
  });
125
+
126
+ it('locks provider-controlled amount and occurrence controls', async () => {
127
+ renderDialog({
128
+ transaction: {
129
+ amount: 123,
130
+ category_id: 'category-1',
131
+ description: 'Polar sale',
132
+ id: 'transaction-1',
133
+ report_opt_in: true,
134
+ source: {
135
+ checkoutId: 'checkout-1',
136
+ entryId: 'entry-1',
137
+ inventoryHref: 'https://inventory.tuturuuu.com/ws-1/sales',
138
+ kind: 'sale',
139
+ provider: 'polar',
140
+ providerReferenceId: 'order-1',
141
+ reconciliationHref: '/ws-1/transactions?reconciliation=entry-1',
142
+ type: 'inventory',
143
+ },
144
+ taken_at: '2026-05-27T00:00:00.000Z',
145
+ wallet_id: 'wallet-1',
146
+ },
147
+ });
148
+
149
+ expect(await screen.findByText('provider_fields_locked')).toBeVisible();
150
+ expect(
151
+ screen.getByRole('switch', {
152
+ name: 'transaction-data-table.include_time',
153
+ })
154
+ ).toBeDisabled();
155
+ });
125
156
  });
@@ -168,6 +168,7 @@ export function TransactionEditDialog({
168
168
  timezone,
169
169
  }: TransactionEditDialogProps) {
170
170
  const t = useTranslations();
171
+ const sourceT = useTranslations('inventory-finance-reconciliation');
171
172
  const locale = useLocale();
172
173
  const router = useRouter();
173
174
  const queryClient = useQueryClient();
@@ -197,6 +198,7 @@ export function TransactionEditDialog({
197
198
  // Disable all fields if user lacks permission to view/update confidential transaction
198
199
  const isDisabled =
199
200
  hasAnyConfidentialField && !canUpdateConfidentialTransactions;
201
+ const isProviderLinked = Boolean(transaction.source);
200
202
 
201
203
  // Check if user can delete this specific transaction
202
204
  const canDeleteThisTransaction =
@@ -345,15 +347,19 @@ export function TransactionEditDialog({
345
347
 
346
348
  const payload = {
347
349
  description,
348
- amount: finalAmount,
349
350
  origin_wallet_id: walletId,
350
351
  category_id: categoryId,
351
- taken_at: takenAt?.toISOString(),
352
352
  report_opt_in: reportOptIn,
353
353
  tag_ids: selectedTagIds,
354
354
  is_amount_confidential: isAmountConfidential,
355
355
  is_description_confidential: isDescriptionConfidential,
356
356
  is_category_confidential: isCategoryConfidential,
357
+ ...(isProviderLinked
358
+ ? {}
359
+ : {
360
+ amount: finalAmount,
361
+ taken_at: takenAt?.toISOString(),
362
+ }),
357
363
  };
358
364
 
359
365
  if (transaction?.id) {
@@ -388,6 +394,7 @@ export function TransactionEditDialog({
388
394
  canSave,
389
395
  canUpdateTransactions,
390
396
  isExpense,
397
+ isProviderLinked,
391
398
  amount,
392
399
  transaction,
393
400
  wsId,
@@ -519,6 +526,12 @@ export function TransactionEditDialog({
519
526
  {t('workspace-finance-transactions.confidential')}
520
527
  </Badge>
521
528
  )}
529
+ {transaction.source && (
530
+ <Badge variant="outline">
531
+ {sourceT(`provider_${transaction.source.provider}`)} ·{' '}
532
+ {sourceT(`kind_${transaction.source.kind}`)}
533
+ </Badge>
534
+ )}
522
535
  </div>
523
536
  <DialogDescription className="text-xs">
524
537
  {canUpdateTransactions
@@ -634,7 +647,9 @@ export function TransactionEditDialog({
634
647
  value={amount}
635
648
  onChange={setAmount}
636
649
  placeholder="0"
637
- disabled={isDisabled || !canUpdateTransactions}
650
+ disabled={
651
+ isDisabled || !canUpdateTransactions || isProviderLinked
652
+ }
638
653
  locale={locale}
639
654
  currencySuffix={selectedWalletCurrency}
640
655
  className={cn(
@@ -667,6 +682,11 @@ export function TransactionEditDialog({
667
682
  }).format(isExpense ? -amount : amount)}
668
683
  </span>
669
684
  </div>
685
+ {isProviderLinked && (
686
+ <p className="text-muted-foreground text-xs">
687
+ {sourceT('provider_fields_locked')}
688
+ </p>
689
+ )}
670
690
  </div>
671
691
  )}
672
692
 
@@ -760,7 +780,9 @@ export function TransactionEditDialog({
760
780
  includeTimeLabel={t('transaction-data-table.include_time')}
761
781
  allowClear={false}
762
782
  showFooterControls={true}
763
- disabled={isDisabled || !canUpdateTransactions}
783
+ disabled={
784
+ isDisabled || !canUpdateTransactions || isProviderLinked
785
+ }
764
786
  preferences={{
765
787
  timezone: timezone || 'auto',
766
788
  timeFormat: locale === 'vi' ? '24h' : '12h',
@@ -45,6 +45,7 @@ export function TransactionDetailsClientPage({
45
45
  timezone,
46
46
  }: Props) {
47
47
  const t = useTranslations();
48
+ const sourceT = useTranslations('inventory-finance-reconciliation');
48
49
  const financeHref = useFinanceHref();
49
50
  const [isEditOpen, setIsEditOpen] = useState(false);
50
51
  const { isConfidential: areNumbersHidden } =
@@ -89,6 +90,12 @@ export function TransactionDetailsClientPage({
89
90
  {transaction.category && (
90
91
  <Badge variant="outline">{transaction.category}</Badge>
91
92
  )}
93
+ {transaction.source && (
94
+ <Badge variant="outline">
95
+ {sourceT(`provider_${transaction.source.provider}`)} ·{' '}
96
+ {sourceT(`kind_${transaction.source.kind}`)}
97
+ </Badge>
98
+ )}
92
99
  </div>
93
100
  <p className="text-muted-foreground text-sm">
94
101
  {transaction.description || t('ws-transactions.no_description')}
@@ -152,6 +159,21 @@ export function TransactionDetailsClientPage({
152
159
  </div>
153
160
 
154
161
  <div className="space-y-3 p-4">
162
+ {transaction.source && (
163
+ <>
164
+ <DetailItem
165
+ icon={<Tag className="h-5 w-5 text-primary" />}
166
+ label={sourceT('provider_source')}
167
+ value={
168
+ <span className="font-mono text-xs">
169
+ {transaction.source.providerReferenceId}
170
+ </span>
171
+ }
172
+ href={transaction.source.inventoryHref}
173
+ />
174
+ <Separator />
175
+ </>
176
+ )}
155
177
  <DetailItem
156
178
  icon={<Wallet className="h-5 w-5 text-primary" />}
157
179
  label={t('transaction-data-table.wallet')}
@@ -4,6 +4,7 @@ import {
4
4
  FinancePermissionWarningDialog,
5
5
  } from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
6
6
  import ExportDialogContent from '@tuturuuu/ui/finance/transactions/export-dialog-content';
7
+ import { InventoryReconciliationPanel } from '@tuturuuu/ui/finance/transactions/inventory-reconciliation-panel';
7
8
  import { TransactionsCreateSummary } from '@tuturuuu/ui/finance/transactions/transactions-create-summary';
8
9
  import { TransactionsInfinitePage } from '@tuturuuu/ui/finance/transactions/transactions-infinite-page';
9
10
  import { Separator } from '@tuturuuu/ui/separator';
@@ -55,6 +56,7 @@ export default async function TransactionsPage({
55
56
  const workspaceCurrency = resolveSupportedCurrency(resolvedCurrency);
56
57
 
57
58
  const canViewTransactions = containsPermission('view_transactions');
59
+ const canManageFinance = containsPermission('manage_finance');
58
60
  const canExportFinanceData = containsPermission('export_finance_data');
59
61
  const canCreateTransactions = containsPermission('create_transactions');
60
62
  const canUpdateTransactions = containsPermission('update_transactions');
@@ -99,6 +101,12 @@ export default async function TransactionsPage({
99
101
 
100
102
  return (
101
103
  <>
104
+ {canManageFinance && (
105
+ <InventoryReconciliationPanel
106
+ defaultCurrency={workspaceCurrency}
107
+ wsId={wsId}
108
+ />
109
+ )}
102
110
  <TransactionsCreateSummary
103
111
  pluralTitle={t('ws-transactions.plural')}
104
112
  singularTitle={t('ws-transactions.singular')}