@voyantjs/distribution-react 0.105.2 → 0.107.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/README.md +56 -10
- package/dist/components/booking-link-detail-page.d.ts +10 -0
- package/dist/components/booking-link-detail-page.d.ts.map +1 -0
- package/dist/components/booking-link-detail-page.js +51 -0
- package/dist/components/channel-detail-page.d.ts +12 -0
- package/dist/components/channel-detail-page.d.ts.map +1 -0
- package/dist/components/channel-detail-page.js +41 -0
- package/dist/components/channel-sync-page.d.ts +8 -0
- package/dist/components/channel-sync-page.d.ts.map +1 -0
- package/dist/components/channel-sync-page.js +257 -0
- package/dist/components/channels-page.d.ts +6 -0
- package/dist/components/channels-page.d.ts.map +1 -0
- package/dist/components/channels-page.js +132 -0
- package/dist/components/commission-rule-detail-page.d.ts +10 -0
- package/dist/components/commission-rule-detail-page.d.ts.map +1 -0
- package/dist/components/commission-rule-detail-page.js +57 -0
- package/dist/components/contract-detail-page.d.ts +10 -0
- package/dist/components/contract-detail-page.d.ts.map +1 -0
- package/dist/components/contract-detail-page.js +64 -0
- package/dist/components/distribution-overview.d.ts +19 -0
- package/dist/components/distribution-overview.d.ts.map +1 -0
- package/dist/components/distribution-overview.js +13 -0
- package/dist/components/distribution-page.d.ts +26 -0
- package/dist/components/distribution-page.d.ts.map +1 -0
- package/dist/components/distribution-page.js +190 -0
- package/dist/components/distribution-section-header.d.ts +7 -0
- package/dist/components/distribution-section-header.d.ts.map +1 -0
- package/dist/components/distribution-section-header.js +6 -0
- package/dist/components/distribution-shared.d.ts +32 -0
- package/dist/components/distribution-shared.d.ts.map +1 -0
- package/dist/components/distribution-shared.js +246 -0
- package/dist/components/distribution-tabs-primary.d.ts +57 -0
- package/dist/components/distribution-tabs-primary.d.ts.map +1 -0
- package/dist/components/distribution-tabs-primary.js +89 -0
- package/dist/components/distribution-tabs-secondary.d.ts +58 -0
- package/dist/components/distribution-tabs-secondary.d.ts.map +1 -0
- package/dist/components/distribution-tabs-secondary.js +89 -0
- package/dist/components/mapping-detail-page.d.ts +10 -0
- package/dist/components/mapping-detail-page.d.ts.map +1 -0
- package/dist/components/mapping-detail-page.js +51 -0
- package/dist/components/webhook-event-detail-page.d.ts +9 -0
- package/dist/components/webhook-event-detail-page.d.ts.map +1 -0
- package/dist/components/webhook-event-detail-page.js +46 -0
- package/dist/i18n/en.d.ts +592 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +561 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/messages.d.ts +409 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/provider.d.ts +1207 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +44 -0
- package/dist/i18n/ro.d.ts +592 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ro.js +561 -0
- package/dist/i18n/utils.d.ts +4 -0
- package/dist/i18n/utils.d.ts.map +1 -0
- package/dist/i18n/utils.js +8 -0
- package/dist/ui.d.ts +16 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +14 -0
- package/package.json +53 -9
- package/src/styles.css +11 -0
package/README.md
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
|
-
# @voyantjs/
|
|
1
|
+
# @voyantjs/distribution-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The distribution client tier: headless data hooks/clients plus the styled UI
|
|
4
|
+
components and page-level compositions (formerly `@voyantjs/distribution-ui`).
|
|
5
|
+
|
|
6
|
+
Headless consumers import from the root, `./hooks`, `./client`, or
|
|
7
|
+
`./query-keys` — these pull no styling peers. Styled surfaces live under
|
|
8
|
+
`./ui`, `./components/*`, `./i18n`, and `./styles.css`, whose heavier peers
|
|
9
|
+
(`@voyantjs/ui`, `@tanstack/react-table`) are optional and only needed when
|
|
10
|
+
you import those subpaths.
|
|
4
11
|
|
|
5
12
|
## Install
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
|
-
pnpm add @voyantjs/
|
|
15
|
+
pnpm add @voyantjs/distribution-react @voyantjs/distribution @tanstack/react-query react react-dom zod
|
|
9
16
|
```
|
|
10
17
|
|
|
11
18
|
## Usage
|
|
12
19
|
|
|
13
20
|
```tsx
|
|
14
|
-
import {
|
|
21
|
+
import { VoyantDistributionProvider, useChannels } from "@voyantjs/distribution-react"
|
|
15
22
|
|
|
16
23
|
function App() {
|
|
17
24
|
return (
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
</
|
|
25
|
+
<VoyantDistributionProvider baseUrl="/api">
|
|
26
|
+
<ChannelsList />
|
|
27
|
+
</VoyantDistributionProvider>
|
|
21
28
|
)
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
function
|
|
25
|
-
const { data } =
|
|
26
|
-
return <>{data?.data.map((
|
|
31
|
+
function ChannelsList() {
|
|
32
|
+
const { data } = useChannels()
|
|
33
|
+
return <>{data?.data.map((channel) => <div key={channel.id}>{channel.name}</div>)}</>
|
|
27
34
|
}
|
|
28
35
|
```
|
|
29
36
|
|
|
37
|
+
## UI components
|
|
38
|
+
|
|
39
|
+
Importable React UI components for Voyant distribution. Bundler-consumed (Vite, Next.js, webpack, etc.).
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @voyantjs/distribution-react @voyantjs/ui @tanstack/react-query react react-dom
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`@voyantjs/ui` provides the design-system primitives; it is an optional peer
|
|
46
|
+
required only when importing the styled subpaths.
|
|
47
|
+
|
|
48
|
+
All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
|
|
49
|
+
|
|
50
|
+
### I18n
|
|
51
|
+
|
|
52
|
+
Components render English by default. To localize them, wrap your UI in
|
|
53
|
+
`DistributionUiMessagesProvider` and import only the locales your app supports.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { DistributionUiMessagesProvider } from "@voyantjs/distribution-react/ui"
|
|
57
|
+
import { distributionUiEn } from "@voyantjs/distribution-react/i18n/en"
|
|
58
|
+
import { distributionUiRo } from "@voyantjs/distribution-react/i18n/ro"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
English-only apps should import only `./i18n/en`. Bilingual apps can import
|
|
62
|
+
`./i18n/en` and `./i18n/ro`.
|
|
63
|
+
|
|
64
|
+
### Components
|
|
65
|
+
|
|
66
|
+
- `DistributionPage`, `DistributionOverview`
|
|
67
|
+
- `ChannelsPage`, `ChannelSyncPage`
|
|
68
|
+
- `ChannelDetailPage`, `ContractDetailPage`, `CommissionRuleDetailPage`, `MappingDetailPage`, `BookingLinkDetailPage`, `WebhookEventDetailPage`
|
|
69
|
+
- `DistributionChannelsTab`, `DistributionContractsTab`, `DistributionCommissionsTab`
|
|
70
|
+
- `DistributionMappingsTab`, `DistributionBookingLinksTab`, `DistributionWebhooksTab`
|
|
71
|
+
|
|
72
|
+
### Not included (registry-only)
|
|
73
|
+
|
|
74
|
+
Some components couple to TanStack Router or template-local helpers and remain available only via the shadcn registry: `distribution-dialogs-commercial`, `distribution-dialogs-commission`, `distribution-dialogs-sync`, `distribution-dialogs-webhook`. Import via `npx shadcn add @voyant/<component>` and customize per-project.
|
|
75
|
+
|
|
30
76
|
## License
|
|
31
77
|
|
|
32
78
|
Apache-2.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface BookingLinkDetailPageProps {
|
|
2
|
+
id: string;
|
|
3
|
+
className?: string;
|
|
4
|
+
onBack?: () => void;
|
|
5
|
+
onDeleted?: () => void;
|
|
6
|
+
onChannelOpen?: (channelId: string) => void;
|
|
7
|
+
onBookingOpen?: (bookingId: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function BookingLinkDetailPage({ id, className, onBack, onDeleted, onChannelOpen, onBookingOpen, }: BookingLinkDetailPageProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=booking-link-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-link-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/booking-link-detail-page.tsx"],"names":[],"mappings":"AAwBA,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5C;AAID,wBAAgB,qBAAqB,CAAC,EACpC,EAAE,EACF,SAAS,EACT,MAAa,EACb,SAAgB,EAChB,aAAoB,EACpB,aAAoB,GACrB,EAAE,0BAA0B,2CAwI5B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmActionButton, } from "@voyantjs/ui/components";
|
|
4
|
+
import { cn } from "@voyantjs/ui/lib/utils";
|
|
5
|
+
import { ArrowLeft, Link2, Loader2, ReceiptText } from "lucide-react";
|
|
6
|
+
import { useDistributionUiI18nOrDefault } from "../i18n/index.js";
|
|
7
|
+
import { distributionQueryKeys, fetchWithValidation, getBookingLinkQueryOptions, getBookingQueryOptions, getChannelQueryOptions, successEnvelope, useVoyantDistributionContext, } from "../index.js";
|
|
8
|
+
import { formatDistributionDateTime } from "./distribution-shared.js";
|
|
9
|
+
const noop = () => { };
|
|
10
|
+
export function BookingLinkDetailPage({ id, className, onBack = noop, onDeleted = noop, onChannelOpen = noop, onBookingOpen = noop, }) {
|
|
11
|
+
const i18n = useDistributionUiI18nOrDefault();
|
|
12
|
+
const { messages } = i18n;
|
|
13
|
+
const detail = messages.details.bookingLink;
|
|
14
|
+
const client = useVoyantDistributionContext();
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
const linkQuery = useQuery({
|
|
17
|
+
...getBookingLinkQueryOptions(client, id),
|
|
18
|
+
select: (result) => result.data,
|
|
19
|
+
});
|
|
20
|
+
const link = linkQuery.data;
|
|
21
|
+
const channelQuery = useQuery({
|
|
22
|
+
...getChannelQueryOptions(client, link?.channelId),
|
|
23
|
+
select: (result) => result.data,
|
|
24
|
+
enabled: Boolean(link?.channelId),
|
|
25
|
+
});
|
|
26
|
+
const bookingQuery = useQuery({
|
|
27
|
+
...getBookingQueryOptions(client, link?.bookingId),
|
|
28
|
+
select: (result) => result.data,
|
|
29
|
+
enabled: Boolean(link?.bookingId),
|
|
30
|
+
});
|
|
31
|
+
const remove = useMutation({
|
|
32
|
+
mutationFn: () => fetchWithValidation(`/v1/distribution/booking-links/${id}`, successEnvelope, client, {
|
|
33
|
+
method: "DELETE", // i18n-literal-ok HTTP method
|
|
34
|
+
}),
|
|
35
|
+
onSuccess: () => {
|
|
36
|
+
void queryClient.invalidateQueries({ queryKey: distributionQueryKeys.bookingLinks() });
|
|
37
|
+
queryClient.removeQueries({ queryKey: distributionQueryKeys.bookingLink(id) });
|
|
38
|
+
onDeleted();
|
|
39
|
+
onBack();
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (linkQuery.isPending) {
|
|
43
|
+
return (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) }));
|
|
44
|
+
}
|
|
45
|
+
if (!link) {
|
|
46
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center gap-4 py-12", children: [_jsx("p", { className: "text-muted-foreground", children: detail.notFound }), _jsx(Button, { variant: "outline", onClick: onBack, children: messages.common.backToDistribution })] }));
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs("div", { "data-slot": "booking-link-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: onBack, children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: detail.title }), _jsxs("div", { className: "mt-1 flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: link.externalStatus ?? messages.common.unmappedStatus }), _jsx(Badge, { variant: "secondary", children: link.externalReference ?? messages.common.noReference })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => onChannelOpen(link.channelId), children: [_jsx(Link2, { className: "mr-2 h-4 w-4" }), detail.openChannel] }), _jsxs(Button, { variant: "outline", onClick: () => onBookingOpen(link.bookingId), children: [_jsx(ReceiptText, { className: "mr-2 h-4 w-4" }), detail.openBooking] }), _jsx(ConfirmActionButton, { buttonLabel: detail.deleteButton, confirmLabel: detail.deleteButton, title: detail.deleteConfirm, description: detail.deleteDescription, variant: "destructive", confirmVariant: "destructive", disabled: remove.isPending, onConfirm: async () => {
|
|
49
|
+
await remove.mutateAsync();
|
|
50
|
+
} })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.sections.details }) }), _jsxs(CardContent, { className: "grid gap-3 text-sm md:grid-cols-2", children: [_jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [messages.common.channelLabel, ":"] }), " ", _jsx("span", { children: channelQuery.data?.name ?? link.channelId })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [messages.common.bookingLabel, ":"] }), " ", _jsx("span", { children: bookingQuery.data?.bookingNumber ?? link.bookingId })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.externalBooking, ":"] }), " ", _jsx("span", { children: link.externalBookingId ?? messages.common.none })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.reference, ":"] }), " ", _jsx("span", { children: link.externalReference ?? messages.common.none })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.bookedAtExternal, ":"] }), " ", _jsx("span", { children: formatDistributionDateTime(link.bookedAtExternal, i18n) })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.lastSynced, ":"] }), " ", _jsx("span", { children: formatDistributionDateTime(link.lastSyncedAt, i18n) })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [messages.common.createdLabel, ":"] }), " ", _jsx("span", { children: formatDistributionDateTime(link.createdAt, i18n) })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [messages.common.updatedLabel, ":"] }), " ", _jsx("span", { children: formatDistributionDateTime(link.updatedAt, i18n) })] })] })] })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ChannelDetailPageProps {
|
|
2
|
+
id: string;
|
|
3
|
+
className?: string;
|
|
4
|
+
onBack?: () => void;
|
|
5
|
+
onDeleted?: () => void;
|
|
6
|
+
onContractOpen?: (contractId: string) => void;
|
|
7
|
+
onMappingOpen?: (mappingId: string) => void;
|
|
8
|
+
onBookingLinkOpen?: (bookingLinkId: string) => void;
|
|
9
|
+
onWebhookEventOpen?: (webhookEventId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function ChannelDetailPage({ id, className, onBack, onDeleted, onContractOpen, onMappingOpen, onBookingLinkOpen, onWebhookEventOpen, }: ChannelDetailPageProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=channel-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/channel-detail-page.tsx"],"names":[],"mappings":"AAgCA,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,iBAAiB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAA;IACnD,kBAAkB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;CACtD;AAID,wBAAgB,iBAAiB,CAAC,EAChC,EAAE,EACF,SAAS,EACT,MAAa,EACb,SAAgB,EAChB,cAAqB,EACrB,aAAoB,EACpB,iBAAwB,EACxB,kBAAyB,GAC1B,EAAE,sBAAsB,2CAmRxB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmActionButton, } from "@voyantjs/ui/components";
|
|
3
|
+
import { cn } from "@voyantjs/ui/lib/utils";
|
|
4
|
+
import { ArrowLeft, Link2, Loader2, Package, Webhook } from "lucide-react";
|
|
5
|
+
import { useDistributionUiI18nOrDefault } from "../i18n/index.js";
|
|
6
|
+
import { useBookingLinks, useBookings, useChannel, useChannelMutation, useContracts, useMappings, useProducts, useSuppliers, useWebhookEvents, } from "../index.js";
|
|
7
|
+
import { formatDistributionDate, formatDistributionDateTime, getChannelKindLabel, getChannelStatusLabel, getContractStatusLabel, getPaymentOwnerLabel, } from "./distribution-shared.js";
|
|
8
|
+
const noop = () => { };
|
|
9
|
+
export function ChannelDetailPage({ id, className, onBack = noop, onDeleted = noop, onContractOpen = noop, onMappingOpen = noop, onBookingLinkOpen = noop, onWebhookEventOpen = noop, }) {
|
|
10
|
+
const i18n = useDistributionUiI18nOrDefault();
|
|
11
|
+
const { messages } = i18n;
|
|
12
|
+
const detail = messages.details.channel;
|
|
13
|
+
const channelQuery = useChannel(id);
|
|
14
|
+
const contractsQuery = useContracts({ channelId: id, limit: 25, offset: 0 });
|
|
15
|
+
const mappingsQuery = useMappings({ channelId: id, limit: 25, offset: 0 });
|
|
16
|
+
const bookingLinksQuery = useBookingLinks({ channelId: id, limit: 25, offset: 0 });
|
|
17
|
+
const webhookEventsQuery = useWebhookEvents({ channelId: id, limit: 25, offset: 0 });
|
|
18
|
+
const productsQuery = useProducts({ limit: 25, offset: 0 });
|
|
19
|
+
const bookingsQuery = useBookings({ limit: 25, offset: 0 });
|
|
20
|
+
const suppliersQuery = useSuppliers({ limit: 25, offset: 0 });
|
|
21
|
+
const { remove } = useChannelMutation();
|
|
22
|
+
if (channelQuery.isPending) {
|
|
23
|
+
return (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) }));
|
|
24
|
+
}
|
|
25
|
+
const channel = channelQuery.data;
|
|
26
|
+
if (!channel) {
|
|
27
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center gap-4 py-12", children: [_jsx("p", { className: "text-muted-foreground", children: detail.notFound }), _jsx(Button, { variant: "outline", onClick: onBack, children: messages.common.backToDistribution })] }));
|
|
28
|
+
}
|
|
29
|
+
const productsById = new Map((productsQuery.data?.data ?? []).map((product) => [product.id, product]));
|
|
30
|
+
const bookingsById = new Map((bookingsQuery.data?.data ?? []).map((booking) => [booking.id, booking]));
|
|
31
|
+
const suppliersById = new Map((suppliersQuery.data?.data ?? []).map((supplier) => [supplier.id, supplier]));
|
|
32
|
+
return (_jsxs("div", { "data-slot": "channel-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: onBack, children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: channel.name }), _jsxs("div", { className: "mt-1 flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: getChannelKindLabel(channel.kind, messages) }), _jsx(Badge, { variant: channel.status === "active" ? "default" : "secondary", children: getChannelStatusLabel(channel.status, messages) })] })] }), _jsx(ConfirmActionButton, { buttonLabel: detail.deleteButton, confirmLabel: detail.deleteButton, title: detail.deleteConfirm, description: detail.deleteDescription, variant: "destructive", confirmVariant: "destructive", disabled: remove.isPending, onConfirm: async () => {
|
|
33
|
+
await remove.mutateAsync(id);
|
|
34
|
+
onDeleted();
|
|
35
|
+
onBack();
|
|
36
|
+
} })] }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.sections.details }) }), _jsxs(CardContent, { className: "grid gap-3 text-sm", children: [_jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.website, ":"] }), " ", _jsx("span", { children: channel.website ?? messages.common.none })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.contactName, ":"] }), " ", _jsx("span", { children: channel.contactName ?? messages.common.none })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [detail.labels.contactEmail, ":"] }), " ", _jsx("span", { children: channel.contactEmail ?? messages.common.none })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [messages.common.createdLabel, ":"] }), " ", _jsx("span", { children: formatDistributionDateTime(channel.createdAt, i18n) })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [messages.common.updatedLabel, ":"] }), " ", _jsx("span", { children: formatDistributionDateTime(channel.updatedAt, i18n) })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.sections.metadata }) }), _jsx(CardContent, { className: "text-sm", children: channel.metadata ? (_jsx("pre", { className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: JSON.stringify(channel.metadata, null, 2) })) : (_jsx("p", { className: "text-muted-foreground", children: detail.empty.metadata })) })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Link2, { className: "h-4 w-4" }), _jsx(CardTitle, { children: detail.sections.contracts })] }), _jsx(CardContent, { className: "space-y-3 text-sm", children: (contractsQuery.data?.data.length ?? 0) === 0 ? (_jsx("p", { className: "text-muted-foreground", children: detail.empty.contracts })) : (contractsQuery.data?.data.map((contract) => (_jsxs("button", { type: "button", className: "block w-full rounded-md border p-3 text-left hover:bg-muted/40", onClick: () => onContractOpen(contract.id), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: getContractStatusLabel(contract.status, messages) }), _jsxs("span", { children: [formatDistributionDate(contract.startsAt, i18n), " -", " ", contract.endsAt
|
|
37
|
+
? formatDistributionDate(contract.endsAt, i18n)
|
|
38
|
+
: messages.common.openEnded] })] }), _jsxs("div", { className: "mt-2 text-muted-foreground", children: [detail.labels.supplier, ":", " ", suppliersById.get(contract.supplierId ?? "")?.name ??
|
|
39
|
+
contract.supplierId ??
|
|
40
|
+
messages.common.none] }), _jsxs("div", { className: "text-muted-foreground", children: [detail.labels.payment, ": ", getPaymentOwnerLabel(contract.paymentOwner, messages), " - ", detail.labels.cancellation, ":", " ", messages.common.cancellationOwnerLabels[contract.cancellationOwner]] }), contract.settlementTerms ? (_jsx("div", { className: "mt-2 whitespace-pre-wrap", children: contract.settlementTerms })) : null, contract.notes ? (_jsx("div", { className: "mt-2 whitespace-pre-wrap", children: contract.notes })) : null] }, contract.id)))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Package, { className: "h-4 w-4" }), _jsx(CardTitle, { children: detail.sections.mappings })] }), _jsx(CardContent, { className: "space-y-3 text-sm", children: (mappingsQuery.data?.data.length ?? 0) === 0 ? (_jsx("p", { className: "text-muted-foreground", children: detail.empty.mappings })) : (mappingsQuery.data?.data.map((mapping) => (_jsxs("button", { type: "button", className: "block w-full rounded-md border p-3 text-left hover:bg-muted/40", onClick: () => onMappingOpen(mapping.id), children: [_jsx("div", { className: "font-medium", children: productsById.get(mapping.productId)?.name ?? mapping.productId }), _jsxs("div", { className: "text-muted-foreground", children: [detail.labels.externalProduct, ": ", mapping.externalProductId] }), _jsxs("div", { className: "text-muted-foreground", children: [detail.labels.rate, ": ", mapping.externalRateId ?? messages.common.none, " - ", detail.labels.category, ": ", mapping.externalCategoryId ?? messages.common.none] }), _jsx("div", { className: "mt-2", children: _jsx(Badge, { variant: mapping.active ? "default" : "secondary", children: mapping.active ? messages.common.active : messages.common.inactive }) })] }, mapping.id)))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.sections.bookingLinks }) }), _jsx(CardContent, { className: "space-y-3 text-sm", children: (bookingLinksQuery.data?.data.length ?? 0) === 0 ? (_jsx("p", { className: "text-muted-foreground", children: detail.empty.bookingLinks })) : (bookingLinksQuery.data?.data.map((link) => (_jsxs("button", { type: "button", className: "block w-full rounded-md border p-3 text-left hover:bg-muted/40", onClick: () => onBookingLinkOpen(link.id), children: [_jsxs("div", { className: "font-medium", children: [detail.labels.booking, ":", " ", bookingsById.get(link.bookingId)?.bookingNumber ?? link.bookingId] }), _jsxs("div", { className: "text-muted-foreground", children: [detail.labels.externalBooking, ": ", link.externalBookingId ?? messages.common.none] }), _jsxs("div", { className: "text-muted-foreground", children: [detail.labels.reference, ": ", link.externalReference ?? messages.common.none] }), _jsxs("div", { className: "text-muted-foreground", children: [detail.labels.lastSynced, ": ", formatDistributionDateTime(link.lastSyncedAt, i18n)] })] }, link.id)))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-2", children: [_jsx(Webhook, { className: "h-4 w-4" }), _jsx(CardTitle, { children: detail.sections.webhooks })] }), _jsx(CardContent, { className: "space-y-3 text-sm", children: (webhookEventsQuery.data?.data.length ?? 0) === 0 ? (_jsx("p", { className: "text-muted-foreground", children: detail.empty.webhooks })) : (webhookEventsQuery.data?.data.map((event) => (_jsxs("button", { type: "button", className: "block w-full rounded-md border p-3 text-left hover:bg-muted/40", onClick: () => onWebhookEventOpen(event.id), children: [_jsx("div", { className: "font-medium", children: event.eventType }), _jsxs("div", { className: "text-muted-foreground", children: [messages.common.received, ": ", formatDistributionDateTime(event.receivedAt, i18n)] }), _jsx("div", { className: "mt-2", children: _jsx(Badge, { variant: "outline", children: messages.common.webhookStatusLabels[event.status] }) })] }, event.id)))) })] })] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type VoyantFetcher } from "../index.js";
|
|
2
|
+
export interface ChannelSyncPageProps {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
fetcher?: VoyantFetcher;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function ChannelSyncPage({ baseUrl, fetcher, className }?: ChannelSyncPageProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=channel-sync-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-sync-page.d.ts","sourceRoot":"","sources":["../../src/components/channel-sync-page.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAAgD,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AA2F9F,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAgDD,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAE,oBAAyB,2CA8WzF"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Label, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
|
|
5
|
+
import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
|
|
6
|
+
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@voyantjs/ui/components/empty";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
8
|
+
import { AlertTriangle, ChevronDown, Loader2, RotateCw, X } from "lucide-react";
|
|
9
|
+
import { useEffect, useRef, useState } from "react";
|
|
10
|
+
import { useDistributionUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
|
+
import { defaultFetcher, useVoyantDistributionContext } from "../index.js";
|
|
12
|
+
// Fetch helpers
|
|
13
|
+
async function fetchJson(path, options, init) {
|
|
14
|
+
const res = await options.fetcher(joinUrl(options.baseUrl, path), {
|
|
15
|
+
...init,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
...init?.headers,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
const body = text
|
|
23
|
+
? JSON.parse(text)
|
|
24
|
+
: {};
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(body.error ?? `Request failed: ${res.status}`);
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
const STATUS_VARIANTS = {
|
|
31
|
+
pending: "secondary",
|
|
32
|
+
ok: "default",
|
|
33
|
+
failed: "destructive",
|
|
34
|
+
compensated: "outline",
|
|
35
|
+
};
|
|
36
|
+
const STATUS_TILES = [
|
|
37
|
+
{ key: "pending", tone: "secondary" },
|
|
38
|
+
{ key: "ok", tone: "default" },
|
|
39
|
+
{ key: "failed", tone: "destructive" },
|
|
40
|
+
{ key: "compensated", tone: "outline" },
|
|
41
|
+
];
|
|
42
|
+
const LINKS_REFETCH_MS = 15_000;
|
|
43
|
+
const THROTTLING_REFETCH_MS = 60_000;
|
|
44
|
+
// Page
|
|
45
|
+
export function ChannelSyncPage({ baseUrl, fetcher, className } = {}) {
|
|
46
|
+
const distributionMessages = useDistributionUiMessagesOrDefault();
|
|
47
|
+
const messages = distributionMessages.channelSync;
|
|
48
|
+
const context = useVoyantDistributionContext();
|
|
49
|
+
const client = {
|
|
50
|
+
baseUrl: baseUrl ?? context.baseUrl,
|
|
51
|
+
fetcher: fetcher ?? context.fetcher ?? defaultFetcher,
|
|
52
|
+
};
|
|
53
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
54
|
+
const [bookingId, setBookingId] = useState(null);
|
|
55
|
+
const [bookingSearch, setBookingSearch] = useState("");
|
|
56
|
+
const [selectedBooking, setSelectedBooking] = useState(null);
|
|
57
|
+
const [channelId, setChannelId] = useState(null);
|
|
58
|
+
const [selectedChannel, setSelectedChannel] = useState(null);
|
|
59
|
+
const [drilldownBookingId, setDrilldownBookingId] = useState(null);
|
|
60
|
+
const queryClient = useQueryClient();
|
|
61
|
+
const linksQuery = useQuery({
|
|
62
|
+
queryKey: ["channel-push-links", statusFilter, bookingId, channelId],
|
|
63
|
+
queryFn: () => {
|
|
64
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
65
|
+
if (statusFilter !== "all")
|
|
66
|
+
params.set("status", statusFilter);
|
|
67
|
+
if (bookingId)
|
|
68
|
+
params.set("bookingId", bookingId);
|
|
69
|
+
if (channelId)
|
|
70
|
+
params.set("channelId", channelId);
|
|
71
|
+
return fetchJson(`/v1/admin/distribution/channel-push/links?${params}`, client);
|
|
72
|
+
},
|
|
73
|
+
refetchInterval: LINKS_REFETCH_MS,
|
|
74
|
+
refetchIntervalInBackground: false,
|
|
75
|
+
});
|
|
76
|
+
const throttlingQuery = useQuery({
|
|
77
|
+
queryKey: ["channel-push-throttling"],
|
|
78
|
+
queryFn: () => fetchJson("/v1/admin/distribution/channel-push/throttling", client),
|
|
79
|
+
refetchInterval: THROTTLING_REFETCH_MS,
|
|
80
|
+
});
|
|
81
|
+
const debouncedBookingSearch = useDebouncedValue(bookingSearch, 200);
|
|
82
|
+
const bookingsQuery = useQuery({
|
|
83
|
+
queryKey: ["channel-sync-booking-options", debouncedBookingSearch],
|
|
84
|
+
queryFn: () => {
|
|
85
|
+
const params = new URLSearchParams({ limit: "20" });
|
|
86
|
+
if (debouncedBookingSearch.trim())
|
|
87
|
+
params.set("search", debouncedBookingSearch.trim());
|
|
88
|
+
return fetchJson(`/v1/admin/bookings?${params}`, client);
|
|
89
|
+
},
|
|
90
|
+
placeholderData: (prev) => prev,
|
|
91
|
+
});
|
|
92
|
+
const channelsQuery = useQuery({
|
|
93
|
+
queryKey: ["channel-sync-channel-options"],
|
|
94
|
+
queryFn: () => fetchJson(`/v1/admin/distribution/channels?limit=100`, client),
|
|
95
|
+
staleTime: 60_000,
|
|
96
|
+
});
|
|
97
|
+
const retryMutation = useMutation({
|
|
98
|
+
mutationFn: (id) => fetchJson(`/v1/admin/distribution/channel-push/retry/${id}`, client, { method: "POST" }),
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
void queryClient.invalidateQueries({ queryKey: ["channel-push-links"] });
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const reconcileMutation = useMutation({
|
|
104
|
+
mutationFn: (flow) => fetchJson(`/v1/admin/distribution/channel-push/reconcile/${flow}`, client, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
}),
|
|
107
|
+
onSuccess: () => {
|
|
108
|
+
void queryClient.invalidateQueries({ queryKey: ["channel-push-links"] });
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const counts = linksQuery.data?.counts ?? {};
|
|
112
|
+
const rows = linksQuery.data?.data ?? [];
|
|
113
|
+
const throttledChannels = throttlingQuery.data?.data ?? [];
|
|
114
|
+
const isThrottled = throttledChannels.length > 0;
|
|
115
|
+
const filtersActive = statusFilter !== "all" || bookingId !== null || channelId !== null;
|
|
116
|
+
const clearFilters = () => {
|
|
117
|
+
setStatusFilter("all");
|
|
118
|
+
setBookingId(null);
|
|
119
|
+
setBookingSearch("");
|
|
120
|
+
setSelectedBooking(null);
|
|
121
|
+
setChannelId(null);
|
|
122
|
+
setSelectedChannel(null);
|
|
123
|
+
};
|
|
124
|
+
const bookingOptions = bookingsQuery.data?.data ?? [];
|
|
125
|
+
const channelOptions = channelsQuery.data?.data ?? [];
|
|
126
|
+
return (_jsxs("div", { "data-slot": "channel-sync-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: messages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.description })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(AutoRefreshIndicator, { isFetching: linksQuery.isFetching, dataUpdatedAt: linksQuery.dataUpdatedAt, intervalMs: LINKS_REFETCH_MS, messages: messages }), _jsx(ReconcileMenu, { onRun: (flow) => reconcileMutation.mutate(flow), isRunning: reconcileMutation.isPending, lastResult: reconcileMutation.data ?? null, messages: messages })] })] }), isThrottled ? (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { children: [_jsx("span", { className: "font-medium", children: messages.throttledTitle }), " ", _jsx("span", { children: formatTemplate(messages.throttledBody, {
|
|
127
|
+
count: throttledChannels.reduce((sum, c) => sum + c.count, 0),
|
|
128
|
+
channels: throttledChannels.length,
|
|
129
|
+
channelLabel: throttledChannels.length === 1 ? "channel" : "channels",
|
|
130
|
+
}) })] })] })) : null, _jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-4", children: STATUS_TILES.map((tile) => {
|
|
131
|
+
const isActive = statusFilter === tile.key;
|
|
132
|
+
const value = counts[tile.key] ?? 0;
|
|
133
|
+
const tileMessages = messages.statusTiles[tile.key];
|
|
134
|
+
return (_jsxs("button", { type: "button", onClick: () => setStatusFilter(isActive ? "all" : tile.key), className: cn("group rounded-lg border bg-card p-4 text-left transition-all", "hover:border-foreground/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", isActive && "border-primary ring-2 ring-primary/30"), "aria-pressed": isActive, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: tileMessages.label }), tile.key === "failed" && value > 0 ? (_jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-destructive" })) : null] }), _jsx("div", { className: "mt-1 text-3xl font-semibold tabular-nums", children: value }), _jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: tileMessages.description })] }, tile.key));
|
|
135
|
+
}) }), _jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "cs-booking", className: "text-xs", children: messages.filters.booking }), _jsx(AsyncCombobox, { value: bookingId, onChange: (value) => {
|
|
136
|
+
setBookingId(value);
|
|
137
|
+
if (!value)
|
|
138
|
+
setSelectedBooking(null);
|
|
139
|
+
else {
|
|
140
|
+
const match = bookingOptions.find((b) => b.id === value);
|
|
141
|
+
if (match)
|
|
142
|
+
setSelectedBooking(match);
|
|
143
|
+
}
|
|
144
|
+
}, items: bookingOptions, selectedItem: selectedBooking, getKey: (b) => b.id, getLabel: (b) => b.bookingNumber, getSecondary: (b) => b.status, onSearchChange: setBookingSearch, placeholder: messages.filters.bookingPlaceholder, emptyText: bookingsQuery.isFetching
|
|
145
|
+
? messages.filters.bookingSearching
|
|
146
|
+
: messages.filters.bookingEmpty, triggerClassName: "w-full" })] }), _jsxs("div", { className: "flex flex-1 flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "cs-channel", className: "text-xs", children: messages.filters.channel }), _jsx(AsyncCombobox, { value: channelId, onChange: (value) => {
|
|
147
|
+
setChannelId(value);
|
|
148
|
+
if (!value)
|
|
149
|
+
setSelectedChannel(null);
|
|
150
|
+
else {
|
|
151
|
+
const match = channelOptions.find((c) => c.id === value);
|
|
152
|
+
if (match)
|
|
153
|
+
setSelectedChannel(match);
|
|
154
|
+
}
|
|
155
|
+
}, items: channelOptions, selectedItem: selectedChannel, getKey: (c) => c.id, getLabel: (c) => c.name, getSecondary: (c) => formatChannelKind(c.kind), placeholder: messages.filters.channelPlaceholder, emptyText: messages.filters.channelEmpty, triggerClassName: "w-full" })] }), filtersActive ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1.5 h-3.5 w-3.5" }), distributionMessages.common.clearFilters] })) : null] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsx(CardTitle, { className: "text-sm", children: messages.table.title }), _jsx(CardDescription, { children: filtersActive
|
|
156
|
+
? formatTemplate(messages.table.filteredDescription, { count: rows.length })
|
|
157
|
+
: messages.table.defaultDescription })] }), _jsx(CardContent, { className: "p-0", children: linksQuery.isPending ? (_jsx("div", { className: "flex items-center justify-center p-12", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) })) : rows.length === 0 ? (_jsx(Empty, { className: "border-0", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyTitle, { children: filtersActive ? messages.table.noMatchesTitle : messages.table.noLinksTitle }), _jsx(EmptyDescription, { children: filtersActive
|
|
158
|
+
? messages.table.noMatchesDescription
|
|
159
|
+
: messages.table.noLinksDescription })] }) })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: messages.table.booking }), _jsx(TableHead, { children: messages.table.channel }), _jsx(TableHead, { children: messages.table.status }), _jsx(TableHead, { className: "text-right", children: messages.table.attempts }), _jsx(TableHead, { children: messages.table.lastPush }), _jsx(TableHead, { children: messages.table.externalRef }), _jsx(TableHead, { className: "text-right", children: messages.table.actions })] }) }), _jsx(TableBody, { children: rows.map((row) => {
|
|
160
|
+
const isFailed = row.link.pushStatus === "failed";
|
|
161
|
+
return (_jsxs(TableRow, { className: cn(isFailed && "bg-destructive/5 hover:bg-destructive/10"), children: [_jsxs(TableCell, { className: "font-mono text-xs", children: [_jsx("div", { children: row.link.bookingId }), row.link.bookingItemId ? (_jsx("div", { className: "text-muted-foreground", children: formatTemplate(messages.table.itemPrefix, {
|
|
162
|
+
id: row.link.bookingItemId,
|
|
163
|
+
}) })) : null] }), _jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: row.channelName }), _jsx("div", { className: "text-xs text-muted-foreground", children: formatChannelKind(row.channelKind) })] }), _jsxs(TableCell, { children: [_jsx(Badge, { variant: STATUS_VARIANTS[row.link.pushStatus] ?? "outline", children: messages.statusLabels[row.link.pushStatus] ??
|
|
164
|
+
row.link.pushStatus }), row.link.lastError ? (_jsx("div", { className: "mt-1 max-w-xs truncate text-xs text-destructive", title: row.link.lastError, children: row.link.lastError })) : null] }), _jsx(TableCell, { className: "text-right tabular-nums", children: row.link.pushAttempts }), _jsx(TableCell, { className: "text-xs text-muted-foreground", children: row.link.lastPushAt ? formatRelative(row.link.lastPushAt) : "-" }), _jsx(TableCell, { className: "font-mono text-xs", children: row.link.externalBookingId ?? row.link.externalReference ?? "-" }), _jsx(TableCell, { className: "text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setDrilldownBookingId(row.link.bookingId), children: messages.table.deliveries }), _jsxs(Button, { variant: "outline", size: "sm", disabled: retryMutation.isPending &&
|
|
165
|
+
retryMutation.variables === row.link.bookingId, onClick: () => retryMutation.mutate(row.link.bookingId), children: [retryMutation.isPending &&
|
|
166
|
+
retryMutation.variables === row.link.bookingId ? (_jsx(Loader2, { className: "mr-1 h-3 w-3 animate-spin" })) : null, messages.table.retry] })] }) })] }, row.link.id));
|
|
167
|
+
}) })] })) })] }), _jsx(DeliveriesDrawer, { bookingId: drilldownBookingId, client: client, onClose: () => setDrilldownBookingId(null), messages: messages })] }));
|
|
168
|
+
}
|
|
169
|
+
function ReconcileMenu({ onRun, isRunning, lastResult, messages, }) {
|
|
170
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsxs(Button, { variant: "outline", size: "sm", disabled: isRunning, children: [isRunning ? (_jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" })) : (_jsx(RotateCw, { className: "mr-1.5 h-3.5 w-3.5" })), messages.reconcile.trigger, _jsx(ChevronDown, { className: "ml-1.5 h-3.5 w-3.5" })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: messages.reconcile.menuLabel }), _jsxs(DropdownMenuItem, { onClick: () => onRun("bookings"), children: [messages.reconcile.bookings, _jsx("span", { className: "ml-auto text-xs text-muted-foreground", children: messages.reconcile.priority })] }), _jsx(DropdownMenuItem, { onClick: () => onRun("availability"), children: messages.reconcile.availability }), _jsx(DropdownMenuItem, { onClick: () => onRun("content"), children: messages.reconcile.content })] }), lastResult ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: formatTemplate(messages.reconcile.lastRun, {
|
|
171
|
+
scanned: lastResult.scanned,
|
|
172
|
+
triggered: lastResult.triggered,
|
|
173
|
+
}) })] })) : null] })] }));
|
|
174
|
+
}
|
|
175
|
+
function AutoRefreshIndicator({ isFetching, dataUpdatedAt, intervalMs, messages, }) {
|
|
176
|
+
// Tick every second so the "Updated Xs ago" stays current.
|
|
177
|
+
const [, setNow] = useState(Date.now());
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const id = window.setInterval(() => setNow(Date.now()), 1000);
|
|
180
|
+
return () => window.clearInterval(id);
|
|
181
|
+
}, []);
|
|
182
|
+
if (!dataUpdatedAt) {
|
|
183
|
+
return (_jsxs("span", { className: "hidden items-center gap-1.5 text-xs text-muted-foreground md:flex", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), messages.refresh.loading] }));
|
|
184
|
+
}
|
|
185
|
+
const seconds = Math.max(0, Math.round((Date.now() - dataUpdatedAt) / 1000));
|
|
186
|
+
const intervalSec = Math.round(intervalMs / 1000);
|
|
187
|
+
return (_jsxs("span", { className: "hidden items-center gap-1.5 text-xs text-muted-foreground md:flex", title: formatTemplate(messages.refresh.title, { seconds: intervalSec }), children: [isFetching ? (_jsx(Loader2, { className: "h-3 w-3 animate-spin" })) : (_jsxs("span", { className: "relative flex h-2 w-2", children: [_jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-60" }), _jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-emerald-500" })] })), _jsx("span", { className: "tabular-nums", children: isFetching
|
|
188
|
+
? messages.refresh.refreshing
|
|
189
|
+
: formatTemplate(messages.refresh.updatedAgo, {
|
|
190
|
+
duration: formatShortDuration(seconds),
|
|
191
|
+
}) })] }));
|
|
192
|
+
}
|
|
193
|
+
function useDebouncedValue(value, delayMs) {
|
|
194
|
+
const [debounced, setDebounced] = useState(value);
|
|
195
|
+
const timeoutRef = useRef(null);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (timeoutRef.current)
|
|
198
|
+
clearTimeout(timeoutRef.current);
|
|
199
|
+
timeoutRef.current = setTimeout(() => setDebounced(value), delayMs);
|
|
200
|
+
return () => {
|
|
201
|
+
if (timeoutRef.current)
|
|
202
|
+
clearTimeout(timeoutRef.current);
|
|
203
|
+
};
|
|
204
|
+
}, [value, delayMs]);
|
|
205
|
+
return debounced;
|
|
206
|
+
}
|
|
207
|
+
function DeliveriesDrawer({ bookingId, client, onClose, messages, }) {
|
|
208
|
+
const isOpen = bookingId !== null;
|
|
209
|
+
const query = useQuery({
|
|
210
|
+
enabled: isOpen,
|
|
211
|
+
queryKey: ["channel-push-deliveries", bookingId],
|
|
212
|
+
queryFn: () => {
|
|
213
|
+
const params = new URLSearchParams({ bookingId: bookingId ?? "", limit: "200" });
|
|
214
|
+
return fetchJson(`/v1/admin/distribution/channel-push/deliveries?${params}`, client);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const rows = query.data?.data ?? [];
|
|
218
|
+
return (_jsx(Sheet, { open: isOpen, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(SheetContent, { side: "right", size: "xl", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: formatTemplate(messages.drawer.title, { bookingId: bookingId ?? "" }) }) }), _jsx(SheetBody, { className: "flex flex-col gap-3", children: query.isPending ? (_jsx("div", { className: "flex items-center justify-center p-12", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) })) : rows.length === 0 ? (_jsx(Empty, { children: _jsxs(EmptyHeader, { children: [_jsx(EmptyTitle, { children: messages.drawer.emptyTitle }), _jsx(EmptyDescription, { children: messages.drawer.emptyDescription })] }) })) : (rows.map((row) => (_jsxs(Card, { className: "text-xs", children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: row.status === "succeeded" ? "default" : "destructive", children: row.status }), _jsx("span", { className: "font-mono", children: row.sourceEvent }), _jsx("span", { className: "text-muted-foreground", children: formatTemplate(messages.drawer.attempt, { number: row.attemptNumber }) })] }), _jsx("span", { className: "text-muted-foreground", children: row.durationMs != null ? `${row.durationMs}ms` : "" })] }), _jsxs(CardDescription, { className: "font-mono", children: [row.requestMethod, " ", row.targetUrl] })] }), _jsxs(CardContent, { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [row.responseStatus != null ? (_jsx(Badge, { variant: "outline", children: formatTemplate(messages.drawer.httpStatus, { status: row.responseStatus }) })) : null, row.errorClass ? _jsx(Badge, { variant: "destructive", children: row.errorClass }) : null, _jsx("span", { className: "text-muted-foreground", children: formatRelative(row.createdAt) })] }), row.errorMessage ? (_jsx("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded bg-destructive/10 p-2 text-destructive", children: row.errorMessage })) : null, row.responseBodyExcerpt ? (_jsx("pre", { className: "overflow-x-auto rounded bg-muted p-2", children: row.responseBodyExcerpt })) : null] })] }, row.id)))) })] }) }));
|
|
219
|
+
}
|
|
220
|
+
function joinUrl(baseUrl, path) {
|
|
221
|
+
const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
222
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`;
|
|
223
|
+
return `${trimmedBase}${trimmedPath}`;
|
|
224
|
+
}
|
|
225
|
+
function formatChannelKind(kind) {
|
|
226
|
+
return kind.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase());
|
|
227
|
+
}
|
|
228
|
+
function formatShortDuration(seconds) {
|
|
229
|
+
if (seconds < 60)
|
|
230
|
+
return `${seconds}s`;
|
|
231
|
+
const min = Math.round(seconds / 60);
|
|
232
|
+
if (min < 60)
|
|
233
|
+
return `${min}m`;
|
|
234
|
+
const hours = Math.round(min / 60);
|
|
235
|
+
return `${hours}h`;
|
|
236
|
+
}
|
|
237
|
+
function formatRelative(iso) {
|
|
238
|
+
const date = new Date(iso);
|
|
239
|
+
const diffMs = Date.now() - date.getTime();
|
|
240
|
+
const sec = Math.round(diffMs / 1000);
|
|
241
|
+
if (sec < 60)
|
|
242
|
+
return `${sec}s ago`;
|
|
243
|
+
const min = Math.round(sec / 60);
|
|
244
|
+
if (min < 60)
|
|
245
|
+
return `${min}m ago`;
|
|
246
|
+
const hours = Math.round(min / 60);
|
|
247
|
+
if (hours < 24)
|
|
248
|
+
return `${hours}h ago`;
|
|
249
|
+
const days = Math.round(hours / 24);
|
|
250
|
+
return `${days}d ago`;
|
|
251
|
+
}
|
|
252
|
+
function formatTemplate(template, values) {
|
|
253
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
254
|
+
const value = values[key];
|
|
255
|
+
return value === undefined ? "" : String(value);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels-page.d.ts","sourceRoot":"","sources":["../../src/components/channels-page.tsx"],"names":[],"mappings":"AAgDA,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAWD,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,QAAoB,EAAE,GAAE,iBAAsB,2CAgJvF"}
|