@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.
- package/CHANGELOG.md +43 -0
- package/biome.json +1 -1
- package/package.json +78 -78
- package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.test.tsx +121 -0
- package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.tsx +37 -4
- package/src/components/ui/chat/ai-message-markdown.tsx +6 -1
- package/src/components/ui/chat/chat-external-context-sidebar.tsx +113 -0
- package/src/components/ui/chat/chat-sidebar-conversation-groups.tsx +5 -1
- package/src/components/ui/chat/chat-sidebar-groups.test.ts +33 -0
- package/src/components/ui/chat/chat-sidebar-items.tsx +89 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +29 -1
- package/src/components/ui/chat/chat-sidebar-panel.tsx +4 -3
- package/src/components/ui/chat/chat-sidebar-sections.ts +24 -10
- package/src/components/ui/chat/chat-sidebar.tsx +3 -0
- package/src/components/ui/chat/chat-utils.test.ts +173 -0
- package/src/components/ui/chat/chat-workspace-header.tsx +11 -5
- package/src/components/ui/chat/chat-workspace.tsx +105 -28
- package/src/components/ui/chat/hooks-conversations.ts +9 -1
- package/src/components/ui/chat/hooks-messages.ts +1 -0
- package/src/components/ui/chat/hooks-realtime.ts +93 -4
- package/src/components/ui/chat/message-bubble.tsx +49 -4
- package/src/components/ui/chat/message-composer.tsx +29 -23
- package/src/components/ui/chat/message-list.tsx +5 -3
- package/src/components/ui/chat/message-markdown.tsx +4 -1
- package/src/components/ui/chat/query-keys.ts +7 -1
- package/src/components/ui/chat/selection.ts +1 -1
- package/src/components/ui/chat/utils.ts +126 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +72 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +131 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +55 -18
- package/src/components/ui/custom/workspace-select-helpers.ts +50 -0
- package/src/components/ui/custom/workspace-select.tsx +37 -38
- package/src/components/ui/finance/analytics/analytics-page.tsx +10 -0
- package/src/components/ui/finance/analytics/inventory-provider-summary.tsx +110 -0
- package/src/components/ui/finance/finance-overview-metrics.tsx +49 -0
- package/src/components/ui/finance/transactions/inventory-reconciliation-entry-row.tsx +122 -0
- package/src/components/ui/finance/transactions/inventory-reconciliation-panel.tsx +407 -0
- package/src/components/ui/finance/transactions/transaction-card.tsx +15 -0
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +31 -0
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +26 -4
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +22 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +8 -0
- package/src/components/ui/finance/wallets/walletId/inventory-wallet-contribution.tsx +84 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +9 -0
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +27 -10
- package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +90 -26
- package/src/components/ui/legacy/meet/form.tsx +25 -1
- package/src/components/ui/legacy/meet/lib/meet-ics.test.ts +40 -0
- package/src/components/ui/legacy/meet/lib/meet-ics.ts +50 -0
- package/src/components/ui/legacy/meet/lib/meet-insights.test.ts +76 -0
- package/src/components/ui/legacy/meet/lib/meet-insights.ts +219 -0
- package/src/components/ui/legacy/meet/lib/meet-timezone.test.ts +53 -0
- package/src/components/ui/legacy/meet/lib/meet-timezone.ts +45 -0
- package/src/components/ui/legacy/meet/page.tsx +1 -0
- package/src/components/ui/legacy/meet/planId/agenda-details.tsx +46 -23
- package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +24 -197
- package/src/components/ui/legacy/meet/planId/date-planner.tsx +3 -28
- package/src/components/ui/legacy/meet/planId/meet-insights-panel.tsx +135 -0
- package/src/components/ui/legacy/meet/planId/meet-plan-live-root.tsx +41 -0
- package/src/components/ui/legacy/meet/planId/meet-query.ts +26 -0
- package/src/components/ui/legacy/meet/planId/meet-suggestions-panel.tsx +463 -0
- package/src/components/ui/legacy/meet/planId/page.tsx +58 -18
- package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +154 -140
- package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +88 -56
- package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +3 -5
- package/src/components/ui/legacy/meet/planId/unified-availability.tsx +36 -13
- package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +3 -24
- package/src/components/ui/legacy/meet/plans-grid.tsx +1 -1
- package/src/components/ui/legacy/meet/plans-list-view.tsx +1 -1
- package/src/components/ui/nuqs-adapter.tsx +1 -0
- 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
|
-
|
|
338
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
}
|