@tuturuuu/ui 0.23.0 → 0.25.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 (72) hide show
  1. package/CHANGELOG.md +43 -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-external-context-sidebar.tsx +113 -0
  8. package/src/components/ui/chat/chat-sidebar-conversation-groups.tsx +5 -1
  9. package/src/components/ui/chat/chat-sidebar-groups.test.ts +33 -0
  10. package/src/components/ui/chat/chat-sidebar-items.tsx +89 -5
  11. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +29 -1
  12. package/src/components/ui/chat/chat-sidebar-panel.tsx +4 -3
  13. package/src/components/ui/chat/chat-sidebar-sections.ts +24 -10
  14. package/src/components/ui/chat/chat-sidebar.tsx +3 -0
  15. package/src/components/ui/chat/chat-utils.test.ts +173 -0
  16. package/src/components/ui/chat/chat-workspace-header.tsx +11 -5
  17. package/src/components/ui/chat/chat-workspace.tsx +105 -28
  18. package/src/components/ui/chat/hooks-conversations.ts +9 -1
  19. package/src/components/ui/chat/hooks-messages.ts +1 -0
  20. package/src/components/ui/chat/hooks-realtime.ts +93 -4
  21. package/src/components/ui/chat/message-bubble.tsx +49 -4
  22. package/src/components/ui/chat/message-composer.tsx +29 -23
  23. package/src/components/ui/chat/message-list.tsx +5 -3
  24. package/src/components/ui/chat/message-markdown.tsx +4 -1
  25. package/src/components/ui/chat/query-keys.ts +7 -1
  26. package/src/components/ui/chat/selection.ts +1 -1
  27. package/src/components/ui/chat/utils.ts +126 -3
  28. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +72 -0
  29. package/src/components/ui/custom/onboarding-settings-panel.tsx +131 -0
  30. package/src/components/ui/custom/settings-dialog-shell.tsx +55 -18
  31. package/src/components/ui/custom/workspace-select-helpers.ts +50 -0
  32. package/src/components/ui/custom/workspace-select.tsx +37 -38
  33. package/src/components/ui/finance/analytics/analytics-page.tsx +10 -0
  34. package/src/components/ui/finance/analytics/inventory-provider-summary.tsx +110 -0
  35. package/src/components/ui/finance/finance-overview-metrics.tsx +49 -0
  36. package/src/components/ui/finance/transactions/inventory-reconciliation-entry-row.tsx +122 -0
  37. package/src/components/ui/finance/transactions/inventory-reconciliation-panel.tsx +407 -0
  38. package/src/components/ui/finance/transactions/transaction-card.tsx +15 -0
  39. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +31 -0
  40. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +26 -4
  41. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +22 -0
  42. package/src/components/ui/finance/transactions/transactions-page.tsx +8 -0
  43. package/src/components/ui/finance/wallets/walletId/inventory-wallet-contribution.tsx +84 -0
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +4 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +9 -0
  46. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +27 -10
  47. package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +90 -26
  48. package/src/components/ui/legacy/meet/form.tsx +25 -1
  49. package/src/components/ui/legacy/meet/lib/meet-ics.test.ts +40 -0
  50. package/src/components/ui/legacy/meet/lib/meet-ics.ts +50 -0
  51. package/src/components/ui/legacy/meet/lib/meet-insights.test.ts +76 -0
  52. package/src/components/ui/legacy/meet/lib/meet-insights.ts +219 -0
  53. package/src/components/ui/legacy/meet/lib/meet-timezone.test.ts +53 -0
  54. package/src/components/ui/legacy/meet/lib/meet-timezone.ts +45 -0
  55. package/src/components/ui/legacy/meet/page.tsx +1 -0
  56. package/src/components/ui/legacy/meet/planId/agenda-details.tsx +46 -23
  57. package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +24 -197
  58. package/src/components/ui/legacy/meet/planId/date-planner.tsx +3 -28
  59. package/src/components/ui/legacy/meet/planId/meet-insights-panel.tsx +135 -0
  60. package/src/components/ui/legacy/meet/planId/meet-plan-live-root.tsx +41 -0
  61. package/src/components/ui/legacy/meet/planId/meet-query.ts +26 -0
  62. package/src/components/ui/legacy/meet/planId/meet-suggestions-panel.tsx +463 -0
  63. package/src/components/ui/legacy/meet/planId/page.tsx +58 -18
  64. package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +154 -140
  65. package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +88 -56
  66. package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +3 -5
  67. package/src/components/ui/legacy/meet/planId/unified-availability.tsx +36 -13
  68. package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +3 -24
  69. package/src/components/ui/legacy/meet/plans-grid.tsx +1 -1
  70. package/src/components/ui/legacy/meet/plans-list-view.tsx +1 -1
  71. package/src/components/ui/nuqs-adapter.tsx +1 -0
  72. package/src/hooks/time-blocking-provider.tsx +127 -211
