@voyantjs/distribution-react 0.106.0 → 0.107.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +56 -10
  2. package/dist/components/booking-link-detail-page.d.ts +10 -0
  3. package/dist/components/booking-link-detail-page.d.ts.map +1 -0
  4. package/dist/components/booking-link-detail-page.js +51 -0
  5. package/dist/components/channel-detail-page.d.ts +12 -0
  6. package/dist/components/channel-detail-page.d.ts.map +1 -0
  7. package/dist/components/channel-detail-page.js +41 -0
  8. package/dist/components/channel-sync-page.d.ts +8 -0
  9. package/dist/components/channel-sync-page.d.ts.map +1 -0
  10. package/dist/components/channel-sync-page.js +257 -0
  11. package/dist/components/channels-page.d.ts +6 -0
  12. package/dist/components/channels-page.d.ts.map +1 -0
  13. package/dist/components/channels-page.js +132 -0
  14. package/dist/components/commission-rule-detail-page.d.ts +10 -0
  15. package/dist/components/commission-rule-detail-page.d.ts.map +1 -0
  16. package/dist/components/commission-rule-detail-page.js +57 -0
  17. package/dist/components/contract-detail-page.d.ts +10 -0
  18. package/dist/components/contract-detail-page.d.ts.map +1 -0
  19. package/dist/components/contract-detail-page.js +64 -0
  20. package/dist/components/distribution-overview.d.ts +19 -0
  21. package/dist/components/distribution-overview.d.ts.map +1 -0
  22. package/dist/components/distribution-overview.js +13 -0
  23. package/dist/components/distribution-page.d.ts +26 -0
  24. package/dist/components/distribution-page.d.ts.map +1 -0
  25. package/dist/components/distribution-page.js +190 -0
  26. package/dist/components/distribution-section-header.d.ts +7 -0
  27. package/dist/components/distribution-section-header.d.ts.map +1 -0
  28. package/dist/components/distribution-section-header.js +6 -0
  29. package/dist/components/distribution-shared.d.ts +32 -0
  30. package/dist/components/distribution-shared.d.ts.map +1 -0
  31. package/dist/components/distribution-shared.js +246 -0
  32. package/dist/components/distribution-tabs-primary.d.ts +57 -0
  33. package/dist/components/distribution-tabs-primary.d.ts.map +1 -0
  34. package/dist/components/distribution-tabs-primary.js +89 -0
  35. package/dist/components/distribution-tabs-secondary.d.ts +58 -0
  36. package/dist/components/distribution-tabs-secondary.d.ts.map +1 -0
  37. package/dist/components/distribution-tabs-secondary.js +89 -0
  38. package/dist/components/mapping-detail-page.d.ts +10 -0
  39. package/dist/components/mapping-detail-page.d.ts.map +1 -0
  40. package/dist/components/mapping-detail-page.js +51 -0
  41. package/dist/components/webhook-event-detail-page.d.ts +9 -0
  42. package/dist/components/webhook-event-detail-page.d.ts.map +1 -0
  43. package/dist/components/webhook-event-detail-page.js +46 -0
  44. package/dist/i18n/en.d.ts +592 -0
  45. package/dist/i18n/en.d.ts.map +1 -0
  46. package/dist/i18n/en.js +561 -0
  47. package/dist/i18n/index.d.ts +5 -0
  48. package/dist/i18n/index.d.ts.map +1 -0
  49. package/dist/i18n/index.js +3 -0
  50. package/dist/i18n/messages.d.ts +409 -0
  51. package/dist/i18n/messages.d.ts.map +1 -0
  52. package/dist/i18n/messages.js +1 -0
  53. package/dist/i18n/provider.d.ts +1207 -0
  54. package/dist/i18n/provider.d.ts.map +1 -0
  55. package/dist/i18n/provider.js +44 -0
  56. package/dist/i18n/ro.d.ts +592 -0
  57. package/dist/i18n/ro.d.ts.map +1 -0
  58. package/dist/i18n/ro.js +561 -0
  59. package/dist/i18n/utils.d.ts +4 -0
  60. package/dist/i18n/utils.d.ts.map +1 -0
  61. package/dist/i18n/utils.js +8 -0
  62. package/dist/ui.d.ts +16 -0
  63. package/dist/ui.d.ts.map +1 -0
  64. package/dist/ui.js +14 -0
  65. package/package.json +53 -9
  66. package/src/styles.css +11 -0
package/README.md CHANGED
@@ -1,32 +1,78 @@
1
- # @voyantjs/availability-react
1
+ # @voyantjs/distribution-react
2
2
 
3
- React runtime package for Voyant availability. Provides the shared availability provider, typed fetch client, query keys, constants, and TanStack Query hooks that power availability-focused frontend experiences.
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/availability-react @voyantjs/availability @tanstack/react-query react react-dom zod
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 { VoyantAvailabilityProvider, useSlots } from "@voyantjs/availability-react"
21
+ import { VoyantDistributionProvider, useChannels } from "@voyantjs/distribution-react"
15
22
 
16
23
  function App() {
17
24
  return (
18
- <VoyantAvailabilityProvider baseUrl="/api">
19
- <SlotsList />
20
- </VoyantAvailabilityProvider>
25
+ <VoyantDistributionProvider baseUrl="/api">
26
+ <ChannelsList />
27
+ </VoyantDistributionProvider>
21
28
  )
22
29
  }
23
30
 
24
- function SlotsList() {
25
- const { data } = useSlots()
26
- return <>{data?.data.map((slot) => <div key={slot.id}>{slot.dateLocal}</div>)}</>
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,6 @@
1
+ export interface ChannelsPageProps {
2
+ className?: string;
3
+ pageSize?: number;
4
+ }
5
+ export declare function ChannelsPage({ className, pageSize }?: ChannelsPageProps): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=channels-page.d.ts.map
@@ -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"}