@voyant-travel/distribution-react 0.109.8
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/LICENSE +201 -0
- package/README.md +82 -0
- package/dist/admin/index.d.ts +30 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +65 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +59 -0
- 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-controls.d.ts +15 -0
- package/dist/components/channel-sync-controls.d.ts.map +1 -0
- package/dist/components/channel-sync-controls.js +29 -0
- package/dist/components/channel-sync-deliveries-drawer.d.ts +12 -0
- package/dist/components/channel-sync-deliveries-drawer.d.ts.map +1 -0
- package/dist/components/channel-sync-deliveries-drawer.js +18 -0
- package/dist/components/channel-sync-page-utils.d.ts +99 -0
- package/dist/components/channel-sync-page-utils.d.ts.map +1 -0
- package/dist/components/channel-sync-page-utils.js +85 -0
- package/dist/components/channel-sync-page.d.ts +4 -0
- package/dist/components/channel-sync-page.d.ts.map +1 -0
- package/dist/components/channel-sync-page.js +141 -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/constants.d.ts +103 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +48 -0
- package/dist/external-refs/client.d.ts +14 -0
- package/dist/external-refs/client.d.ts.map +1 -0
- package/dist/external-refs/client.js +58 -0
- package/dist/external-refs/components/entity-ref-picker.d.ts +21 -0
- package/dist/external-refs/components/entity-ref-picker.d.ts.map +1 -0
- package/dist/external-refs/components/entity-ref-picker.js +38 -0
- package/dist/external-refs/components/external-ref-dialog.d.ts +11 -0
- package/dist/external-refs/components/external-ref-dialog.d.ts.map +1 -0
- package/dist/external-refs/components/external-ref-dialog.js +120 -0
- package/dist/external-refs/components/external-refs-page.d.ts +16 -0
- package/dist/external-refs/components/external-refs-page.d.ts.map +1 -0
- package/dist/external-refs/components/external-refs-page.js +109 -0
- package/dist/external-refs/hooks/index.d.ts +3 -0
- package/dist/external-refs/hooks/index.d.ts.map +1 -0
- package/dist/external-refs/hooks/index.js +2 -0
- package/dist/external-refs/hooks/use-external-ref-mutation.d.ts +57 -0
- package/dist/external-refs/hooks/use-external-ref-mutation.d.ts.map +1 -0
- package/dist/external-refs/hooks/use-external-ref-mutation.js +40 -0
- package/dist/external-refs/hooks/use-external-refs.d.ts +26 -0
- package/dist/external-refs/hooks/use-external-refs.d.ts.map +1 -0
- package/dist/external-refs/hooks/use-external-refs.js +12 -0
- package/dist/external-refs/i18n/en.d.ts +3 -0
- package/dist/external-refs/i18n/en.d.ts.map +1 -0
- package/dist/external-refs/i18n/en.js +92 -0
- package/dist/external-refs/i18n/index.d.ts +5 -0
- package/dist/external-refs/i18n/index.d.ts.map +1 -0
- package/dist/external-refs/i18n/index.js +3 -0
- package/dist/external-refs/i18n/messages.d.ts +85 -0
- package/dist/external-refs/i18n/messages.d.ts.map +1 -0
- package/dist/external-refs/i18n/messages.js +1 -0
- package/dist/external-refs/i18n/provider.d.ts +26 -0
- package/dist/external-refs/i18n/provider.d.ts.map +1 -0
- package/dist/external-refs/i18n/provider.js +44 -0
- package/dist/external-refs/i18n/ro.d.ts +3 -0
- package/dist/external-refs/i18n/ro.d.ts.map +1 -0
- package/dist/external-refs/i18n/ro.js +92 -0
- package/dist/external-refs/index.d.ts +7 -0
- package/dist/external-refs/index.d.ts.map +1 -0
- package/dist/external-refs/index.js +6 -0
- package/dist/external-refs/provider.d.ts +2 -0
- package/dist/external-refs/provider.d.ts.map +1 -0
- package/dist/external-refs/provider.js +1 -0
- package/dist/external-refs/query-keys.d.ts +18 -0
- package/dist/external-refs/query-keys.d.ts.map +1 -0
- package/dist/external-refs/query-keys.js +6 -0
- package/dist/external-refs/query-options.d.ts +159 -0
- package/dist/external-refs/query-options.d.ts.map +1 -0
- package/dist/external-refs/query-options.js +31 -0
- package/dist/external-refs/schemas.d.ts +82 -0
- package/dist/external-refs/schemas.d.ts.map +1 -0
- package/dist/external-refs/schemas.js +20 -0
- package/dist/external-refs/ui.d.ts +5 -0
- package/dist/external-refs/ui.d.ts.map +1 -0
- package/dist/external-refs/ui.js +4 -0
- package/dist/hooks/index.d.ts +12 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +12 -0
- package/dist/hooks/use-booking-links.d.ts +20 -0
- package/dist/hooks/use-booking-links.d.ts.map +1 -0
- package/dist/hooks/use-booking-links.js +9 -0
- package/dist/hooks/use-bookings.d.ts +14 -0
- package/dist/hooks/use-bookings.d.ts.map +1 -0
- package/dist/hooks/use-bookings.js +9 -0
- package/dist/hooks/use-channel-mutation.d.ts +69 -0
- package/dist/hooks/use-channel-mutation.d.ts.map +1 -0
- package/dist/hooks/use-channel-mutation.js +49 -0
- package/dist/hooks/use-channel.d.ts +16 -0
- package/dist/hooks/use-channel.d.ts.map +1 -0
- package/dist/hooks/use-channel.js +13 -0
- package/dist/hooks/use-channels.d.ts +20 -0
- package/dist/hooks/use-channels.d.ts.map +1 -0
- package/dist/hooks/use-channels.js +9 -0
- package/dist/hooks/use-commission-rules.d.ts +23 -0
- package/dist/hooks/use-commission-rules.d.ts.map +1 -0
- package/dist/hooks/use-commission-rules.js +9 -0
- package/dist/hooks/use-contracts.d.ts +22 -0
- package/dist/hooks/use-contracts.d.ts.map +1 -0
- package/dist/hooks/use-contracts.js +9 -0
- package/dist/hooks/use-mappings.d.ts +19 -0
- package/dist/hooks/use-mappings.d.ts.map +1 -0
- package/dist/hooks/use-mappings.js +9 -0
- package/dist/hooks/use-products.d.ts +14 -0
- package/dist/hooks/use-products.d.ts.map +1 -0
- package/dist/hooks/use-products.js +9 -0
- package/dist/hooks/use-suppliers.d.ts +14 -0
- package/dist/hooks/use-suppliers.d.ts.map +1 -0
- package/dist/hooks/use-suppliers.js +9 -0
- package/dist/hooks/use-webhook-events.d.ts +21 -0
- package/dist/hooks/use-webhook-events.d.ts.map +1 -0
- package/dist/hooks/use-webhook-events.js +9 -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/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +1 -0
- package/dist/query-keys.d.ts +61 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +30 -0
- package/dist/query-options.d.ts +999 -0
- package/dist/query-options.d.ts.map +1 -0
- package/dist/query-options.js +219 -0
- package/dist/schemas.d.ts +615 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +144 -0
- package/dist/suppliers/admin/index.d.ts +69 -0
- package/dist/suppliers/admin/index.d.ts.map +1 -0
- package/dist/suppliers/admin/index.js +111 -0
- package/dist/suppliers/admin/pages/supplier-detail-page.d.ts +9 -0
- package/dist/suppliers/admin/pages/supplier-detail-page.d.ts.map +1 -0
- package/dist/suppliers/admin/pages/supplier-detail-page.js +11 -0
- package/dist/suppliers/admin/slots.d.ts +19 -0
- package/dist/suppliers/admin/slots.d.ts.map +1 -0
- package/dist/suppliers/admin/slots.js +18 -0
- package/dist/suppliers/admin/supplier-detail-host.d.ts +30 -0
- package/dist/suppliers/admin/supplier-detail-host.d.ts.map +1 -0
- package/dist/suppliers/admin/supplier-detail-host.js +34 -0
- package/dist/suppliers/admin/supplier-detail-skeleton.d.ts +9 -0
- package/dist/suppliers/admin/supplier-detail-skeleton.d.ts.map +1 -0
- package/dist/suppliers/admin/supplier-detail-skeleton.js +20 -0
- package/dist/suppliers/admin/suppliers-host.d.ts +11 -0
- package/dist/suppliers/admin/suppliers-host.d.ts.map +1 -0
- package/dist/suppliers/admin/suppliers-host.js +17 -0
- package/dist/suppliers/admin/suppliers-list-skeleton.d.ts +9 -0
- package/dist/suppliers/admin/suppliers-list-skeleton.d.ts.map +1 -0
- package/dist/suppliers/admin/suppliers-list-skeleton.js +24 -0
- package/dist/suppliers/client.d.ts +14 -0
- package/dist/suppliers/client.d.ts.map +1 -0
- package/dist/suppliers/client.js +59 -0
- package/dist/suppliers/components/message-format.d.ts +2 -0
- package/dist/suppliers/components/message-format.d.ts.map +1 -0
- package/dist/suppliers/components/message-format.js +3 -0
- package/dist/suppliers/components/rate-dialog.d.ts +11 -0
- package/dist/suppliers/components/rate-dialog.d.ts.map +1 -0
- package/dist/suppliers/components/rate-dialog.js +93 -0
- package/dist/suppliers/components/service-dialog.d.ts +10 -0
- package/dist/suppliers/components/service-dialog.d.ts.map +1 -0
- package/dist/suppliers/components/service-dialog.js +69 -0
- package/dist/suppliers/components/supplier-combobox.d.ts +13 -0
- package/dist/suppliers/components/supplier-combobox.d.ts.map +1 -0
- package/dist/suppliers/components/supplier-combobox.js +23 -0
- package/dist/suppliers/components/supplier-detail-page.d.ts +17 -0
- package/dist/suppliers/components/supplier-detail-page.d.ts.map +1 -0
- package/dist/suppliers/components/supplier-detail-page.js +105 -0
- package/dist/suppliers/components/supplier-dialog.d.ts +9 -0
- package/dist/suppliers/components/supplier-dialog.d.ts.map +1 -0
- package/dist/suppliers/components/supplier-dialog.js +118 -0
- package/dist/suppliers/components/supplier-service-row.d.ts +22 -0
- package/dist/suppliers/components/supplier-service-row.d.ts.map +1 -0
- package/dist/suppliers/components/supplier-service-row.js +18 -0
- package/dist/suppliers/components/suppliers-page.d.ts +10 -0
- package/dist/suppliers/components/suppliers-page.d.ts.map +1 -0
- package/dist/suppliers/components/suppliers-page.js +94 -0
- package/dist/suppliers/constants.d.ts +69 -0
- package/dist/suppliers/constants.d.ts.map +1 -0
- package/dist/suppliers/constants.js +34 -0
- package/dist/suppliers/hooks/index.d.ts +10 -0
- package/dist/suppliers/hooks/index.d.ts.map +1 -0
- package/dist/suppliers/hooks/index.js +10 -0
- package/dist/suppliers/hooks/use-supplier-mutation.d.ts +95 -0
- package/dist/suppliers/hooks/use-supplier-mutation.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-mutation.js +41 -0
- package/dist/suppliers/hooks/use-supplier-note-mutation.d.ts +13 -0
- package/dist/suppliers/hooks/use-supplier-note-mutation.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-note-mutation.js +20 -0
- package/dist/suppliers/hooks/use-supplier-notes.d.ts +13 -0
- package/dist/suppliers/hooks/use-supplier-notes.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-notes.js +12 -0
- package/dist/suppliers/hooks/use-supplier-rate-mutation.d.ts +56 -0
- package/dist/suppliers/hooks/use-supplier-rate-mutation.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-rate-mutation.js +41 -0
- package/dist/suppliers/hooks/use-supplier-service-mutation.d.ts +45 -0
- package/dist/suppliers/hooks/use-supplier-service-mutation.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-service-mutation.js +44 -0
- package/dist/suppliers/hooks/use-supplier-service-rates.d.ts +20 -0
- package/dist/suppliers/hooks/use-supplier-service-rates.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-service-rates.js +12 -0
- package/dist/suppliers/hooks/use-supplier-services.d.ts +19 -0
- package/dist/suppliers/hooks/use-supplier-services.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier-services.js +12 -0
- package/dist/suppliers/hooks/use-supplier.d.ts +38 -0
- package/dist/suppliers/hooks/use-supplier.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-supplier.js +9 -0
- package/dist/suppliers/hooks/use-suppliers.d.ts +42 -0
- package/dist/suppliers/hooks/use-suppliers.d.ts.map +1 -0
- package/dist/suppliers/hooks/use-suppliers.js +9 -0
- package/dist/suppliers/i18n/en.d.ts +204 -0
- package/dist/suppliers/i18n/en.d.ts.map +1 -0
- package/dist/suppliers/i18n/en.js +203 -0
- package/dist/suppliers/i18n/index.d.ts +5 -0
- package/dist/suppliers/i18n/index.d.ts.map +1 -0
- package/dist/suppliers/i18n/index.js +3 -0
- package/dist/suppliers/i18n/messages.d.ts +187 -0
- package/dist/suppliers/i18n/messages.d.ts.map +1 -0
- package/dist/suppliers/i18n/messages.js +1 -0
- package/dist/suppliers/i18n/provider.d.ts +430 -0
- package/dist/suppliers/i18n/provider.d.ts.map +1 -0
- package/dist/suppliers/i18n/provider.js +44 -0
- package/dist/suppliers/i18n/ro.d.ts +204 -0
- package/dist/suppliers/i18n/ro.d.ts.map +1 -0
- package/dist/suppliers/i18n/ro.js +203 -0
- package/dist/suppliers/index.d.ts +9 -0
- package/dist/suppliers/index.d.ts.map +1 -0
- package/dist/suppliers/index.js +8 -0
- package/dist/suppliers/provider.d.ts +2 -0
- package/dist/suppliers/provider.d.ts.map +1 -0
- package/dist/suppliers/provider.js +1 -0
- package/dist/suppliers/query-keys.d.ts +29 -0
- package/dist/suppliers/query-keys.d.ts.map +1 -0
- package/dist/suppliers/query-keys.js +12 -0
- package/dist/suppliers/query-options.d.ts +467 -0
- package/dist/suppliers/query-options.d.ts.map +1 -0
- package/dist/suppliers/query-options.js +63 -0
- package/dist/suppliers/schemas.d.ts +363 -0
- package/dist/suppliers/schemas.d.ts.map +1 -0
- package/dist/suppliers/schemas.js +109 -0
- package/dist/suppliers/ui.d.ts +9 -0
- package/dist/suppliers/ui.d.ts.map +1 -0
- package/dist/suppliers/ui.js +8 -0
- package/dist/suppliers/utils.d.ts +4 -0
- package/dist/suppliers/utils.d.ts.map +1 -0
- package/dist/suppliers/utils.js +10 -0
- package/dist/ui.d.ts +16 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +14 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +44 -0
- package/package.json +263 -0
- package/src/external-refs/styles.css +11 -0
- package/src/styles.css +11 -0
- package/src/suppliers/styles.css +11 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@voyant-travel/ui/components";
|
|
3
|
+
import { ChevronDown, Loader2, RotateCw } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { formatShortDuration, formatTemplate, } from "./channel-sync-page-utils.js";
|
|
6
|
+
export function ReconcileMenu({ onRun, isRunning, lastResult, messages, }) {
|
|
7
|
+
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, {
|
|
8
|
+
scanned: lastResult.scanned,
|
|
9
|
+
triggered: lastResult.triggered,
|
|
10
|
+
}) })] })) : null] })] }));
|
|
11
|
+
}
|
|
12
|
+
export function AutoRefreshIndicator({ isFetching, dataUpdatedAt, intervalMs, messages, }) {
|
|
13
|
+
// Tick every second so the "Updated Xs ago" stays current.
|
|
14
|
+
const [, setNow] = useState(Date.now());
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const id = window.setInterval(() => setNow(Date.now()), 1000);
|
|
17
|
+
return () => window.clearInterval(id);
|
|
18
|
+
}, []);
|
|
19
|
+
if (!dataUpdatedAt) {
|
|
20
|
+
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] }));
|
|
21
|
+
}
|
|
22
|
+
const seconds = Math.max(0, Math.round((Date.now() - dataUpdatedAt) / 1000));
|
|
23
|
+
const intervalSec = Math.round(intervalMs / 1000);
|
|
24
|
+
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
|
|
25
|
+
? messages.refresh.refreshing
|
|
26
|
+
: formatTemplate(messages.refresh.updatedAgo, {
|
|
27
|
+
duration: formatShortDuration(seconds),
|
|
28
|
+
}) })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { useDistributionUiMessagesOrDefault } from "../i18n/index.js";
|
|
2
|
+
import type { VoyantFetcher } from "../index.js";
|
|
3
|
+
export declare function DeliveriesDrawer({ bookingId, client, onClose, messages, }: {
|
|
4
|
+
bookingId: string | null;
|
|
5
|
+
client: {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
fetcher: VoyantFetcher;
|
|
8
|
+
};
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
messages: ReturnType<typeof useDistributionUiMessagesOrDefault>["channelSync"];
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=channel-sync-deliveries-drawer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-sync-deliveries-drawer.d.ts","sourceRoot":"","sources":["../../src/components/channel-sync-deliveries-drawer.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAQhD,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,MAAM,EACN,OAAO,EACP,QAAQ,GACT,EAAE;IACD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,aAAa,CAAA;KAAE,CAAA;IACnD,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,UAAU,CAAC,OAAO,kCAAkC,CAAC,CAAC,aAAa,CAAC,CAAA;CAC/E,2CAmFA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { Badge, Card, CardContent, CardDescription, CardHeader, Empty, EmptyDescription, EmptyHeader, EmptyTitle, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, } from "@voyant-travel/ui/components";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { fetchJson, formatRelative, formatTemplate, } from "./channel-sync-page-utils.js";
|
|
6
|
+
export function DeliveriesDrawer({ bookingId, client, onClose, messages, }) {
|
|
7
|
+
const isOpen = bookingId !== null;
|
|
8
|
+
const query = useQuery({
|
|
9
|
+
enabled: isOpen,
|
|
10
|
+
queryKey: ["channel-push-deliveries", bookingId],
|
|
11
|
+
queryFn: () => {
|
|
12
|
+
const params = new URLSearchParams({ bookingId: bookingId ?? "", limit: "200" });
|
|
13
|
+
return fetchJson(`/v1/admin/distribution?${params}`, client);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const rows = query.data?.data ?? [];
|
|
17
|
+
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)))) })] }) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { VoyantFetcher } from "../index.js";
|
|
2
|
+
export type PushStatus = "pending" | "ok" | "failed" | "compensated";
|
|
3
|
+
export interface ChannelBookingLinkRow {
|
|
4
|
+
link: {
|
|
5
|
+
id: string;
|
|
6
|
+
channelId: string;
|
|
7
|
+
bookingId: string;
|
|
8
|
+
bookingItemId: string | null;
|
|
9
|
+
sourceKind: string | null;
|
|
10
|
+
sourceConnectionId: string | null;
|
|
11
|
+
pushStatus: PushStatus | string;
|
|
12
|
+
pushAttempts: number;
|
|
13
|
+
lastPushAt: string | null;
|
|
14
|
+
lastError: string | null;
|
|
15
|
+
externalBookingId: string | null;
|
|
16
|
+
externalReference: string | null;
|
|
17
|
+
externalStatus: string | null;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
};
|
|
20
|
+
channelName: string;
|
|
21
|
+
channelKind: string;
|
|
22
|
+
}
|
|
23
|
+
export interface LinksResponse {
|
|
24
|
+
data: ChannelBookingLinkRow[];
|
|
25
|
+
counts: Record<string, number>;
|
|
26
|
+
}
|
|
27
|
+
export interface DeliveryRow {
|
|
28
|
+
id: string;
|
|
29
|
+
sourceModule: string;
|
|
30
|
+
sourceEvent: string;
|
|
31
|
+
sourceEntityId: string | null;
|
|
32
|
+
targetUrl: string;
|
|
33
|
+
targetKind: string | null;
|
|
34
|
+
targetRef: string | null;
|
|
35
|
+
requestMethod: string;
|
|
36
|
+
responseStatus: number | null;
|
|
37
|
+
responseBodyExcerpt: string | null;
|
|
38
|
+
attemptNumber: number;
|
|
39
|
+
status: string;
|
|
40
|
+
errorClass: string | null;
|
|
41
|
+
errorMessage: string | null;
|
|
42
|
+
durationMs: number | null;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
}
|
|
45
|
+
export interface DeliveriesResponse {
|
|
46
|
+
data: DeliveryRow[];
|
|
47
|
+
}
|
|
48
|
+
export interface ThrottlingRow {
|
|
49
|
+
channelId: string | null;
|
|
50
|
+
count: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ThrottlingResponse {
|
|
53
|
+
data: ThrottlingRow[];
|
|
54
|
+
sinceMs: number;
|
|
55
|
+
}
|
|
56
|
+
export interface ReconcilerResult {
|
|
57
|
+
scanned: number;
|
|
58
|
+
triggered: number;
|
|
59
|
+
}
|
|
60
|
+
export interface BookingRecord {
|
|
61
|
+
id: string;
|
|
62
|
+
bookingNumber: string;
|
|
63
|
+
status: string;
|
|
64
|
+
}
|
|
65
|
+
export interface BookingsResponse {
|
|
66
|
+
data: BookingRecord[];
|
|
67
|
+
}
|
|
68
|
+
export interface ChannelRecord {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
kind: string;
|
|
72
|
+
status: string;
|
|
73
|
+
}
|
|
74
|
+
export interface ChannelsResponse {
|
|
75
|
+
data: ChannelRecord[];
|
|
76
|
+
}
|
|
77
|
+
export interface ChannelSyncPageProps {
|
|
78
|
+
baseUrl?: string;
|
|
79
|
+
fetcher?: VoyantFetcher;
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|
|
82
|
+
export declare function fetchJson<T>(path: string, options: {
|
|
83
|
+
baseUrl: string;
|
|
84
|
+
fetcher: VoyantFetcher;
|
|
85
|
+
}, init?: RequestInit): Promise<T>;
|
|
86
|
+
export declare const STATUS_VARIANTS: Record<string, "default" | "secondary" | "destructive" | "outline">;
|
|
87
|
+
export declare const STATUS_TILES: ReadonlyArray<{
|
|
88
|
+
key: PushStatus;
|
|
89
|
+
tone: "default" | "secondary" | "destructive" | "outline";
|
|
90
|
+
}>;
|
|
91
|
+
export declare const LINKS_REFETCH_MS = 15000;
|
|
92
|
+
export declare const THROTTLING_REFETCH_MS = 60000;
|
|
93
|
+
export declare function useDebouncedValue<T>(value: T, delayMs: number): T;
|
|
94
|
+
export declare function joinUrl(baseUrl: string, path: string): string;
|
|
95
|
+
export declare function formatChannelKind(kind: string): string;
|
|
96
|
+
export declare function formatShortDuration(seconds: number): string;
|
|
97
|
+
export declare function formatRelative(iso: string): string;
|
|
98
|
+
export declare function formatTemplate(template: string, values: Record<string, string | number>): string;
|
|
99
|
+
//# sourceMappingURL=channel-sync-page-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-sync-page-utils.d.ts","sourceRoot":"","sources":["../../src/components/channel-sync-page-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
// Fetch helpers
|
|
3
|
+
export async function fetchJson(path, options, init) {
|
|
4
|
+
const res = await options.fetcher(joinUrl(options.baseUrl, path), {
|
|
5
|
+
...init,
|
|
6
|
+
headers: {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
...init?.headers,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
const text = await res.text();
|
|
12
|
+
const body = text
|
|
13
|
+
? JSON.parse(text)
|
|
14
|
+
: {};
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw new Error(body.error ?? `Request failed: ${res.status}`);
|
|
17
|
+
}
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
20
|
+
export const STATUS_VARIANTS = {
|
|
21
|
+
pending: "secondary",
|
|
22
|
+
ok: "default",
|
|
23
|
+
failed: "destructive",
|
|
24
|
+
compensated: "outline",
|
|
25
|
+
};
|
|
26
|
+
export const STATUS_TILES = [
|
|
27
|
+
{ key: "pending", tone: "secondary" },
|
|
28
|
+
{ key: "ok", tone: "default" },
|
|
29
|
+
{ key: "failed", tone: "destructive" },
|
|
30
|
+
{ key: "compensated", tone: "outline" },
|
|
31
|
+
];
|
|
32
|
+
export const LINKS_REFETCH_MS = 15_000;
|
|
33
|
+
export const THROTTLING_REFETCH_MS = 60_000;
|
|
34
|
+
export function useDebouncedValue(value, delayMs) {
|
|
35
|
+
const [debounced, setDebounced] = useState(value);
|
|
36
|
+
const timeoutRef = useRef(null);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (timeoutRef.current)
|
|
39
|
+
clearTimeout(timeoutRef.current);
|
|
40
|
+
timeoutRef.current = setTimeout(() => setDebounced(value), delayMs);
|
|
41
|
+
return () => {
|
|
42
|
+
if (timeoutRef.current)
|
|
43
|
+
clearTimeout(timeoutRef.current);
|
|
44
|
+
};
|
|
45
|
+
}, [value, delayMs]);
|
|
46
|
+
return debounced;
|
|
47
|
+
}
|
|
48
|
+
export function joinUrl(baseUrl, path) {
|
|
49
|
+
const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
50
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`;
|
|
51
|
+
return `${trimmedBase}${trimmedPath}`;
|
|
52
|
+
}
|
|
53
|
+
export function formatChannelKind(kind) {
|
|
54
|
+
return kind.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase());
|
|
55
|
+
}
|
|
56
|
+
export function formatShortDuration(seconds) {
|
|
57
|
+
if (seconds < 60)
|
|
58
|
+
return `${seconds}s`;
|
|
59
|
+
const min = Math.round(seconds / 60);
|
|
60
|
+
if (min < 60)
|
|
61
|
+
return `${min}m`;
|
|
62
|
+
const hours = Math.round(min / 60);
|
|
63
|
+
return `${hours}h`;
|
|
64
|
+
}
|
|
65
|
+
export function formatRelative(iso) {
|
|
66
|
+
const date = new Date(iso);
|
|
67
|
+
const diffMs = Date.now() - date.getTime();
|
|
68
|
+
const sec = Math.round(diffMs / 1000);
|
|
69
|
+
if (sec < 60)
|
|
70
|
+
return `${sec}s ago`;
|
|
71
|
+
const min = Math.round(sec / 60);
|
|
72
|
+
if (min < 60)
|
|
73
|
+
return `${min}m ago`;
|
|
74
|
+
const hours = Math.round(min / 60);
|
|
75
|
+
if (hours < 24)
|
|
76
|
+
return `${hours}h ago`;
|
|
77
|
+
const days = Math.round(hours / 24);
|
|
78
|
+
return `${days}d ago`;
|
|
79
|
+
}
|
|
80
|
+
export function formatTemplate(template, values) {
|
|
81
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
82
|
+
const value = values[key];
|
|
83
|
+
return value === undefined ? "" : String(value);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ChannelSyncPageProps } from "./channel-sync-page-utils.js";
|
|
2
|
+
export type { ChannelSyncPageProps } from "./channel-sync-page-utils.js";
|
|
3
|
+
export declare function ChannelSyncPage({ baseUrl, fetcher, className }?: ChannelSyncPageProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
//# 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":"AAmCA,OAAO,EAIL,KAAK,oBAAoB,EAe1B,MAAM,8BAA8B,CAAA;AAErC,YAAY,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AAIxE,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAE,oBAAyB,2CA2WzF"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Label, } from "@voyant-travel/ui/components";
|
|
5
|
+
import { AsyncCombobox } from "@voyant-travel/ui/components/async-combobox";
|
|
6
|
+
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle, } from "@voyant-travel/ui/components/empty";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyant-travel/ui/components/table";
|
|
8
|
+
import { AlertTriangle, Loader2, X } from "lucide-react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { useDistributionUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
|
+
import { defaultFetcher, useVoyantDistributionContext } from "../index.js";
|
|
12
|
+
import { AutoRefreshIndicator, ReconcileMenu } from "./channel-sync-controls.js";
|
|
13
|
+
import { DeliveriesDrawer } from "./channel-sync-deliveries-drawer.js";
|
|
14
|
+
import { fetchJson, formatChannelKind, formatRelative, formatTemplate, LINKS_REFETCH_MS, STATUS_TILES, STATUS_VARIANTS, THROTTLING_REFETCH_MS, useDebouncedValue, } from "./channel-sync-page-utils.js";
|
|
15
|
+
// Page
|
|
16
|
+
export function ChannelSyncPage({ baseUrl, fetcher, className } = {}) {
|
|
17
|
+
const distributionMessages = useDistributionUiMessagesOrDefault();
|
|
18
|
+
const messages = distributionMessages.channelSync;
|
|
19
|
+
const context = useVoyantDistributionContext();
|
|
20
|
+
const client = {
|
|
21
|
+
baseUrl: baseUrl ?? context.baseUrl,
|
|
22
|
+
fetcher: fetcher ?? context.fetcher ?? defaultFetcher,
|
|
23
|
+
};
|
|
24
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
25
|
+
const [bookingId, setBookingId] = useState(null);
|
|
26
|
+
const [bookingSearch, setBookingSearch] = useState("");
|
|
27
|
+
const [selectedBooking, setSelectedBooking] = useState(null);
|
|
28
|
+
const [channelId, setChannelId] = useState(null);
|
|
29
|
+
const [selectedChannel, setSelectedChannel] = useState(null);
|
|
30
|
+
const [drilldownBookingId, setDrilldownBookingId] = useState(null);
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
const linksQuery = useQuery({
|
|
33
|
+
queryKey: ["channel-push-links", statusFilter, bookingId, channelId],
|
|
34
|
+
queryFn: () => {
|
|
35
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
36
|
+
if (statusFilter !== "all")
|
|
37
|
+
params.set("status", statusFilter);
|
|
38
|
+
if (bookingId)
|
|
39
|
+
params.set("bookingId", bookingId);
|
|
40
|
+
if (channelId)
|
|
41
|
+
params.set("channelId", channelId);
|
|
42
|
+
return fetchJson(`/v1/admin/distribution?${params}`, client);
|
|
43
|
+
},
|
|
44
|
+
refetchInterval: LINKS_REFETCH_MS,
|
|
45
|
+
refetchIntervalInBackground: false,
|
|
46
|
+
});
|
|
47
|
+
const throttlingQuery = useQuery({
|
|
48
|
+
queryKey: ["channel-push-throttling"],
|
|
49
|
+
queryFn: () => fetchJson("/v1/admin/distribution", client),
|
|
50
|
+
refetchInterval: THROTTLING_REFETCH_MS,
|
|
51
|
+
});
|
|
52
|
+
const debouncedBookingSearch = useDebouncedValue(bookingSearch, 200);
|
|
53
|
+
const bookingsQuery = useQuery({
|
|
54
|
+
queryKey: ["channel-sync-booking-options", debouncedBookingSearch],
|
|
55
|
+
queryFn: () => {
|
|
56
|
+
const params = new URLSearchParams({ limit: "20" });
|
|
57
|
+
if (debouncedBookingSearch.trim())
|
|
58
|
+
params.set("search", debouncedBookingSearch.trim());
|
|
59
|
+
return fetchJson(`/v1/admin/bookings?${params}`, client);
|
|
60
|
+
},
|
|
61
|
+
placeholderData: (prev) => prev,
|
|
62
|
+
});
|
|
63
|
+
const channelsQuery = useQuery({
|
|
64
|
+
queryKey: ["channel-sync-channel-options"],
|
|
65
|
+
queryFn: () => fetchJson(`/v1/admin/distribution/channels?limit=100`, client),
|
|
66
|
+
staleTime: 60_000,
|
|
67
|
+
});
|
|
68
|
+
const retryMutation = useMutation({
|
|
69
|
+
mutationFn: (id) => fetchJson(`/v1/admin/distribution/${id}`, client, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
}),
|
|
72
|
+
onSuccess: () => {
|
|
73
|
+
void queryClient.invalidateQueries({ queryKey: ["channel-push-links"] });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const reconcileMutation = useMutation({
|
|
77
|
+
mutationFn: (flow) => fetchJson(`/v1/admin/distribution/${flow}`, client, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
}),
|
|
80
|
+
onSuccess: () => {
|
|
81
|
+
void queryClient.invalidateQueries({ queryKey: ["channel-push-links"] });
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const counts = linksQuery.data?.counts ?? {};
|
|
85
|
+
const rows = linksQuery.data?.data ?? [];
|
|
86
|
+
const throttledChannels = throttlingQuery.data?.data ?? [];
|
|
87
|
+
const isThrottled = throttledChannels.length > 0;
|
|
88
|
+
const filtersActive = statusFilter !== "all" || bookingId !== null || channelId !== null;
|
|
89
|
+
const clearFilters = () => {
|
|
90
|
+
setStatusFilter("all");
|
|
91
|
+
setBookingId(null);
|
|
92
|
+
setBookingSearch("");
|
|
93
|
+
setSelectedBooking(null);
|
|
94
|
+
setChannelId(null);
|
|
95
|
+
setSelectedChannel(null);
|
|
96
|
+
};
|
|
97
|
+
const bookingOptions = bookingsQuery.data?.data ?? [];
|
|
98
|
+
const channelOptions = channelsQuery.data?.data ?? [];
|
|
99
|
+
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, {
|
|
100
|
+
count: throttledChannels.reduce((sum, c) => sum + c.count, 0),
|
|
101
|
+
channels: throttledChannels.length,
|
|
102
|
+
channelLabel: throttledChannels.length === 1 ? "channel" : "channels",
|
|
103
|
+
}) })] })] })) : null, _jsx("div", { className: "grid grid-cols-2 gap-3 md:grid-cols-4", children: STATUS_TILES.map((tile) => {
|
|
104
|
+
const isActive = statusFilter === tile.key;
|
|
105
|
+
const value = counts[tile.key] ?? 0;
|
|
106
|
+
const tileMessages = messages.statusTiles[tile.key];
|
|
107
|
+
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));
|
|
108
|
+
}) }), _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) => {
|
|
109
|
+
setBookingId(value);
|
|
110
|
+
if (!value)
|
|
111
|
+
setSelectedBooking(null);
|
|
112
|
+
else {
|
|
113
|
+
const match = bookingOptions.find((b) => b.id === value);
|
|
114
|
+
if (match)
|
|
115
|
+
setSelectedBooking(match);
|
|
116
|
+
}
|
|
117
|
+
}, 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
|
|
118
|
+
? messages.filters.bookingSearching
|
|
119
|
+
: 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) => {
|
|
120
|
+
setChannelId(value);
|
|
121
|
+
if (!value)
|
|
122
|
+
setSelectedChannel(null);
|
|
123
|
+
else {
|
|
124
|
+
const match = channelOptions.find((c) => c.id === value);
|
|
125
|
+
if (match)
|
|
126
|
+
setSelectedChannel(match);
|
|
127
|
+
}
|
|
128
|
+
}, 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
|
|
129
|
+
? formatTemplate(messages.table.filteredDescription, { count: rows.length })
|
|
130
|
+
: 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
|
|
131
|
+
? messages.table.noMatchesDescription
|
|
132
|
+
: 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) => {
|
|
133
|
+
const isFailed = row.link.pushStatus === "failed";
|
|
134
|
+
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, {
|
|
135
|
+
id: row.link.bookingItemId,
|
|
136
|
+
}) })) : 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] ??
|
|
137
|
+
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 &&
|
|
138
|
+
retryMutation.variables === row.link.bookingId, onClick: () => retryMutation.mutate(row.link.bookingId), children: [retryMutation.isPending &&
|
|
139
|
+
retryMutation.variables === row.link.bookingId ? (_jsx(Loader2, { className: "mr-1 h-3 w-3 animate-spin" })) : null, messages.table.retry] })] }) })] }, row.link.id));
|
|
140
|
+
}) })] })) })] }), _jsx(DeliveriesDrawer, { bookingId: drilldownBookingId, client: client, onClose: () => setDrilldownBookingId(null), messages: messages })] }));
|
|
141
|
+
}
|
|
@@ -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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyant-travel/ui/components";
|
|
4
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
5
|
+
import { Loader2, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { useDistributionUiI18nOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { useChannelMutation, useChannels, } from "../index.js";
|
|
9
|
+
const PAGE_SIZE = 25;
|
|
10
|
+
const defaultFormValues = {
|
|
11
|
+
name: "",
|
|
12
|
+
kind: "direct",
|
|
13
|
+
status: "active",
|
|
14
|
+
website: "",
|
|
15
|
+
contactName: "",
|
|
16
|
+
contactEmail: "",
|
|
17
|
+
};
|
|
18
|
+
export function ChannelsPage({ className, pageSize = PAGE_SIZE } = {}) {
|
|
19
|
+
const { messages } = useDistributionUiI18nOrDefault();
|
|
20
|
+
const page = messages.settings.channelsPage;
|
|
21
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
22
|
+
const [editing, setEditing] = useState();
|
|
23
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
24
|
+
const { data, isPending, refetch } = useChannels({
|
|
25
|
+
limit: pageSize,
|
|
26
|
+
offset: pageIndex * pageSize,
|
|
27
|
+
});
|
|
28
|
+
const { remove } = useChannelMutation();
|
|
29
|
+
const channels = data?.data ?? [];
|
|
30
|
+
const total = data?.total ?? 0;
|
|
31
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
32
|
+
return (_jsxs("div", { "data-slot": "channels-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: page.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: page.description })] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
33
|
+
setEditing(undefined);
|
|
34
|
+
setSheetOpen(true);
|
|
35
|
+
}, children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), page.addChannel] })] }), isPending ? (_jsx(ChannelsListSkeleton, {})) : (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: channels.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-muted-foreground", children: page.empty })) : (_jsx("div", { className: "flex flex-col divide-y", children: channels.map((channel) => (_jsxs("div", { className: "flex items-center justify-between px-6 py-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: channel.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: messages.common.channelKindLabels[channel.kind] }), channel.status !== "active" ? (_jsx(Badge, { variant: "secondary", className: "text-xs", children: messages.common.channelStatusLabels[channel.status] })) : null] }), _jsxs("div", { className: "flex flex-wrap gap-3 text-xs text-muted-foreground", children: [channel.website ? _jsx("span", { children: channel.website }) : null, channel.contactName ? _jsx("span", { children: channel.contactName }) : null, channel.contactEmail ? _jsx("span", { children: channel.contactEmail }) : null] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => {
|
|
36
|
+
setEditing(channel);
|
|
37
|
+
setSheetOpen(true);
|
|
38
|
+
}, children: [_jsx(Pencil, { className: "h-4 w-4" }), page.edit] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", disabled: remove.isPending, onClick: () => {
|
|
39
|
+
if (window.confirm(page.deleteConfirm)) {
|
|
40
|
+
void remove.mutateAsync(channel.id).then(() => refetch());
|
|
41
|
+
}
|
|
42
|
+
}, children: [_jsx(Trash2, { className: "h-4 w-4" }), page.delete] })] })] })] }, channel.id))) })) })), _jsxs("div", { className: "flex items-center justify-between gap-4 text-sm text-muted-foreground", children: [_jsx("span", { children: page.paginationShowing
|
|
43
|
+
.replace("{count}", String(channels.length))
|
|
44
|
+
.replace("{total}", String(total)) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: pageIndex === 0, onClick: () => setPageIndex((current) => Math.max(0, current - 1)), children: page.paginationPrevious }), _jsx("span", { children: page.paginationPage
|
|
45
|
+
.replace("{page}", String(pageIndex + 1))
|
|
46
|
+
.replace("{pageCount}", String(pageCount)) }), _jsx(Button, { variant: "outline", size: "sm", disabled: (pageIndex + 1) * pageSize >= total, onClick: () => setPageIndex((current) => current + 1), children: page.paginationNext })] })] }), _jsx(ChannelSheet, { open: sheetOpen, onOpenChange: setSheetOpen, channel: editing, onSuccess: () => {
|
|
47
|
+
setSheetOpen(false);
|
|
48
|
+
setEditing(undefined);
|
|
49
|
+
void refetch();
|
|
50
|
+
} })] }));
|
|
51
|
+
}
|
|
52
|
+
function ChannelSheet({ open, onOpenChange, channel, onSuccess, }) {
|
|
53
|
+
const { messages } = useDistributionUiI18nOrDefault();
|
|
54
|
+
const page = messages.settings.channelsPage;
|
|
55
|
+
const isEditing = !!channel;
|
|
56
|
+
const { create, update } = useChannelMutation();
|
|
57
|
+
const [values, setValues] = useState(defaultFormValues);
|
|
58
|
+
const [errors, setErrors] = useState({});
|
|
59
|
+
const channelKinds = Object.entries(messages.common.channelKindLabels).map(([value, label]) => ({
|
|
60
|
+
value: value,
|
|
61
|
+
label,
|
|
62
|
+
}));
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (open && channel) {
|
|
65
|
+
setValues({
|
|
66
|
+
name: channel.name,
|
|
67
|
+
kind: channel.kind,
|
|
68
|
+
status: channel.status,
|
|
69
|
+
website: channel.website ?? "",
|
|
70
|
+
contactName: channel.contactName ?? "",
|
|
71
|
+
contactEmail: channel.contactEmail ?? "",
|
|
72
|
+
});
|
|
73
|
+
setErrors({});
|
|
74
|
+
}
|
|
75
|
+
else if (open) {
|
|
76
|
+
setValues(defaultFormValues);
|
|
77
|
+
setErrors({});
|
|
78
|
+
}
|
|
79
|
+
}, [open, channel]);
|
|
80
|
+
const isSubmitting = create.isPending || update.isPending;
|
|
81
|
+
const setValue = (key, value) => setValues((current) => ({ ...current, [key]: value }));
|
|
82
|
+
const onSubmit = async (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
const nextErrors = validateChannelForm(values, page);
|
|
85
|
+
setErrors(nextErrors);
|
|
86
|
+
if (Object.keys(nextErrors).length > 0)
|
|
87
|
+
return;
|
|
88
|
+
const payload = {
|
|
89
|
+
name: values.name.trim(),
|
|
90
|
+
kind: values.kind,
|
|
91
|
+
status: values.status,
|
|
92
|
+
website: normalizeOptional(values.website),
|
|
93
|
+
contactName: normalizeOptional(values.contactName),
|
|
94
|
+
contactEmail: normalizeOptional(values.contactEmail),
|
|
95
|
+
};
|
|
96
|
+
if (isEditing) {
|
|
97
|
+
await update.mutateAsync({ id: channel.id, input: payload });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await create.mutateAsync(payload);
|
|
101
|
+
}
|
|
102
|
+
onSuccess();
|
|
103
|
+
};
|
|
104
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? page.editSheetTitle : page.newSheetTitle }) }), _jsxs("form", { onSubmit: onSubmit, className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(SheetBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: page.nameLabel }), _jsx(Input, { value: values.name, onChange: (event) => setValue("name", event.target.value), placeholder: page.namePlaceholder, autoFocus: true }), errors.name ? _jsx("p", { className: "text-xs text-destructive", children: errors.name }) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: page.kindLabel }), _jsxs(Select, { items: channelKinds, value: values.kind, onValueChange: (value) => setValue("kind", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: channelKinds.map((kind) => (_jsx(SelectItem, { value: kind.value, children: kind.label }, kind.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: page.statusLabel }), _jsxs(Select, { value: values.status, onValueChange: (value) => setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.entries(messages.common.channelStatusLabels).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: page.websiteLabel }), _jsx(Input, { value: values.website, onChange: (event) => setValue("website", event.target.value), placeholder: page.websitePlaceholder }), errors.website ? _jsx("p", { className: "text-xs text-destructive", children: errors.website }) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: page.primaryContactLabel }), _jsx(Input, { value: values.contactName, onChange: (event) => setValue("contactName", event.target.value), placeholder: page.primaryContactPlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: page.contactEmailLabel }), _jsx(Input, { value: values.contactEmail, onChange: (event) => setValue("contactEmail", event.target.value), placeholder: page.contactEmailPlaceholder }), errors.contactEmail ? (_jsx("p", { className: "text-xs text-destructive", children: errors.contactEmail })) : null] })] })] }), _jsxs(SheetFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", size: "sm", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? page.saveChanges : page.createChannel] })] })] })] }) }));
|
|
105
|
+
}
|
|
106
|
+
function ChannelsListSkeleton() {
|
|
107
|
+
const rows = ["first", "second", "third", "fourth", "fifth"];
|
|
108
|
+
return (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: rows.map((row) => (_jsxs("div", { className: "flex items-center justify-between border-b px-6 py-3 last:border-b-0", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "h-4 w-44 rounded bg-muted" }), _jsx("div", { className: "h-3 w-64 rounded bg-muted" })] }), _jsx("div", { className: "h-8 w-8 rounded bg-muted" })] }, row))) }));
|
|
109
|
+
}
|
|
110
|
+
function validateChannelForm(values, page) {
|
|
111
|
+
const errors = {};
|
|
112
|
+
if (!values.name.trim())
|
|
113
|
+
errors.name = page.validationNameRequired;
|
|
114
|
+
if (values.name.length > 255)
|
|
115
|
+
errors.name = page.validationNameRequired;
|
|
116
|
+
if (values.website.trim()) {
|
|
117
|
+
try {
|
|
118
|
+
new URL(values.website.trim());
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
errors.website = page.validationInvalidUrl;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (values.contactEmail.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.contactEmail)) {
|
|
125
|
+
errors.contactEmail = page.validationInvalidEmail;
|
|
126
|
+
}
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
function normalizeOptional(value) {
|
|
130
|
+
const trimmed = value.trim();
|
|
131
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CommissionRuleDetailPageProps {
|
|
2
|
+
id: string;
|
|
3
|
+
className?: string;
|
|
4
|
+
onBack?: () => void;
|
|
5
|
+
onDeleted?: () => void;
|
|
6
|
+
onContractOpen?: (contractId: string) => void;
|
|
7
|
+
onProductOpen?: (productId: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function CommissionRuleDetailPage({ id, className, onBack, onDeleted, onContractOpen, onProductOpen, }: CommissionRuleDetailPageProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=commission-rule-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commission-rule-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/commission-rule-detail-page.tsx"],"names":[],"mappings":"AAyBA,MAAM,WAAW,6BAA6B;IAC5C,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;CAC5C;AAID,wBAAgB,wBAAwB,CAAC,EACvC,EAAE,EACF,SAAS,EACT,MAAa,EACb,SAAgB,EAChB,cAAqB,EACrB,aAAoB,GACrB,EAAE,6BAA6B,2CAgK/B"}
|