@@ -29,7 +29,7 @@ import { workspaceHandleSchema } from '@tuturuuu/utils/workspace-handle';
29
29
  import { WORKSPACE_LIMIT_ERROR_CODE } from '@tuturuuu/utils/workspace-limits';
30
30
  import Image from 'next/image';
31
31
  import { usePathname, useRouter } from 'next/navigation';
32
- import { useTranslations } from 'next-intl';
32
+ import { useLocale, useTranslations } from 'next-intl';
33
33
  import type { ReactNode } from 'react';
34
34
  import { useState } from 'react';
35
35
  import { toast } from 'sonner';
@@ -69,8 +69,10 @@ import { Input } from '../input';
69
69
  import { Popover, PopoverContent, PopoverTrigger } from '../popover';
70
70
  import { TUTURUUU_LOGO_URL } from './tuturuuu-logo';
71
71
  import {
72
+ buildWorkspaceSetupHandoffUrl,
72
73
  mergeWorkspaceSelectWorkspaces,
73
74
  normalizeWorkspaceSwitchPath,
75
+ resolveWorkspaceAvatarUrl,
74
76
  } from './workspace-select-helpers';
75
77
  import { useOpenWorkspaceSelectWhenRevealed } from './workspace-select-reveal';
76
78
 
@@ -82,13 +84,6 @@ const JoinWorkspaceByHandleFormSchema = z.object({
82
84
  handle: workspaceHandleSchema,
83
85
  });
84
86
 
85
- function resolveWorkspaceAvatarUrl(
86
- avatarUrl: string | null | undefined,
87
- fallbackLogoUrl: string
88
- ) {
89
- return avatarUrl === TUTURUUU_LOGO_URL ? fallbackLogoUrl : avatarUrl;
90
- }
91
-
92
87
  function WorkspaceIcon({
93
88
  name,
94
89
  avatarUrl,
@@ -100,10 +95,7 @@ function WorkspaceIcon({
100
95
  className?: string;
101
96
  fallbackLogoUrl?: string;
102
97
  }) {
103
- const resolvedAvatarUrl = resolveWorkspaceAvatarUrl(
104
- avatarUrl,
105
- fallbackLogoUrl
106
- );
98
+ const resolvedAvatarUrl = resolveWorkspaceAvatarUrl(avatarUrl);
107
99
  const shouldSkipFallbackOptimization = /^https?:\/\//u.test(fallbackLogoUrl);
108
100
 
109
101
  return (
@@ -159,6 +151,7 @@ export function WorkspaceSelect({
159
151
  resolveNextPathname,
160
152
  triggerClassName,
161
153
  popoverModal = false,
154
+ platformWorkspaceSetupUrl,
162
155
  }: {
163
156
  wsId: string;
164
157
  hideLeading?: boolean;
@@ -177,8 +170,11 @@ export function WorkspaceSelect({
177
170
  triggerClassName?: string;
178
171
  /** Keep the picker interactive and scrollable when rendered inside a modal. */
179
172
  popoverModal?: boolean;
173
+ /** Platform origin used to prepare a newly created satellite workspace. */
174
+ platformWorkspaceSetupUrl?: string;
180
175
  }) {
181
176
  const t = useTranslations();
177
+ const locale = useLocale();
182
178
  const router = useRouter();
183
179
  const pathname = usePathname();
184
180
  const queryClient = useQueryClient();
@@ -280,11 +276,26 @@ export function WorkspaceSelect({
280
276
 
281
277
  try {
282
278
  const { id } = await createTeamWorkspace(formData);
279
+ const workspaceLandingPath = getWorkspaceLandingPath(id);
283
280
  form.reset();
284
- router.push(getWorkspaceLandingPath(id));
285
- router.refresh();
286
281
  setShowNewWorkspaceDialog(false);
287
282
  setOpen(false);
283
+
284
+ if (platformWorkspaceSetupUrl) {
285
+ window.location.assign(
286
+ buildWorkspaceSetupHandoffUrl({
287
+ locale,
288
+ platformUrl: platformWorkspaceSetupUrl,
289
+ returnOrigin: window.location.origin,
290
+ returnPath: workspaceLandingPath,
291
+ workspaceId: id,
292
+ })
293
+ );
294
+ return;
295
+ }
296
+
297
+ router.push(workspaceLandingPath);
298
+ router.refresh();
288
299
  } catch (error) {
289
300
  console.error('Error creating workspace:', error);
290
301
  if (
@@ -333,11 +344,9 @@ export function WorkspaceSelect({
333
344
  id: rootWorkspace.id,
334
345
  label: rootWorkspace.name || t('common.root'),
335
346
  value: ROOT_WORKSPACE_ID,
336
- avatarUrl:
337
- resolveWorkspaceAvatarUrl(
338
- rootWorkspace.avatar_url,
339
- fallbackLogoUrl
340
- ) || fallbackLogoUrl,
347
+ avatarUrl: resolveWorkspaceAvatarUrl(rootWorkspace.avatar_url, {
348
+ rootWorkspaceLogoUrl: TUTURUUU_LOGO_URL,
349
+ }),
341
350
  tier: rootWorkspace.tier as
342
351
  | 'FREE'
343
352
  | 'PLUS'
@@ -355,10 +364,7 @@ export function WorkspaceSelect({
355
364
  id: personalWorkspace.id,
356
365
  label: personalWorkspace.name || 'Personal',
357
366
  value: PERSONAL_WORKSPACE_SLUG,
358
- avatarUrl: resolveWorkspaceAvatarUrl(
359
- personalWorkspace.avatar_url,
360
- fallbackLogoUrl
361
- ),
367
+ avatarUrl: resolveWorkspaceAvatarUrl(personalWorkspace.avatar_url),
362
368
  tier: personalWorkspace.tier as
363
369
  | 'FREE'
364
370
  | 'PLUS'
@@ -380,10 +386,7 @@ export function WorkspaceSelect({
380
386
  }),
381
387
  // Signal creator-owned workspaces for UI
382
388
  isCreator: workspace?.created_by_me === true,
383
- avatarUrl: resolveWorkspaceAvatarUrl(
384
- workspace.avatar_url,
385
- fallbackLogoUrl
386
- ),
389
+ avatarUrl: resolveWorkspaceAvatarUrl(workspace.avatar_url),
387
390
  tier: workspace.tier || null,
388
391
  })
389
392
  ),
@@ -398,10 +401,7 @@ export function WorkspaceSelect({
398
401
  personal: workspace?.personal,
399
402
  }),
400
403
  accessType: 'guest' as const,
401
- avatarUrl: resolveWorkspaceAvatarUrl(
402
- workspace.avatar_url,
403
- fallbackLogoUrl
404
- ),
404
+ avatarUrl: resolveWorkspaceAvatarUrl(workspace.avatar_url),
405
405
  guestBoardCount: workspace.guest_board_count ?? 0,
406
406
  guestLandingPath: workspace.guest_landing_path ?? '/tasks/boards',
407
407
  guestProducts: workspace.guest_products ?? ['tasks'],
@@ -579,13 +579,12 @@ export function WorkspaceSelect({
579
579
  fallbackLogoUrl={fallbackLogoUrl}
580
580
  name={workspace?.name}
581
581
  avatarUrl={
582
- resolveWorkspaceAvatarUrl(
583
- workspace?.avatar_url,
584
- fallbackLogoUrl
585
- ) ||
586
- (workspace?.id === ROOT_WORKSPACE_ID
587
- ? fallbackLogoUrl
588
- : undefined)
582
+ resolveWorkspaceAvatarUrl(workspace?.avatar_url, {
583
+ rootWorkspaceLogoUrl:
584
+ workspace?.id === ROOT_WORKSPACE_ID
585
+ ? TUTURUUU_LOGO_URL
586
+ : undefined,
587
+ }) ?? undefined
589
588
  }
590
589
  />
591
590
  <div
@@ -18,13 +18,16 @@ import {
18
18
  import { AnalyticsDateControls } from './analytics-date-controls';
19
19
  import { BalanceTrendChart } from './balance-trend-chart';
20
20
  import { IncomeExpenseChart } from './income-expense-chart';
21
+ import { InventoryProviderSummary } from './inventory-provider-summary';
21
22
 
22
23
  interface AnalyticsPageProps {
24
+ canManageFinance?: boolean;
23
25
  wsId: string;
24
26
  currency?: string;
25
27
  }
26
28
 
27
29
  export default function AnalyticsPage({
30
+ canManageFinance = false,
28
31
  wsId,
29
32
  currency = 'USD',
30
33
  }: AnalyticsPageProps) {
@@ -102,6 +105,13 @@ export default function AnalyticsPage({
102
105
 
103
106
  {/* Charts Grid */}
104
107
  <div className="grid gap-6 lg:grid-cols-2">
108
+ <InventoryProviderSummary
109
+ canManageFinance={canManageFinance}
110
+ currency={currency}
111
+ endDate={filters.apiDateRange.endDate}
112
+ startDate={filters.apiDateRange.startDate}
113
+ wsId={wsId}
114
+ />
105
115
  {/* Income vs Expense Chart */}
106
116
  {showDailyChart ? (
107
117
  <IncomeExpenseChart
@@ -0,0 +1,110 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { listInventoryFinanceEntries } from '@tuturuuu/internal-api';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
6
+ import { Skeleton } from '@tuturuuu/ui/skeleton';
7
+ import { getCurrencyLocale } from '@tuturuuu/utils/currencies';
8
+ import { useTranslations } from 'next-intl';
9
+
10
+ function formatCurrency(value: number, currency: string) {
11
+ return new Intl.NumberFormat(getCurrencyLocale(currency), {
12
+ currency,
13
+ maximumFractionDigits: currency === 'VND' ? 0 : 2,
14
+ style: 'currency',
15
+ }).format(value);
16
+ }
17
+
18
+ export function InventoryProviderSummary({
19
+ canManageFinance,
20
+ currency,
21
+ endDate,
22
+ startDate,
23
+ wsId,
24
+ }: {
25
+ canManageFinance: boolean;
26
+ currency: string;
27
+ endDate?: string | null;
28
+ startDate?: string | null;
29
+ wsId: string;
30
+ }) {
31
+ const t = useTranslations('inventory-finance-reconciliation');
32
+ const { data, isLoading } = useQuery({
33
+ enabled: canManageFinance,
34
+ queryKey: [
35
+ 'inventory-finance-analytics',
36
+ wsId,
37
+ currency,
38
+ startDate,
39
+ endDate,
40
+ ],
41
+ queryFn: () =>
42
+ listInventoryFinanceEntries(wsId, {
43
+ currency,
44
+ endDate: endDate
45
+ ? new Date(`${endDate}T23:59:59.999Z`).toISOString()
46
+ : undefined,
47
+ limit: 1,
48
+ startDate: startDate
49
+ ? new Date(`${startDate}T00:00:00.000Z`).toISOString()
50
+ : undefined,
51
+ }),
52
+ });
53
+ if (!canManageFinance) return null;
54
+ if (isLoading) return <Skeleton className="col-span-full h-48" />;
55
+ const summary = data?.summary;
56
+ const amount = (
57
+ values: Array<{ amount: number; currency: string }> | undefined
58
+ ) => values?.find((item) => item.currency === currency)?.amount ?? 0;
59
+ const rows = [
60
+ { label: t('gross_provider_sales'), value: amount(summary?.grossSales) },
61
+ { label: t('refunds'), value: amount(summary?.refunds) },
62
+ {
63
+ label: t('chargeback_holds'),
64
+ value: amount(summary?.chargebackHolds),
65
+ },
66
+ {
67
+ label: t('chargeback_releases'),
68
+ value: amount(summary?.chargebackReleases),
69
+ },
70
+ { label: t('net_provider_sales'), value: amount(summary?.netSales) },
71
+ ];
72
+
73
+ return (
74
+ <Card className="col-span-full">
75
+ <CardHeader>
76
+ <CardTitle>{t('analytics_title')}</CardTitle>
77
+ <p className="text-muted-foreground text-sm">
78
+ {t('analytics_description')}
79
+ </p>
80
+ </CardHeader>
81
+ <CardContent>
82
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
83
+ {rows.map((row) => (
84
+ <div className="rounded-xl border bg-muted/20 p-4" key={row.label}>
85
+ <p className="text-muted-foreground text-xs">{row.label}</p>
86
+ <p className="mt-1 font-semibold text-lg">
87
+ {formatCurrency(row.value, currency)}
88
+ </p>
89
+ </div>
90
+ ))}
91
+ </div>
92
+ <div className="mt-4 grid gap-3 md:grid-cols-3">
93
+ {(summary?.providers ?? []).map((provider) => (
94
+ <div className="rounded-xl border p-4" key={provider.provider}>
95
+ <p className="font-medium">
96
+ {t(`provider_${provider.provider}`)}
97
+ </p>
98
+ <p className="mt-2 text-muted-foreground text-sm">
99
+ {t('net_provider_sales')}{' '}
100
+ <span className="font-medium text-foreground">
101
+ {formatCurrency(amount(provider.netSales), currency)}
102
+ </span>
103
+ </p>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </CardContent>
108
+ </Card>
109
+ );
110
+ }
@@ -3,6 +3,7 @@ import {
3
3
  ArrowDownRight,
4
4
  ArrowRightLeft,
5
5
  ArrowUpRight,
6
+ CircleAlert,
6
7
  FileText,
7
8
  FolderTree,
8
9
  Wallet2,
@@ -55,6 +56,14 @@ export async function FinanceOverviewMetrics({
55
56
  );
56
57
 
57
58
  const transactionsHref = `/${wsId}${financePrefix}/transactions`;
59
+ const reconciliationHref = `${transactionsHref}?reconciliation=needs-wallet`;
60
+ const canManageFinance = permissions.containsPermission('manage_finance');
61
+ const currencyAmount = (
62
+ values: Array<{ amount: number; currency: string }>
63
+ ) => values.find((item) => item.currency === currency)?.amount ?? 0;
64
+ const pending = metrics.inventoryPending.find(
65
+ (item) => item.currency === currency
66
+ );
58
67
 
59
68
  return (
60
69
  <>
@@ -115,6 +124,46 @@ export async function FinanceOverviewMetrics({
115
124
  href={transactionsHref}
116
125
  icon={<Activity className="h-5 w-5" />}
117
126
  />
127
+ {canManageFinance && (
128
+ <>
129
+ <StatisticCard
130
+ title={t('inventory-finance-reconciliation.pending_metric')}
131
+ value={t('inventory-finance-reconciliation.pending_metric_value', {
132
+ amount: new Intl.NumberFormat(
133
+ currency === 'VND' ? 'vi-VN' : 'en-US',
134
+ { currency, style: 'currency' }
135
+ ).format(pending?.amount ?? 0),
136
+ count: pending?.count ?? 0,
137
+ })}
138
+ href={reconciliationHref}
139
+ icon={<CircleAlert className="h-5 w-5" />}
140
+ />
141
+ <StatisticCard
142
+ title={t('inventory-finance-reconciliation.gross_provider_sales')}
143
+ value={currencyAmount(metrics.inventoryReconciliation.grossSales)}
144
+ href={reconciliationHref}
145
+ icon={<ArrowUpRight className="h-5 w-5" />}
146
+ currency={currency}
147
+ />
148
+ <StatisticCard
149
+ title={t('inventory-finance-reconciliation.refunds_chargebacks')}
150
+ value={
151
+ currencyAmount(metrics.inventoryReconciliation.refunds) +
152
+ currencyAmount(metrics.inventoryReconciliation.chargebackHolds)
153
+ }
154
+ href={reconciliationHref}
155
+ icon={<ArrowDownRight className="h-5 w-5" />}
156
+ currency={currency}
157
+ />
158
+ <StatisticCard
159
+ title={t('inventory-finance-reconciliation.net_provider_sales')}
160
+ value={currencyAmount(metrics.inventoryReconciliation.netSales)}
161
+ href={reconciliationHref}
162
+ icon={<ArrowRightLeft className="h-5 w-5" />}
163
+ currency={currency}
164
+ />
165
+ </>
166
+ )}
118
167
  </>
119
168
  );
120
169
  }
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+
3
+ import type {
4
+ InventoryFinanceEntry,
5
+ InventoryFinanceProvider,
6
+ } from '@tuturuuu/internal-api';
7
+ import { Badge } from '@tuturuuu/ui/badge';
8
+ import { Button } from '@tuturuuu/ui/button';
9
+ import { Checkbox } from '@tuturuuu/ui/checkbox';
10
+ import { getCurrencyLocale } from '@tuturuuu/utils/currencies';
11
+ import { minorToMajor } from '@tuturuuu/utils/money';
12
+ import Link from 'next/link';
13
+ import { useTranslations } from 'next-intl';
14
+
15
+ export function formatInventoryReconciliationAmount(
16
+ amount: number,
17
+ currency: string
18
+ ) {
19
+ return new Intl.NumberFormat(getCurrencyLocale(currency), {
20
+ currency,
21
+ maximumFractionDigits: currency === 'VND' ? 0 : 2,
22
+ style: 'currency',
23
+ }).format(amount);
24
+ }
25
+
26
+ export function inventoryProviderTranslationKey(
27
+ provider: InventoryFinanceProvider
28
+ ) {
29
+ if (provider === 'cash') return 'provider_cash' as const;
30
+ if (provider === 'polar') return 'provider_polar' as const;
31
+ if (provider === 'square_pos') return 'provider_square_pos' as const;
32
+ return 'provider_square_terminal' as const;
33
+ }
34
+
35
+ export function InventoryReconciliationEntryRow({
36
+ checked,
37
+ entry,
38
+ onCheckedChange,
39
+ }: {
40
+ checked: boolean;
41
+ entry: InventoryFinanceEntry;
42
+ onCheckedChange: (checked: boolean) => void;
43
+ }) {
44
+ const t = useTranslations('inventory-finance-reconciliation');
45
+ return (
46
+ <div className="grid gap-3 border-b p-4 last:border-b-0 md:grid-cols-[auto_1.2fr_1fr_1fr_auto] md:items-center">
47
+ <Checkbox
48
+ checked={checked}
49
+ onCheckedChange={(value) => onCheckedChange(value === true)}
50
+ aria-label={t('select_entry')}
51
+ />
52
+ <div className="min-w-0">
53
+ <div className="flex flex-wrap items-center gap-2">
54
+ <Badge variant="outline">
55
+ {t(inventoryProviderTranslationKey(entry.provider))}
56
+ </Badge>
57
+ <Badge variant="secondary">{t(`kind_${entry.kind}`)}</Badge>
58
+ {entry.synchronizationError && (
59
+ <Badge variant="destructive">{t('mapping_error')}</Badge>
60
+ )}
61
+ </div>
62
+ <p className="mt-1 truncate font-medium text-sm">
63
+ {entry.customer?.name ||
64
+ entry.customer?.email ||
65
+ t('unknown_customer')}
66
+ </p>
67
+ {entry.allocations.length > 0 ? (
68
+ <details className="mt-2 text-xs">
69
+ <summary className="cursor-pointer text-muted-foreground">
70
+ {t('allocation_breakdown')}
71
+ </summary>
72
+ <div className="mt-2 grid gap-1">
73
+ {entry.allocations.map((allocation) => (
74
+ <div
75
+ className="flex items-center justify-between gap-3"
76
+ key={allocation.lineId}
77
+ >
78
+ <span className="min-w-0 truncate">
79
+ {allocation.quantity}× {allocation.title}
80
+ </span>
81
+ <span className="shrink-0 font-medium tabular-nums">
82
+ {formatInventoryReconciliationAmount(
83
+ minorToMajor(
84
+ allocation.recognizedRevenueAmount,
85
+ entry.currency
86
+ ),
87
+ entry.currency
88
+ )}
89
+ </span>
90
+ </div>
91
+ ))}
92
+ </div>
93
+ </details>
94
+ ) : null}
95
+ </div>
96
+ <div className="min-w-0 text-sm">
97
+ <p className="truncate font-mono text-xs">
98
+ {entry.providerReferenceId}
99
+ </p>
100
+ <p className="text-muted-foreground text-xs">
101
+ {new Intl.DateTimeFormat(undefined, {
102
+ dateStyle: 'medium',
103
+ timeStyle: 'short',
104
+ }).format(new Date(entry.occurredAt))}
105
+ </p>
106
+ </div>
107
+ <div className="text-sm">
108
+ <p className="font-semibold">
109
+ {formatInventoryReconciliationAmount(entry.amount, entry.currency)}
110
+ </p>
111
+ <p className="text-muted-foreground text-xs">
112
+ {entry.wallet?.name ?? t('needs_wallet')}
113
+ </p>
114
+ </div>
115
+ <div className="flex justify-end">
116
+ <Button asChild size="sm" variant="ghost">
117
+ <Link href={entry.source.inventoryHref}>{t('inventory_sale')}</Link>
118
+ </Button>
119
+ </div>
120
+ </div>
121
+ );
122
+ }