| 1 |
import { r as reactExports, u as useQuery, j as jsxRuntimeExports, N as Network, b$ as Link, Q as Inbox, as as Clock, V as AlertTriangle, A as Activity, aG as Info, Z as Zap, b2 as Globe, bD as ShieldCheck, T as TrendingUp, c0 as Layers, C as Crown, X as ExternalLink, bv as Settings2, v as useQueryClient, w as useMutation, ax as Plus, aO as Trash2, O as RefreshCw, c1 as Power, c2 as PowerOff, E as Loader2, aE as CheckCircle2, aF as HelpCircle, bk as Send, aW as Save, by as EyeOff, aR as Eye, aH as Copy, ay as X, y as ChevronDown, aP as Search, aB as Check } from "./react-vendor-CithbthY.js"; |
| 2 |
import { a as apiClient, _ as __, u as useToast, s as sprintf, I as Input } from "./index-Dec_weLB.js"; |
| 3 |
import { P as PageHeader, C as Card, d as CardContent, f as CardHeader, g as CardTitle, h as CardDescription, B as Button, e as Badge, k as ConfirmationDialog, S as Select, s as Label } from "../../admin/dist/js/app.js"; |
| 4 |
import { M as ModulePageSkeleton, b as ModuleListSkeleton, c as ModuleTableSkeleton } from "./module-skeleton-CpoAoviJ.js"; |
| 5 |
const channelManagerApi = { |
| 6 |
getMeta: () => apiClient.get("/channel-manager/meta"), |
| 7 |
listChannels: () => apiClient.get("/channel-manager/channels"), |
| 8 |
createChannel: (payload) => apiClient.post("/channel-manager/channels", payload), |
| 9 |
updateChannel: (id, payload) => apiClient.put(`/channel-manager/channels/${id}`, payload), |
| 10 |
deleteChannel: (id) => apiClient.delete(`/channel-manager/channels/${id}`), |
| 11 |
updateCredential: (id, field, value) => apiClient.put(`/channel-manager/channels/${id}/credential`, { |
| 12 |
field, |
| 13 |
value |
| 14 |
}), |
| 15 |
testConnection: (id) => apiClient.post( |
| 16 |
`/channel-manager/channels/${id}/test-connection`, |
| 17 |
{} |
| 18 |
), |
| 19 |
syncChannel: (id) => apiClient.post(`/channel-manager/channels/${id}/sync`, {}), |
| 20 |
channelHealth: (id) => apiClient.get(`/channel-manager/channels/${id}/health`), |
| 21 |
testWebhook: (id) => apiClient.post( |
| 22 |
`/channel-manager/channels/${id}/test-webhook`, |
| 23 |
{} |
| 24 |
), |
| 25 |
bulkMappings: (action, ids) => apiClient.post("/channel-manager/mappings/bulk", { |
| 26 |
action, |
| 27 |
ids |
| 28 |
}), |
| 29 |
listMappings: (channelId) => apiClient.get("/channel-manager/mappings", { |
| 30 |
params: channelId ? { channel_id: channelId } : {} |
| 31 |
}), |
| 32 |
createMapping: (payload) => apiClient.post("/channel-manager/mappings", payload), |
| 33 |
updateMapping: (id, payload) => apiClient.put(`/channel-manager/mappings/${id}`, payload), |
| 34 |
deleteMapping: (id) => apiClient.delete(`/channel-manager/mappings/${id}`), |
| 35 |
syncMapping: (id) => apiClient.post(`/channel-manager/mappings/${id}/sync`, {}), |
| 36 |
listBookings: (filters = {}) => apiClient.get("/channel-manager/bookings", { params: filters }), |
| 37 |
listLogs: (filters = {}) => apiClient.get("/channel-manager/logs", { params: filters }), |
| 38 |
/* --------------------- reconciliation report --------------------- */ |
| 39 |
/** Cross-channel snapshot — flags inventory staleness, pending |
| 40 |
* booking promotions, sync success/fail counts, and breaker states. |
| 41 |
* Suitable both for the React reconciliation tab AND for external |
| 42 |
* monitoring polling. */ |
| 43 |
getReconciliation: () => apiClient.get("/channel-manager/reconciliation"), |
| 44 |
/** Per-channel + per-provider + global latency aggregation. avg / |
| 45 |
* p50 / p95 / p99 of duration_ms across 24h and 7d windows. */ |
| 46 |
getLatency: () => apiClient.get("/channel-manager/latency") |
| 47 |
}; |
| 48 |
function getInitialTab() { |
| 49 |
if (typeof window === "undefined") return "channels"; |
| 50 |
const tab = new URLSearchParams(window.location.search).get("tab"); |
| 51 |
if (tab === "mappings" || tab === "bookings" || tab === "logs" || tab === "reconciliation" || tab === "latency") |
| 52 |
return tab; |
| 53 |
return "channels"; |
| 54 |
} |
| 55 |
const ChannelManager = () => { |
| 56 |
const [activeTab, setActiveTab] = reactExports.useState(() => getInitialTab()); |
| 57 |
const switchTab = (next) => { |
| 58 |
setActiveTab(next); |
| 59 |
if (typeof window !== "undefined") { |
| 60 |
const url = new URL(window.location.href); |
| 61 |
url.searchParams.set("tab", next); |
| 62 |
window.history.replaceState({}, "", url.toString()); |
| 63 |
} |
| 64 |
}; |
| 65 |
const { data: meta, isLoading: metaLoading } = useQuery({ |
| 66 |
queryKey: ["channel-manager-meta"], |
| 67 |
queryFn: () => channelManagerApi.getMeta() |
| 68 |
}); |
| 69 |
if (metaLoading) { |
| 70 |
return /* @__PURE__ */ jsxRuntimeExports.jsx(ModulePageSkeleton, { variant: "tabs" }); |
| 71 |
} |
| 72 |
if (!meta || !meta.is_eligible) { |
| 73 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ |
| 74 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 75 |
PageHeader, |
| 76 |
{ |
| 77 |
description: __( |
| 78 |
"Distribute your tours on global OTAs from one dashboard. Push inventory and pricing automatically, receive bookings back into a unified inbox.", |
| 79 |
"yatra" |
| 80 |
) |
| 81 |
} |
| 82 |
), |
| 83 |
/* @__PURE__ */ jsxRuntimeExports.jsx(AboutCard, {}), |
| 84 |
/* @__PURE__ */ jsxRuntimeExports.jsx(UpgradeCard, { meta }) |
| 85 |
] }); |
| 86 |
} |
| 87 |
const ready = Boolean(meta.is_module_enabled); |
| 88 |
const tabs = [ |
| 89 |
{ key: "channels", label: __("Channels", "yatra"), icon: Network }, |
| 90 |
{ key: "mappings", label: __("Trip mappings", "yatra"), icon: Link }, |
| 91 |
{ key: "bookings", label: __("Bookings inbox", "yatra"), icon: Inbox }, |
| 92 |
{ key: "logs", label: __("Sync activity", "yatra"), icon: Clock }, |
| 93 |
// Reconciliation answers "where do I have unfinished business?" — |
| 94 |
// stale mappings, pending booking promotions, breaker states. |
| 95 |
{ |
| 96 |
key: "reconciliation", |
| 97 |
label: __("Reconciliation", "yatra"), |
| 98 |
icon: AlertTriangle |
| 99 |
}, |
| 100 |
// Latency answers "are my OTAs slow today?" — p50/p95/p99 of |
| 101 |
// sync durations across 24h + 7d windows. |
| 102 |
{ key: "latency", label: __("Latency", "yatra"), icon: Activity } |
| 103 |
]; |
| 104 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ |
| 105 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 106 |
PageHeader, |
| 107 |
{ |
| 108 |
description: __( |
| 109 |
"Distribute your tours on global OTAs from one dashboard. Push inventory and pricing automatically, receive bookings back into a unified inbox.", |
| 110 |
"yatra" |
| 111 |
) |
| 112 |
} |
| 113 |
), |
| 114 |
/* @__PURE__ */ jsxRuntimeExports.jsx(AboutCard, {}), |
| 115 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 116 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx("nav", { className: "flex flex-wrap gap-1 px-4", children: tabs.map((tab) => { |
| 117 |
const Icon = tab.icon; |
| 118 |
const active = activeTab === tab.key; |
| 119 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 120 |
"button", |
| 121 |
{ |
| 122 |
type: "button", |
| 123 |
onClick: () => switchTab(tab.key), |
| 124 |
className: `flex items-center gap-2 px-4 py-3 border-b-2 font-medium text-sm transition-colors ${active ? "border-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"}`, |
| 125 |
children: [ |
| 126 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "w-4 h-4" }), |
| 127 |
tab.label |
| 128 |
] |
| 129 |
}, |
| 130 |
tab.key |
| 131 |
); |
| 132 |
}) }) }), |
| 133 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-6", children: !ready ? /* @__PURE__ */ jsxRuntimeExports.jsx(ModulePrompt, {}) : activeTab === "channels" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChannelsSection, { meta }) : activeTab === "mappings" ? /* @__PURE__ */ jsxRuntimeExports.jsx(MappingsSection, {}) : activeTab === "bookings" ? /* @__PURE__ */ jsxRuntimeExports.jsx(BookingsSection, {}) : activeTab === "reconciliation" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ReconciliationSection, {}) : activeTab === "latency" ? /* @__PURE__ */ jsxRuntimeExports.jsx(LatencySection, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(LogsSection, {}) }) |
| 134 |
] }) |
| 135 |
] }); |
| 136 |
}; |
| 137 |
const AboutCard = () => /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { className: "border-blue-200 bg-blue-50/40 dark:border-blue-800 dark:bg-blue-950/20", children: [ |
| 138 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 139 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [ |
| 140 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-5 w-5 text-blue-600 dark:text-blue-400" }), |
| 141 |
__("What is a Channel Manager?", "yatra") |
| 142 |
] }), |
| 143 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardDescription, { className: "mt-2 leading-relaxed", children: [ |
| 144 |
__( |
| 145 |
"OTAs (Online Travel Agencies) — Viator, GetYourGuide, Klook, TripAdvisor, Airbnb Experiences — drive 30–60% of bookings for most tour operators. Managing them by hand means updating inventory and prices in multiple dashboards, copy-pasting bookings between systems, and risking overbooking when seats sell on two channels at once.", |
| 146 |
"yatra" |
| 147 |
), |
| 148 |
/* @__PURE__ */ jsxRuntimeExports.jsx("br", {}), |
| 149 |
/* @__PURE__ */ jsxRuntimeExports.jsx("br", {}), |
| 150 |
__( |
| 151 |
"This module connects directly to each OTA's API. Inventory + pricing are pushed automatically whenever a booking, departure, or availability change happens here. Bookings made on the OTA flow back into the same booking list as direct sales — same customer notifications, same revenue reporting, same automation rules.", |
| 152 |
"yatra" |
| 153 |
) |
| 154 |
] }) |
| 155 |
] }), |
| 156 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { children: [ |
| 157 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [ |
| 158 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 159 |
HowItWorksStep, |
| 160 |
{ |
| 161 |
icon: Network, |
| 162 |
step: 1, |
| 163 |
title: __("Connect a channel", "yatra"), |
| 164 |
body: __( |
| 165 |
"Add an OTA, paste in your provider credentials, and set commission + price offset. Start in sandbox mode until your first test booking flows through.", |
| 166 |
"yatra" |
| 167 |
) |
| 168 |
} |
| 169 |
), |
| 170 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 171 |
HowItWorksStep, |
| 172 |
{ |
| 173 |
icon: Link, |
| 174 |
step: 2, |
| 175 |
title: __("Map your trips", "yatra"), |
| 176 |
body: __( |
| 177 |
"For each trip you want to sell on that OTA, link it to its counterpart product on their side. One trip can map to many channels with different prices and inventory rules.", |
| 178 |
"yatra" |
| 179 |
) |
| 180 |
} |
| 181 |
), |
| 182 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 183 |
HowItWorksStep, |
| 184 |
{ |
| 185 |
icon: Zap, |
| 186 |
step: 3, |
| 187 |
title: __("Sync runs automatically", "yatra"), |
| 188 |
body: __( |
| 189 |
"Inventory and pricing push within seconds of any change. Bookings arrive via webhook, are staged for safety, then promoted into the standard booking list — instantly visible everywhere else.", |
| 190 |
"yatra" |
| 191 |
) |
| 192 |
} |
| 193 |
) |
| 194 |
] }), |
| 195 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [ |
| 196 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 197 |
ValueCallout, |
| 198 |
{ |
| 199 |
icon: Globe, |
| 200 |
title: __("Reach new customers", "yatra"), |
| 201 |
body: __( |
| 202 |
"OTAs market your trips to millions of travelers worldwide.", |
| 203 |
"yatra" |
| 204 |
) |
| 205 |
} |
| 206 |
), |
| 207 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 208 |
ValueCallout, |
| 209 |
{ |
| 210 |
icon: ShieldCheck, |
| 211 |
title: __("No overbooking", "yatra"), |
| 212 |
body: __( |
| 213 |
"Real-time inventory keeps stock in sync across every channel.", |
| 214 |
"yatra" |
| 215 |
) |
| 216 |
} |
| 217 |
), |
| 218 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 219 |
ValueCallout, |
| 220 |
{ |
| 221 |
icon: TrendingUp, |
| 222 |
title: __("Smart pricing", "yatra"), |
| 223 |
body: __( |
| 224 |
"Per-channel offsets cover commission while keeping margins.", |
| 225 |
"yatra" |
| 226 |
) |
| 227 |
} |
| 228 |
), |
| 229 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 230 |
ValueCallout, |
| 231 |
{ |
| 232 |
icon: Layers, |
| 233 |
title: __("Bring your own credentials", "yatra"), |
| 234 |
body: __( |
| 235 |
"Connections go direct to the OTA. No proxying, no markup.", |
| 236 |
"yatra" |
| 237 |
) |
| 238 |
} |
| 239 |
) |
| 240 |
] }) |
| 241 |
] }) |
| 242 |
] }); |
| 243 |
const HowItWorksStep = ({ icon: Icon, step, title, body }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-lg border border-blue-200/70 bg-white p-4 dark:border-blue-800/60 dark:bg-gray-900", children: [ |
| 244 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ |
| 245 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-full bg-blue-600 text-xs font-semibold text-white", children: step }), |
| 246 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "h-4 w-4 text-blue-600 dark:text-blue-400" }), |
| 247 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: title }) |
| 248 |
] }), |
| 249 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs leading-relaxed text-gray-600 dark:text-gray-300", children: body }) |
| 250 |
] }); |
| 251 |
const ValueCallout = ({ icon: Icon, title, body }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-900", children: [ |
| 252 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ |
| 253 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "h-4 w-4 text-blue-500" }), |
| 254 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h5", { className: "text-xs font-semibold text-gray-900 dark:text-white", children: title }) |
| 255 |
] }), |
| 256 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 text-xs text-gray-600 dark:text-gray-400", children: body }) |
| 257 |
] }); |
| 258 |
const UpgradeCard = ({ meta }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { className: "max-w-3xl", children: [ |
| 259 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [ |
| 260 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md bg-purple-100 p-2 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Crown, { className: "h-5 w-5" }) }), |
| 261 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [ |
| 262 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Channel Manager — Scale plan", "yatra") }), |
| 263 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { className: "mt-1", children: __( |
| 264 |
"Available on the Scale plan. Standalone channel managers charge $99–299/month for the same capability — it's included here at no extra cost.", |
| 265 |
"yatra" |
| 266 |
) }) |
| 267 |
] }) |
| 268 |
] }) }), |
| 269 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 270 |
"a", |
| 271 |
{ |
| 272 |
href: (meta == null ? void 0 : meta.upgrade_url) || "https://wpyatra.com/pricing?module=channel-manager", |
| 273 |
target: "_blank", |
| 274 |
rel: "noopener noreferrer", |
| 275 |
children: [ |
| 276 |
__("Upgrade to Scale", "yatra"), |
| 277 |
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "ml-2 h-4 w-4" }) |
| 278 |
] |
| 279 |
} |
| 280 |
) }) }) |
| 281 |
] }); |
| 282 |
const ModulePrompt = () => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-6 dark:border-amber-800 dark:bg-amber-950/30", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3", children: [ |
| 283 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Settings2, { className: "mt-0.5 h-6 w-6 shrink-0 text-amber-600 dark:text-amber-400" }), |
| 284 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 285 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-base font-semibold text-amber-900 dark:text-amber-100", children: __("Enable the Channel Manager module", "yatra") }), |
| 286 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 text-sm text-amber-800/90 dark:text-amber-200/90", children: __( |
| 287 |
"Your license tier qualifies. Turn on Channel Manager under Modules to start configuring it.", |
| 288 |
"yatra" |
| 289 |
) }), |
| 290 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 291 |
Button, |
| 292 |
{ |
| 293 |
asChild: true, |
| 294 |
variant: "outline", |
| 295 |
className: "mt-3 border-amber-300 text-amber-800 hover:bg-amber-100 dark:border-amber-700 dark:text-amber-200 dark:hover:bg-amber-900/40", |
| 296 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "admin.php?page=yatra&subpage=modules", children: __("Open Modules", "yatra") }) |
| 297 |
} |
| 298 |
) |
| 299 |
] }) |
| 300 |
] }) }); |
| 301 |
const ChannelsSection = ({ meta }) => { |
| 302 |
const queryClient = useQueryClient(); |
| 303 |
const { showToast } = useToast(); |
| 304 |
const [editing, setEditing] = reactExports.useState(null); |
| 305 |
const [pendingDeleteChannel, setPendingDeleteChannel] = reactExports.useState(null); |
| 306 |
const { data, isLoading } = useQuery({ |
| 307 |
queryKey: ["channel-manager-channels"], |
| 308 |
queryFn: () => channelManagerApi.listChannels() |
| 309 |
}); |
| 310 |
const deleteChannel = useMutation({ |
| 311 |
mutationFn: (id) => channelManagerApi.deleteChannel(id), |
| 312 |
onSuccess: () => { |
| 313 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-channels"] }); |
| 314 |
showToast(__("Channel deleted.", "yatra"), "success"); |
| 315 |
setPendingDeleteChannel(null); |
| 316 |
}, |
| 317 |
onError: (e) => { |
| 318 |
showToast(extractError(e), "error"); |
| 319 |
setPendingDeleteChannel(null); |
| 320 |
} |
| 321 |
}); |
| 322 |
if (isLoading) { |
| 323 |
return /* @__PURE__ */ jsxRuntimeExports.jsx(ModuleListSkeleton, { rows: 4 }); |
| 324 |
} |
| 325 |
if (editing !== null) { |
| 326 |
const channel = editing === "new" ? null : (data == null ? void 0 : data.data.find((c) => c.id === editing)) ?? null; |
| 327 |
return /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 328 |
ChannelEditForm, |
| 329 |
{ |
| 330 |
meta, |
| 331 |
existing: channel, |
| 332 |
onClose: () => setEditing(null) |
| 333 |
} |
| 334 |
); |
| 335 |
} |
| 336 |
const channels = (data == null ? void 0 : data.data) ?? []; |
| 337 |
const supportedChannelNames = meta.channel_types.map((t) => t.name).join(", "); |
| 338 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ |
| 339 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [ |
| 340 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-2xl", children: [ |
| 341 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Configured channels", "yatra") }), |
| 342 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __( |
| 343 |
"Add one connection per OTA you want to distribute on. Credentials are stored libsodium-encrypted; nothing is proxied through any third-party service.", |
| 344 |
"yatra" |
| 345 |
) }), |
| 346 |
meta.channel_types.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mt-2 text-xs text-gray-500 dark:text-gray-400", children: [ |
| 347 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-gray-700 dark:text-gray-300", children: __("Supported OTAs:", "yatra") }), |
| 348 |
" ", |
| 349 |
supportedChannelNames |
| 350 |
] }) |
| 351 |
] }), |
| 352 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => setEditing("new"), children: [ |
| 353 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "mr-1.5 h-4 w-4" }), |
| 354 |
__("Add channel", "yatra") |
| 355 |
] }) |
| 356 |
] }), |
| 357 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: channels.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(EmptyChannels, { onAdd: () => setEditing("new"), meta }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full", children: [ |
| 358 |
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 359 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Channel", "yatra") }), |
| 360 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Account", "yatra") }), |
| 361 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Health", "yatra") }), |
| 362 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Mode", "yatra") }), |
| 363 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Status", "yatra") }), |
| 364 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Last sync", "yatra") }), |
| 365 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { align: "right", children: __("Actions", "yatra") }) |
| 366 |
] }) }), |
| 367 |
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: channels.map((ch) => { |
| 368 |
const type = meta.channel_types.find( |
| 369 |
(t) => t.type === ch.channel_type |
| 370 |
); |
| 371 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 372 |
"tr", |
| 373 |
{ |
| 374 |
className: "hover:bg-gray-50 dark:hover:bg-gray-800/50", |
| 375 |
children: [ |
| 376 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-4 py-3", children: [ |
| 377 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white", children: [ |
| 378 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Network, { className: "h-4 w-4 text-purple-500" }), |
| 379 |
(type == null ? void 0 : type.name) ?? ch.channel_type |
| 380 |
] }), |
| 381 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: ch.display_name || "—" }) |
| 382 |
] }), |
| 383 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm text-gray-700 dark:text-gray-200", children: ch.account_label || "—" }), |
| 384 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(HealthBadge, { health: ch.health }) }), |
| 385 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 386 |
Badge, |
| 387 |
{ |
| 388 |
className: ch.is_test_mode ? "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", |
| 389 |
children: ch.is_test_mode ? __("Sandbox", "yatra") : __("Live", "yatra") |
| 390 |
} |
| 391 |
) }), |
| 392 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 393 |
Badge, |
| 394 |
{ |
| 395 |
className: ch.is_enabled ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300", |
| 396 |
children: ch.is_enabled ? __("Enabled", "yatra") : __("Disabled", "yatra") |
| 397 |
} |
| 398 |
) }), |
| 399 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-4 py-3 text-xs text-gray-500 dark:text-gray-400", children: [ |
| 400 |
ch.last_sync_at ? new Date(ch.last_sync_at).toLocaleString() : "—", |
| 401 |
ch.last_sync_status === "failed" && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 inline-flex items-center gap-1 text-red-600", children: [ |
| 402 |
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-3 w-3" }), |
| 403 |
__("failed", "yatra") |
| 404 |
] }) |
| 405 |
] }), |
| 406 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex gap-1", children: [ |
| 407 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 408 |
Button, |
| 409 |
{ |
| 410 |
variant: "outline", |
| 411 |
size: "sm", |
| 412 |
onClick: () => setEditing(ch.id), |
| 413 |
children: __("Edit", "yatra") |
| 414 |
} |
| 415 |
), |
| 416 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 417 |
Button, |
| 418 |
{ |
| 419 |
variant: "outline", |
| 420 |
size: "sm", |
| 421 |
className: "border-red-200 text-red-700 hover:bg-red-50 dark:border-red-700 dark:text-red-300 dark:hover:bg-red-900/30", |
| 422 |
onClick: () => setPendingDeleteChannel({ |
| 423 |
id: ch.id, |
| 424 |
name: ch.display_name || ch.channel_type |
| 425 |
}), |
| 426 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "h-3.5 w-3.5" }) |
| 427 |
} |
| 428 |
) |
| 429 |
] }) }) |
| 430 |
] |
| 431 |
}, |
| 432 |
ch.id |
| 433 |
); |
| 434 |
}) }) |
| 435 |
] }) }) }) }), |
| 436 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 437 |
ConfirmationDialog, |
| 438 |
{ |
| 439 |
isOpen: pendingDeleteChannel !== null, |
| 440 |
onClose: () => { |
| 441 |
if (!deleteChannel.isPending) setPendingDeleteChannel(null); |
| 442 |
}, |
| 443 |
onConfirm: () => { |
| 444 |
if (pendingDeleteChannel) |
| 445 |
deleteChannel.mutate(pendingDeleteChannel.id); |
| 446 |
}, |
| 447 |
title: __("Delete channel?", "yatra"), |
| 448 |
description: pendingDeleteChannel ? __( |
| 449 |
"Delete the “{name}” channel? Credentials and trip mappings linked to it will be removed. This cannot be undone.", |
| 450 |
"yatra" |
| 451 |
).replace("{name}", pendingDeleteChannel.name) : "", |
| 452 |
confirmText: __("Delete channel", "yatra"), |
| 453 |
cancelText: __("Cancel", "yatra"), |
| 454 |
variant: "danger", |
| 455 |
isLoading: deleteChannel.isPending |
| 456 |
} |
| 457 |
) |
| 458 |
] }); |
| 459 |
}; |
| 460 |
const EmptyChannels = ({ onAdd, meta }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-10 text-center", children: [ |
| 461 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Network, { className: "h-7 w-7 text-blue-600 dark:text-blue-400" }) }), |
| 462 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-base font-semibold text-gray-900 dark:text-white", children: __("No channels yet", "yatra") }), |
| 463 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mx-auto mt-1 max-w-md text-sm text-gray-500 dark:text-gray-400", children: __( |
| 464 |
"Connect your first OTA to start distributing trips. You'll need API credentials from that OTA's partner portal — there's a help link inside the form.", |
| 465 |
"yatra" |
| 466 |
) }), |
| 467 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 flex justify-center gap-2", children: [ |
| 468 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: onAdd, children: [ |
| 469 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "mr-1.5 h-4 w-4" }), |
| 470 |
__("Add your first channel", "yatra") |
| 471 |
] }), |
| 472 |
meta.docs_url && /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: meta.docs_url, target: "_blank", rel: "noopener noreferrer", children: [ |
| 473 |
/* @__PURE__ */ jsxRuntimeExports.jsx(HelpCircle, { className: "mr-1.5 h-4 w-4" }), |
| 474 |
__("Read the setup guide", "yatra") |
| 475 |
] }) }) |
| 476 |
] }) |
| 477 |
] }); |
| 478 |
const ChannelEditForm = ({ meta, existing, onClose }) => { |
| 479 |
var _a, _b; |
| 480 |
const queryClient = useQueryClient(); |
| 481 |
const { showToast } = useToast(); |
| 482 |
const isCreate = existing === null; |
| 483 |
const [channelType, setChannelType] = reactExports.useState( |
| 484 |
(existing == null ? void 0 : existing.channel_type) ?? ((_a = meta.channel_types[0]) == null ? void 0 : _a.type) ?? "" |
| 485 |
); |
| 486 |
const [displayName, setDisplayName] = reactExports.useState((existing == null ? void 0 : existing.display_name) ?? ""); |
| 487 |
const [accountLabel, setAccountLabel] = reactExports.useState( |
| 488 |
(existing == null ? void 0 : existing.account_label) ?? "" |
| 489 |
); |
| 490 |
const [isEnabled, setIsEnabled] = reactExports.useState((existing == null ? void 0 : existing.is_enabled) ?? false); |
| 491 |
const [isTestMode, setIsTestMode] = reactExports.useState((existing == null ? void 0 : existing.is_test_mode) ?? true); |
| 492 |
const [currency, setCurrency] = reactExports.useState((existing == null ? void 0 : existing.currency) ?? "USD"); |
| 493 |
const [defaultOffset, setDefaultOffset] = reactExports.useState( |
| 494 |
(existing == null ? void 0 : existing.default_offset_percent) ?? 0 |
| 495 |
); |
| 496 |
const [commission, setCommission] = reactExports.useState( |
| 497 |
(existing == null ? void 0 : existing.commission_percent) ?? 0 |
| 498 |
); |
| 499 |
const [buffer, setBuffer] = reactExports.useState((existing == null ? void 0 : existing.inventory_buffer) ?? 0); |
| 500 |
const [allowedIps, setAllowedIps] = reactExports.useState( |
| 501 |
((_b = existing == null ? void 0 : existing.settings) == null ? void 0 : _b.allowed_ips) ?? "" |
| 502 |
); |
| 503 |
const typeDef = reactExports.useMemo( |
| 504 |
() => meta.channel_types.find((t) => t.type === channelType) ?? null, |
| 505 |
[meta.channel_types, channelType] |
| 506 |
); |
| 507 |
const save = useMutation({ |
| 508 |
mutationFn: () => { |
| 509 |
const payload = { |
| 510 |
channel_type: channelType, |
| 511 |
display_name: displayName, |
| 512 |
account_label: accountLabel, |
| 513 |
is_enabled: isEnabled, |
| 514 |
is_test_mode: isTestMode, |
| 515 |
currency, |
| 516 |
default_offset_percent: defaultOffset, |
| 517 |
commission_percent: commission, |
| 518 |
inventory_buffer: buffer, |
| 519 |
// Merge into existing settings so unrelated keys (per-provider |
| 520 |
// overrides, custom fields a future feature might add) are |
| 521 |
// preserved on update. |
| 522 |
settings: { |
| 523 |
...(existing == null ? void 0 : existing.settings) ?? {}, |
| 524 |
allowed_ips: allowedIps |
| 525 |
} |
| 526 |
}; |
| 527 |
return isCreate ? channelManagerApi.createChannel(payload) : channelManagerApi.updateChannel(existing.id, payload); |
| 528 |
}, |
| 529 |
onSuccess: () => { |
| 530 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-channels"] }); |
| 531 |
showToast( |
| 532 |
isCreate ? __("Channel created. Add credentials next to enable sync.", "yatra") : __("Channel saved.", "yatra"), |
| 533 |
"success" |
| 534 |
); |
| 535 |
if (isCreate) onClose(); |
| 536 |
}, |
| 537 |
onError: (e) => showToast(extractError(e), "error") |
| 538 |
}); |
| 539 |
const testConnection = useMutation({ |
| 540 |
mutationFn: () => channelManagerApi.testConnection(existing.id), |
| 541 |
onSuccess: (res) => { |
| 542 |
if (res.ok) { |
| 543 |
showToast( |
| 544 |
__("Connection OK — credentials accepted by the OTA.", "yatra"), |
| 545 |
"success" |
| 546 |
); |
| 547 |
} else { |
| 548 |
showToast(res.error || __("Connection failed.", "yatra"), "error"); |
| 549 |
} |
| 550 |
}, |
| 551 |
onError: (e) => showToast(extractError(e), "error") |
| 552 |
}); |
| 553 |
const syncChannel = useMutation({ |
| 554 |
mutationFn: () => channelManagerApi.syncChannel(existing.id), |
| 555 |
onSuccess: (res) => showToast(res.message, "success"), |
| 556 |
onError: (e) => showToast(extractError(e), "error") |
| 557 |
}); |
| 558 |
const testWebhook = useMutation({ |
| 559 |
mutationFn: () => channelManagerApi.testWebhook(existing.id), |
| 560 |
onSuccess: (res) => { |
| 561 |
if (res.ok) { |
| 562 |
showToast( |
| 563 |
__( |
| 564 |
"Webhook self-test succeeded — your endpoint is reachable.", |
| 565 |
"yatra" |
| 566 |
), |
| 567 |
"success" |
| 568 |
); |
| 569 |
} else { |
| 570 |
showToast( |
| 571 |
res.error || __( |
| 572 |
"Webhook self-test failed — see Sync activity for details.", |
| 573 |
"yatra" |
| 574 |
), |
| 575 |
"error" |
| 576 |
); |
| 577 |
} |
| 578 |
}, |
| 579 |
onError: (e) => showToast(extractError(e), "error") |
| 580 |
}); |
| 581 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ |
| 582 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [ |
| 583 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 584 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-white", children: isCreate ? __("Add channel", "yatra") : __("Edit channel", "yatra") }), |
| 585 |
(typeDef == null ? void 0 : typeDef.description) && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-2xl", children: typeDef.description }), |
| 586 |
typeDef && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [ |
| 587 |
typeDef.docs_url && /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 588 |
"a", |
| 589 |
{ |
| 590 |
href: typeDef.docs_url, |
| 591 |
target: "_blank", |
| 592 |
rel: "noopener noreferrer", |
| 593 |
className: "inline-flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400", |
| 594 |
children: [ |
| 595 |
/* @__PURE__ */ jsxRuntimeExports.jsx(HelpCircle, { className: "h-3 w-3" }), |
| 596 |
__("Integration docs", "yatra") |
| 597 |
] |
| 598 |
} |
| 599 |
), |
| 600 |
typeDef.signup_url && /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 601 |
"a", |
| 602 |
{ |
| 603 |
href: typeDef.signup_url, |
| 604 |
target: "_blank", |
| 605 |
rel: "noopener noreferrer", |
| 606 |
className: "inline-flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400", |
| 607 |
children: [ |
| 608 |
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "h-3 w-3" }), |
| 609 |
__("Apply to partner program", "yatra") |
| 610 |
] |
| 611 |
} |
| 612 |
) |
| 613 |
] }) |
| 614 |
] }), |
| 615 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 flex-wrap", children: [ |
| 616 |
!isCreate && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ |
| 617 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 618 |
Button, |
| 619 |
{ |
| 620 |
variant: "outline", |
| 621 |
onClick: () => testConnection.mutate(), |
| 622 |
disabled: testConnection.isPending, |
| 623 |
children: [ |
| 624 |
testConnection.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "mr-1.5 h-4 w-4" }), |
| 625 |
__("Test connection", "yatra") |
| 626 |
] |
| 627 |
} |
| 628 |
), |
| 629 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 630 |
Button, |
| 631 |
{ |
| 632 |
variant: "outline", |
| 633 |
onClick: () => syncChannel.mutate(), |
| 634 |
disabled: syncChannel.isPending || !isEnabled, |
| 635 |
title: !isEnabled ? __("Enable the channel before syncing.", "yatra") : void 0, |
| 636 |
children: [ |
| 637 |
syncChannel.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "mr-1.5 h-4 w-4" }), |
| 638 |
__("Sync now", "yatra") |
| 639 |
] |
| 640 |
} |
| 641 |
), |
| 642 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 643 |
Button, |
| 644 |
{ |
| 645 |
variant: "outline", |
| 646 |
onClick: () => testWebhook.mutate(), |
| 647 |
disabled: testWebhook.isPending, |
| 648 |
title: __( |
| 649 |
"Sends a signed synthetic booking to your own webhook URL so you can confirm the round-trip works.", |
| 650 |
"yatra" |
| 651 |
), |
| 652 |
children: [ |
| 653 |
testWebhook.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Send, { className: "mr-1.5 h-4 w-4" }), |
| 654 |
__("Test webhook", "yatra") |
| 655 |
] |
| 656 |
} |
| 657 |
) |
| 658 |
] }), |
| 659 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Back to list", "yatra") }), |
| 660 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 661 |
Button, |
| 662 |
{ |
| 663 |
onClick: () => save.mutate(), |
| 664 |
disabled: save.isPending || channelType === "", |
| 665 |
children: [ |
| 666 |
save.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-1.5 h-4 w-4" }), |
| 667 |
isCreate ? __("Create channel", "yatra") : __("Save changes", "yatra") |
| 668 |
] |
| 669 |
} |
| 670 |
) |
| 671 |
] }) |
| 672 |
] }), |
| 673 |
isCreate && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md border border-blue-200 bg-blue-50/60 px-4 py-3 text-sm text-blue-900 dark:border-blue-800 dark:bg-blue-950/30 dark:text-blue-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "flex items-start gap-2", children: [ |
| 674 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-4 w-4 mt-0.5 flex-shrink-0" }), |
| 675 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: __( |
| 676 |
"Channels are created in disabled + sandbox mode by default. You'll add credentials and test the connection on the next screen, then flip Enabled on once you've verified everything works.", |
| 677 |
"yatra" |
| 678 |
) }) |
| 679 |
] }) }), |
| 680 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 681 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 682 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Connection", "yatra") }), |
| 683 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 684 |
"Pick which OTA this connection is for and give it a friendly label.", |
| 685 |
"yatra" |
| 686 |
) }) |
| 687 |
] }), |
| 688 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "space-y-5", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-5 md:grid-cols-2", children: [ |
| 689 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 690 |
FormField, |
| 691 |
{ |
| 692 |
label: __("OTA channel", "yatra"), |
| 693 |
description: __( |
| 694 |
"Pick the OTA you're connecting. Once a channel is created, this is locked — to switch OTAs you'd add a new channel.", |
| 695 |
"yatra" |
| 696 |
), |
| 697 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 698 |
Select, |
| 699 |
{ |
| 700 |
value: channelType, |
| 701 |
onChange: (e) => setChannelType(e.target.value), |
| 702 |
disabled: !isCreate, |
| 703 |
children: meta.channel_types.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: t.type, children: t.name }, t.type)) |
| 704 |
} |
| 705 |
) |
| 706 |
} |
| 707 |
), |
| 708 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 709 |
FormField, |
| 710 |
{ |
| 711 |
label: __("Display name", "yatra"), |
| 712 |
description: __( |
| 713 |
"How this channel appears in the admin UI. Leave blank to use the OTA's name.", |
| 714 |
"yatra" |
| 715 |
), |
| 716 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 717 |
Input, |
| 718 |
{ |
| 719 |
value: displayName, |
| 720 |
onChange: (e) => setDisplayName(e.target.value), |
| 721 |
placeholder: (typeDef == null ? void 0 : typeDef.name) ?? "" |
| 722 |
} |
| 723 |
) |
| 724 |
} |
| 725 |
), |
| 726 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 727 |
FormField, |
| 728 |
{ |
| 729 |
label: __("Account label (internal)", "yatra"), |
| 730 |
description: __( |
| 731 |
"Optional note shown in logs and reports. Useful when you run multiple accounts on the same OTA (e.g. 'US production', 'EU staging').", |
| 732 |
"yatra" |
| 733 |
), |
| 734 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 735 |
Input, |
| 736 |
{ |
| 737 |
value: accountLabel, |
| 738 |
onChange: (e) => setAccountLabel(e.target.value), |
| 739 |
placeholder: __("e.g. US production account", "yatra") |
| 740 |
} |
| 741 |
) |
| 742 |
} |
| 743 |
), |
| 744 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 745 |
FormField, |
| 746 |
{ |
| 747 |
label: __("Currency", "yatra"), |
| 748 |
description: __( |
| 749 |
"ISO 4217 code (e.g. USD, EUR, GBP). Must match the currency your products are priced in on the OTA — mismatches cause failed pricing pushes.", |
| 750 |
"yatra" |
| 751 |
), |
| 752 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 753 |
Input, |
| 754 |
{ |
| 755 |
value: currency, |
| 756 |
onChange: (e) => setCurrency(e.target.value.toUpperCase().slice(0, 3)), |
| 757 |
placeholder: "USD", |
| 758 |
maxLength: 3 |
| 759 |
} |
| 760 |
) |
| 761 |
} |
| 762 |
) |
| 763 |
] }) }) |
| 764 |
] }), |
| 765 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 766 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 767 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Pricing & inventory rules", "yatra") }), |
| 768 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 769 |
"How prices and seat counts on this channel differ from your base trip. These apply across every trip mapped to this channel — individual mappings can override on a per-trip basis.", |
| 770 |
"yatra" |
| 771 |
) }) |
| 772 |
] }), |
| 773 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-5", children: [ |
| 774 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-5 md:grid-cols-3", children: [ |
| 775 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 776 |
FormField, |
| 777 |
{ |
| 778 |
label: __("Default price offset (%)", "yatra"), |
| 779 |
description: __( |
| 780 |
"Adds (or subtracts) a percentage to your base trip price before pushing to this channel. Use a positive number to cover the OTA's commission so your net stays whole; negative to run a channel discount.", |
| 781 |
"yatra" |
| 782 |
), |
| 783 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 784 |
Input, |
| 785 |
{ |
| 786 |
type: "number", |
| 787 |
step: "0.01", |
| 788 |
value: defaultOffset, |
| 789 |
onChange: (e) => setDefaultOffset(Number(e.target.value) || 0), |
| 790 |
placeholder: "0" |
| 791 |
} |
| 792 |
) |
| 793 |
} |
| 794 |
), |
| 795 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 796 |
FormField, |
| 797 |
{ |
| 798 |
label: __("Commission (%) — informational", "yatra"), |
| 799 |
description: __( |
| 800 |
"What the OTA deducts from each sale. Not sent to the channel — used here only to estimate your net revenue in reports.", |
| 801 |
"yatra" |
| 802 |
), |
| 803 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 804 |
Input, |
| 805 |
{ |
| 806 |
type: "number", |
| 807 |
step: "0.01", |
| 808 |
value: commission, |
| 809 |
onChange: (e) => setCommission(Number(e.target.value) || 0), |
| 810 |
placeholder: "0" |
| 811 |
} |
| 812 |
) |
| 813 |
} |
| 814 |
), |
| 815 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 816 |
FormField, |
| 817 |
{ |
| 818 |
label: __("Inventory buffer (seats)", "yatra"), |
| 819 |
description: __( |
| 820 |
"Seats held back from this channel so direct-bookers and other OTAs always have stock. Example: a 20-seat departure with a buffer of 2 will only ever show 18 seats on this channel.", |
| 821 |
"yatra" |
| 822 |
), |
| 823 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 824 |
Input, |
| 825 |
{ |
| 826 |
type: "number", |
| 827 |
min: 0, |
| 828 |
value: buffer, |
| 829 |
onChange: (e) => setBuffer(Number(e.target.value) || 0), |
| 830 |
placeholder: "0" |
| 831 |
} |
| 832 |
) |
| 833 |
} |
| 834 |
) |
| 835 |
] }), |
| 836 |
!isCreate && /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 837 |
FormField, |
| 838 |
{ |
| 839 |
label: __("Inbound webhook IP allowlist", "yatra"), |
| 840 |
description: __( |
| 841 |
"Optional defense-in-depth on top of the signature secret. Comma- or newline-separated CIDRs. Empty = no restriction (default). When set, inbound webhook deliveries from any source outside the list are dropped BEFORE signature verification.", |
| 842 |
"yatra" |
| 843 |
), |
| 844 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 845 |
"textarea", |
| 846 |
{ |
| 847 |
className: "w-full font-mono text-xs px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 min-h-[80px]", |
| 848 |
value: allowedIps, |
| 849 |
onChange: (e) => setAllowedIps(e.target.value), |
| 850 |
placeholder: "203.0.113.0/24\n198.51.100.42" |
| 851 |
} |
| 852 |
) |
| 853 |
} |
| 854 |
) |
| 855 |
] }) |
| 856 |
] }), |
| 857 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 858 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 859 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Channel state", "yatra") }), |
| 860 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 861 |
"Both flags must be considered together: a disabled channel won't sync at all, regardless of mode; sandbox mode lets you test live API calls against the OTA's staging environment without affecting real bookings.", |
| 862 |
"yatra" |
| 863 |
) }) |
| 864 |
] }), |
| 865 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [ |
| 866 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 867 |
ToggleRow, |
| 868 |
{ |
| 869 |
label: __("Enabled", "yatra"), |
| 870 |
description: __( |
| 871 |
"When off, this channel won't push inventory or pricing, and incoming bookings won't be ingested. Logs still record any attempts so you can audit them.", |
| 872 |
"yatra" |
| 873 |
), |
| 874 |
checked: isEnabled, |
| 875 |
onChange: setIsEnabled |
| 876 |
} |
| 877 |
), |
| 878 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 879 |
ToggleRow, |
| 880 |
{ |
| 881 |
label: __("Sandbox / test mode", "yatra"), |
| 882 |
description: __( |
| 883 |
"Routes API calls to the OTA's sandbox environment. Always start in sandbox until a test booking has flowed through end-to-end. Switch off only after you've verified the connection in production.", |
| 884 |
"yatra" |
| 885 |
), |
| 886 |
checked: isTestMode, |
| 887 |
onChange: setIsTestMode, |
| 888 |
tone: "amber" |
| 889 |
} |
| 890 |
) |
| 891 |
] }) |
| 892 |
] }), |
| 893 |
!isCreate && typeDef && /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 894 |
CredentialsCard, |
| 895 |
{ |
| 896 |
channelId: existing.id, |
| 897 |
typeDef, |
| 898 |
status: existing.credentials ?? {} |
| 899 |
} |
| 900 |
), |
| 901 |
!isCreate && /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 902 |
WebhookCard, |
| 903 |
{ |
| 904 |
channelId: existing.id, |
| 905 |
webhookUrlTemplate: meta.webhook_url |
| 906 |
} |
| 907 |
) |
| 908 |
] }); |
| 909 |
}; |
| 910 |
const CredentialsCard = ({ channelId, typeDef, status }) => { |
| 911 |
const queryClient = useQueryClient(); |
| 912 |
const { showToast } = useToast(); |
| 913 |
const [drafts, setDrafts] = reactExports.useState({}); |
| 914 |
const [visible, setVisible] = reactExports.useState({}); |
| 915 |
const save = useMutation({ |
| 916 |
mutationFn: (vars) => channelManagerApi.updateCredential(channelId, vars.field, vars.value), |
| 917 |
onSuccess: () => { |
| 918 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-channels"] }); |
| 919 |
showToast(__("Credential saved.", "yatra"), "success"); |
| 920 |
}, |
| 921 |
onError: (e) => showToast(extractError(e), "error") |
| 922 |
}); |
| 923 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 924 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 925 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [ |
| 926 |
/* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "h-5 w-5 text-blue-500" }), |
| 927 |
__("API credentials", "yatra") |
| 928 |
] }), |
| 929 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 930 |
"Stored encrypted with libsodium AEAD (XChaCha20-Poly1305). After saving, the value is never sent back to the browser — you only see a masked last-4 hint to confirm something is set.", |
| 931 |
"yatra" |
| 932 |
) }) |
| 933 |
] }), |
| 934 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "space-y-6", children: Object.entries(typeDef.credentials).map(([field, def]) => { |
| 935 |
const fieldStatus = status[field] ?? { |
| 936 |
configured: false, |
| 937 |
hint: "" |
| 938 |
}; |
| 939 |
const draft = drafts[field] ?? ""; |
| 940 |
const isVisible = !!visible[field]; |
| 941 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ |
| 942 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [ |
| 943 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Label, { className: "flex items-center gap-2", children: [ |
| 944 |
def.label, |
| 945 |
def.required && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-red-600 dark:text-red-400", children: "*" }) |
| 946 |
] }), |
| 947 |
def.docs_url && /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 948 |
"a", |
| 949 |
{ |
| 950 |
href: def.docs_url, |
| 951 |
target: "_blank", |
| 952 |
rel: "noopener noreferrer", |
| 953 |
className: "inline-flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400", |
| 954 |
children: [ |
| 955 |
__("Where to find this", "yatra"), |
| 956 |
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "h-3 w-3" }) |
| 957 |
] |
| 958 |
} |
| 959 |
) |
| 960 |
] }), |
| 961 |
def.description && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 flex items-start gap-1.5", children: [ |
| 962 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0 text-gray-400" }), |
| 963 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: def.description }) |
| 964 |
] }), |
| 965 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 flex-wrap", children: [ |
| 966 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1 min-w-[260px]", children: [ |
| 967 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 968 |
Input, |
| 969 |
{ |
| 970 |
type: isVisible ? "text" : "password", |
| 971 |
value: draft, |
| 972 |
placeholder: fieldStatus.configured ? fieldStatus.hint : __("Paste the credential value…", "yatra"), |
| 973 |
onChange: (e) => setDrafts((d) => ({ ...d, [field]: e.target.value })) |
| 974 |
} |
| 975 |
), |
| 976 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 977 |
"button", |
| 978 |
{ |
| 979 |
type: "button", |
| 980 |
onClick: () => setVisible((v) => ({ ...v, [field]: !v[field] })), |
| 981 |
className: "absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600", |
| 982 |
"aria-label": isVisible ? __("Hide value", "yatra") : __("Show value", "yatra"), |
| 983 |
children: isVisible ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-4 w-4" }) |
| 984 |
} |
| 985 |
) |
| 986 |
] }), |
| 987 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 988 |
Button, |
| 989 |
{ |
| 990 |
onClick: () => { |
| 991 |
save.mutate({ field, value: draft }); |
| 992 |
setDrafts((d) => ({ ...d, [field]: "" })); |
| 993 |
}, |
| 994 |
disabled: save.isPending || draft.trim() === "", |
| 995 |
children: [ |
| 996 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-1.5 h-4 w-4" }), |
| 997 |
__("Save", "yatra") |
| 998 |
] |
| 999 |
} |
| 1000 |
), |
| 1001 |
fieldStatus.configured && /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1002 |
Button, |
| 1003 |
{ |
| 1004 |
variant: "outline", |
| 1005 |
onClick: () => save.mutate({ field, value: "" }), |
| 1006 |
disabled: save.isPending, |
| 1007 |
className: "border-red-200 text-red-700 hover:bg-red-50 dark:border-red-700 dark:text-red-300 dark:hover:bg-red-900/30", |
| 1008 |
children: __("Clear", "yatra") |
| 1009 |
} |
| 1010 |
) |
| 1011 |
] }), |
| 1012 |
fieldStatus.configured ? /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-green-600 dark:text-green-400 flex items-center gap-1", children: [ |
| 1013 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-3 w-3" }), |
| 1014 |
__("Configured", "yatra"), |
| 1015 |
" — ", |
| 1016 |
fieldStatus.hint |
| 1017 |
] }) : def.required ? /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1", children: [ |
| 1018 |
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-3 w-3" }), |
| 1019 |
__("Required — sync will fail without this.", "yatra") |
| 1020 |
] }) : null |
| 1021 |
] }, field); |
| 1022 |
}) }) |
| 1023 |
] }); |
| 1024 |
}; |
| 1025 |
const WebhookCard = ({ channelId, webhookUrlTemplate }) => { |
| 1026 |
const { showToast } = useToast(); |
| 1027 |
const url = webhookUrlTemplate.includes("{channel_id}") ? webhookUrlTemplate.replace("{channel_id}", String(channelId)) : `${webhookUrlTemplate.replace(/\/$/, "")}/${channelId}`; |
| 1028 |
const copy = async () => { |
| 1029 |
try { |
| 1030 |
await navigator.clipboard.writeText(url); |
| 1031 |
showToast(__("Webhook URL copied.", "yatra"), "success"); |
| 1032 |
} catch { |
| 1033 |
showToast(__("Couldn't copy — copy manually.", "yatra"), "error"); |
| 1034 |
} |
| 1035 |
}; |
| 1036 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 1037 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 1038 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [ |
| 1039 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Inbox, { className: "h-5 w-5 text-blue-500" }), |
| 1040 |
__("Inbound webhook URL", "yatra") |
| 1041 |
] }), |
| 1042 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 1043 |
"Paste this URL into the OTA's partner portal under booking notifications. Whenever a booking is made on the OTA, they POST it here and it appears in the Bookings inbox seconds later. The endpoint verifies the request signature, so only the OTA can deliver to it.", |
| 1044 |
"yatra" |
| 1045 |
) }) |
| 1046 |
] }), |
| 1047 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { children: [ |
| 1048 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 flex-wrap", children: [ |
| 1049 |
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "flex-1 min-w-[280px] rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-xs font-mono text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 break-all", children: url }), |
| 1050 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { variant: "outline", onClick: copy, children: [ |
| 1051 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "mr-1.5 h-4 w-4" }), |
| 1052 |
__("Copy", "yatra") |
| 1053 |
] }) |
| 1054 |
] }), |
| 1055 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mt-3 text-xs text-gray-500 dark:text-gray-400 flex items-start gap-1.5", children: [ |
| 1056 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0" }), |
| 1057 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: __( |
| 1058 |
"If your site is behind a firewall or VPN, make sure this URL is reachable from the public internet — otherwise the OTA's webhook deliveries will time out and you'll only get bookings on the next hourly cron tick.", |
| 1059 |
"yatra" |
| 1060 |
) }) |
| 1061 |
] }) |
| 1062 |
] }) |
| 1063 |
] }); |
| 1064 |
}; |
| 1065 |
const MappingsSection = () => { |
| 1066 |
var _a; |
| 1067 |
const queryClient = useQueryClient(); |
| 1068 |
const { showToast } = useToast(); |
| 1069 |
const [filterChannel, setFilterChannel] = reactExports.useState(0); |
| 1070 |
const [showAdd, setShowAdd] = reactExports.useState(false); |
| 1071 |
const [editing, setEditing] = reactExports.useState(null); |
| 1072 |
const [selected, setSelected] = reactExports.useState(/* @__PURE__ */ new Set()); |
| 1073 |
const [pendingDelete, setPendingDelete] = reactExports.useState(null); |
| 1074 |
const { data: channelsData } = useQuery({ |
| 1075 |
queryKey: ["channel-manager-channels"], |
| 1076 |
queryFn: () => channelManagerApi.listChannels() |
| 1077 |
}); |
| 1078 |
const { data: mappingsData, isLoading } = useQuery({ |
| 1079 |
queryKey: ["channel-manager-mappings", filterChannel], |
| 1080 |
queryFn: () => channelManagerApi.listMappings(filterChannel || void 0) |
| 1081 |
}); |
| 1082 |
const syncMapping = useMutation({ |
| 1083 |
mutationFn: (id) => channelManagerApi.syncMapping(id), |
| 1084 |
onSuccess: (res) => { |
| 1085 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-mappings"] }); |
| 1086 |
if (res.ok) { |
| 1087 |
showToast(__("Mapping synced.", "yatra"), "success"); |
| 1088 |
} else { |
| 1089 |
showToast(res.error || __("Sync failed.", "yatra"), "error"); |
| 1090 |
} |
| 1091 |
}, |
| 1092 |
onError: (e) => showToast(extractError(e), "error") |
| 1093 |
}); |
| 1094 |
const deleteMapping = useMutation({ |
| 1095 |
mutationFn: (id) => channelManagerApi.deleteMapping(id), |
| 1096 |
onSuccess: () => { |
| 1097 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-mappings"] }); |
| 1098 |
showToast(__("Mapping deleted.", "yatra"), "success"); |
| 1099 |
setPendingDelete(null); |
| 1100 |
}, |
| 1101 |
onError: (e) => { |
| 1102 |
showToast(extractError(e), "error"); |
| 1103 |
setPendingDelete(null); |
| 1104 |
} |
| 1105 |
}); |
| 1106 |
const bulkAction = useMutation({ |
| 1107 |
mutationFn: (vars) => channelManagerApi.bulkMappings(vars.action, vars.ids), |
| 1108 |
onSuccess: (res) => { |
| 1109 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-mappings"] }); |
| 1110 |
setSelected(/* @__PURE__ */ new Set()); |
| 1111 |
showToast(res.message, res.ok ? "success" : "error"); |
| 1112 |
setPendingDelete(null); |
| 1113 |
}, |
| 1114 |
onError: (e) => { |
| 1115 |
showToast(extractError(e), "error"); |
| 1116 |
setPendingDelete(null); |
| 1117 |
} |
| 1118 |
}); |
| 1119 |
const channels = (channelsData == null ? void 0 : channelsData.data) ?? []; |
| 1120 |
const mappings = (mappingsData == null ? void 0 : mappingsData.data) ?? []; |
| 1121 |
const channelById = new Map( |
| 1122 |
channels.map((c) => [c.id, c]) |
| 1123 |
); |
| 1124 |
const tripIdsForLookup = reactExports.useMemo(() => { |
| 1125 |
const ids = /* @__PURE__ */ new Set(); |
| 1126 |
mappings.forEach((m) => { |
| 1127 |
if (m.trip_id) ids.add(Number(m.trip_id)); |
| 1128 |
}); |
| 1129 |
return Array.from(ids).sort((a, b) => a - b); |
| 1130 |
}, [mappings]); |
| 1131 |
const { data: tripsLookup } = useQuery({ |
| 1132 |
queryKey: ["channel-manager-mapping-trip-titles", tripIdsForLookup], |
| 1133 |
enabled: tripIdsForLookup.length > 0, |
| 1134 |
staleTime: 5 * 6e4, |
| 1135 |
queryFn: async () => { |
| 1136 |
var _a2; |
| 1137 |
const params = { |
| 1138 |
per_page: Math.max(tripIdsForLookup.length, 25), |
| 1139 |
include: tripIdsForLookup.join(",") |
| 1140 |
}; |
| 1141 |
const res = await apiClient.get("/trips", { params }); |
| 1142 |
const items = ((_a2 = res == null ? void 0 : res.data) == null ? void 0 : _a2.data) || (res == null ? void 0 : res.data) || res || []; |
| 1143 |
const map = /* @__PURE__ */ new Map(); |
| 1144 |
if (Array.isArray(items)) { |
| 1145 |
items.forEach((t) => { |
| 1146 |
var _a3; |
| 1147 |
const id = Number((t == null ? void 0 : t.id) || 0); |
| 1148 |
if (!id) return; |
| 1149 |
map.set(id, { |
| 1150 |
title: String((t == null ? void 0 : t.title) || (t == null ? void 0 : t.name) || `Trip #${id}`), |
| 1151 |
edit_url: (t == null ? void 0 : t.edit_url) || (t == null ? void 0 : t.admin_edit_url) || (t == null ? void 0 : t.permalink) || `${((_a3 = window == null ? void 0 : window.yatraAdmin) == null ? void 0 : _a3.adminUrl) || "admin.php"}?page=yatra&subpage=trips&action=edit&id=${id}` |
| 1152 |
}); |
| 1153 |
}); |
| 1154 |
} |
| 1155 |
return map; |
| 1156 |
} |
| 1157 |
}); |
| 1158 |
if (editing !== null) { |
| 1159 |
return /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1160 |
MappingEditForm, |
| 1161 |
{ |
| 1162 |
channels, |
| 1163 |
existing: editing, |
| 1164 |
onClose: () => setEditing(null) |
| 1165 |
} |
| 1166 |
); |
| 1167 |
} |
| 1168 |
const visibleIds = mappings.map((m) => m.id); |
| 1169 |
const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id)); |
| 1170 |
const toggleAll = () => { |
| 1171 |
if (allSelected) { |
| 1172 |
setSelected(/* @__PURE__ */ new Set()); |
| 1173 |
} else { |
| 1174 |
setSelected(new Set(visibleIds)); |
| 1175 |
} |
| 1176 |
}; |
| 1177 |
const toggleOne = (id) => { |
| 1178 |
setSelected((prev) => { |
| 1179 |
const next = new Set(prev); |
| 1180 |
next.has(id) ? next.delete(id) : next.add(id); |
| 1181 |
return next; |
| 1182 |
}); |
| 1183 |
}; |
| 1184 |
const runBulk = (action) => { |
| 1185 |
const ids = Array.from(selected); |
| 1186 |
if (ids.length === 0) return; |
| 1187 |
if (action === "delete") { |
| 1188 |
setPendingDelete({ kind: "bulk", ids }); |
| 1189 |
return; |
| 1190 |
} |
| 1191 |
bulkAction.mutate({ action, ids }); |
| 1192 |
}; |
| 1193 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ |
| 1194 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [ |
| 1195 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-2xl", children: [ |
| 1196 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Trip mappings", "yatra") }), |
| 1197 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __( |
| 1198 |
"A mapping links one of your trips to its counterpart product on a channel. One trip can be mapped to many channels (each can have its own price offset); one channel holds one mapping per trip.", |
| 1199 |
"yatra" |
| 1200 |
) }), |
| 1201 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-2 flex items-start gap-1.5", children: [ |
| 1202 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0" }), |
| 1203 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: __( |
| 1204 |
"Once mapped, every change to your trip's availability or price triggers a sync within seconds. Use the per-mapping offset to override the channel default when one trip needs different pricing logic.", |
| 1205 |
"yatra" |
| 1206 |
) }) |
| 1207 |
] }) |
| 1208 |
] }), |
| 1209 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [ |
| 1210 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1211 |
Select, |
| 1212 |
{ |
| 1213 |
value: String(filterChannel), |
| 1214 |
onChange: (e) => setFilterChannel(Number(e.target.value)), |
| 1215 |
className: "w-56", |
| 1216 |
children: [ |
| 1217 |
/* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "0", children: __("All channels", "yatra") }), |
| 1218 |
channels.map((c) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: c.id, children: c.display_name || c.channel_type }, c.id)) |
| 1219 |
] |
| 1220 |
} |
| 1221 |
), |
| 1222 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1223 |
Button, |
| 1224 |
{ |
| 1225 |
onClick: () => setShowAdd(true), |
| 1226 |
disabled: channels.length === 0, |
| 1227 |
title: channels.length === 0 ? __("Add at least one channel first.", "yatra") : void 0, |
| 1228 |
children: [ |
| 1229 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "mr-1.5 h-4 w-4" }), |
| 1230 |
__("Add mapping", "yatra") |
| 1231 |
] |
| 1232 |
} |
| 1233 |
) |
| 1234 |
] }) |
| 1235 |
] }), |
| 1236 |
showAdd && /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1237 |
MappingAddForm, |
| 1238 |
{ |
| 1239 |
channels, |
| 1240 |
defaultChannelId: filterChannel || ((_a = channels[0]) == null ? void 0 : _a.id) || 0, |
| 1241 |
onClose: () => setShowAdd(false) |
| 1242 |
} |
| 1243 |
), |
| 1244 |
selected.size > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-950/30 flex-wrap", children: [ |
| 1245 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-blue-900 dark:text-blue-200", children: __("{n} selected", "yatra").replace("{n}", String(selected.size)) }), |
| 1246 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex gap-2 flex-wrap", children: [ |
| 1247 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1248 |
Button, |
| 1249 |
{ |
| 1250 |
size: "sm", |
| 1251 |
variant: "outline", |
| 1252 |
onClick: () => runBulk("sync"), |
| 1253 |
disabled: bulkAction.isPending, |
| 1254 |
children: [ |
| 1255 |
/* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "mr-1.5 h-3.5 w-3.5" }), |
| 1256 |
__("Sync selected", "yatra") |
| 1257 |
] |
| 1258 |
} |
| 1259 |
), |
| 1260 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1261 |
Button, |
| 1262 |
{ |
| 1263 |
size: "sm", |
| 1264 |
variant: "outline", |
| 1265 |
onClick: () => runBulk("enable"), |
| 1266 |
disabled: bulkAction.isPending, |
| 1267 |
children: [ |
| 1268 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Power, { className: "mr-1.5 h-3.5 w-3.5" }), |
| 1269 |
__("Enable", "yatra") |
| 1270 |
] |
| 1271 |
} |
| 1272 |
), |
| 1273 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1274 |
Button, |
| 1275 |
{ |
| 1276 |
size: "sm", |
| 1277 |
variant: "outline", |
| 1278 |
onClick: () => runBulk("disable"), |
| 1279 |
disabled: bulkAction.isPending, |
| 1280 |
children: [ |
| 1281 |
/* @__PURE__ */ jsxRuntimeExports.jsx(PowerOff, { className: "mr-1.5 h-3.5 w-3.5" }), |
| 1282 |
__("Pause", "yatra") |
| 1283 |
] |
| 1284 |
} |
| 1285 |
), |
| 1286 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1287 |
Button, |
| 1288 |
{ |
| 1289 |
size: "sm", |
| 1290 |
variant: "outline", |
| 1291 |
className: "border-red-200 text-red-700 hover:bg-red-50 dark:border-red-700 dark:text-red-300 dark:hover:bg-red-900/30", |
| 1292 |
onClick: () => runBulk("delete"), |
| 1293 |
disabled: bulkAction.isPending, |
| 1294 |
children: [ |
| 1295 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }), |
| 1296 |
__("Delete", "yatra") |
| 1297 |
] |
| 1298 |
} |
| 1299 |
), |
| 1300 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1301 |
Button, |
| 1302 |
{ |
| 1303 |
size: "sm", |
| 1304 |
variant: "outline", |
| 1305 |
onClick: () => setSelected(/* @__PURE__ */ new Set()), |
| 1306 |
children: __("Clear", "yatra") |
| 1307 |
} |
| 1308 |
) |
| 1309 |
] }) |
| 1310 |
] }), |
| 1311 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ModuleTableSkeleton, { rows: 5, columns: 4 }) }) : mappings.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-10 text-center", children: [ |
| 1312 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Link, { className: "h-7 w-7 text-blue-600 dark:text-blue-400" }) }), |
| 1313 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-base font-semibold text-gray-900 dark:text-white", children: __("No mappings yet", "yatra") }), |
| 1314 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mx-auto mt-1 max-w-md text-sm text-gray-500 dark:text-gray-400", children: channels.length === 0 ? __( |
| 1315 |
"Add a channel first, then come back here to map your trips to it.", |
| 1316 |
"yatra" |
| 1317 |
) : __( |
| 1318 |
"Map each trip you want to sell on a channel to its counterpart product on that channel's side.", |
| 1319 |
"yatra" |
| 1320 |
) }) |
| 1321 |
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full", children: [ |
| 1322 |
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 1323 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-4 py-3 w-10", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1324 |
"input", |
| 1325 |
{ |
| 1326 |
type: "checkbox", |
| 1327 |
checked: allSelected, |
| 1328 |
onChange: toggleAll, |
| 1329 |
"aria-label": __("Select all", "yatra") |
| 1330 |
} |
| 1331 |
) }), |
| 1332 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Channel", "yatra") }), |
| 1333 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Trip", "yatra") }), |
| 1334 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("External product", "yatra") }), |
| 1335 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Offset", "yatra") }), |
| 1336 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Sync state", "yatra") }), |
| 1337 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Last sync", "yatra") }), |
| 1338 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { align: "right", children: __("Actions", "yatra") }) |
| 1339 |
] }) }), |
| 1340 |
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: mappings.map((m) => { |
| 1341 |
const ch = channelById.get(m.channel_id); |
| 1342 |
const isSelected = selected.has(m.id); |
| 1343 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1344 |
"tr", |
| 1345 |
{ |
| 1346 |
className: `hover:bg-gray-50 dark:hover:bg-gray-800/50 ${isSelected ? "bg-blue-50/50 dark:bg-blue-950/20" : ""}`, |
| 1347 |
children: [ |
| 1348 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1349 |
"input", |
| 1350 |
{ |
| 1351 |
type: "checkbox", |
| 1352 |
checked: isSelected, |
| 1353 |
onChange: () => toggleOne(m.id), |
| 1354 |
"aria-label": __("Select mapping", "yatra") |
| 1355 |
} |
| 1356 |
) }), |
| 1357 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm", children: (ch == null ? void 0 : ch.display_name) || (ch == null ? void 0 : ch.channel_type) || `#${m.channel_id}` }), |
| 1358 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm", children: (() => { |
| 1359 |
const trip = tripsLookup == null ? void 0 : tripsLookup.get(m.trip_id); |
| 1360 |
const title = (trip == null ? void 0 : trip.title) || `Trip #${m.trip_id}`; |
| 1361 |
const url = trip == null ? void 0 : trip.edit_url; |
| 1362 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [ |
| 1363 |
url ? /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1364 |
"a", |
| 1365 |
{ |
| 1366 |
href: url, |
| 1367 |
className: "truncate font-medium text-blue-600 hover:underline dark:text-blue-400", |
| 1368 |
title, |
| 1369 |
children: title |
| 1370 |
} |
| 1371 |
) : /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1372 |
"span", |
| 1373 |
{ |
| 1374 |
className: "truncate font-medium", |
| 1375 |
title, |
| 1376 |
children: title |
| 1377 |
} |
| 1378 |
), |
| 1379 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("code", { className: "flex-shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400", children: [ |
| 1380 |
"#", |
| 1381 |
m.trip_id |
| 1382 |
] }) |
| 1383 |
] }); |
| 1384 |
})() }), |
| 1385 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm", children: m.external_url ? /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1386 |
"a", |
| 1387 |
{ |
| 1388 |
href: m.external_url, |
| 1389 |
target: "_blank", |
| 1390 |
rel: "noopener noreferrer", |
| 1391 |
className: "text-blue-600 hover:underline dark:text-blue-400", |
| 1392 |
children: [ |
| 1393 |
m.external_product_id || "—", |
| 1394 |
/* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "ml-1 inline h-3 w-3" }) |
| 1395 |
] |
| 1396 |
} |
| 1397 |
) : /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800", children: m.external_product_id || "—" }) }), |
| 1398 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm", children: m.price_offset_percent !== 0 ? `${m.price_offset_percent > 0 ? "+" : ""}${m.price_offset_percent}%` : "—" }), |
| 1399 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1400 |
Badge, |
| 1401 |
{ |
| 1402 |
className: m.is_active ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300", |
| 1403 |
children: m.is_active ? __("Active", "yatra") : __("Paused", "yatra") |
| 1404 |
} |
| 1405 |
) }), |
| 1406 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-xs text-gray-500 dark:text-gray-400", children: m.last_synced_at ? new Date(m.last_synced_at).toLocaleString() : "—" }), |
| 1407 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex gap-1", children: [ |
| 1408 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1409 |
Button, |
| 1410 |
{ |
| 1411 |
variant: "outline", |
| 1412 |
size: "sm", |
| 1413 |
onClick: () => setEditing(m), |
| 1414 |
title: __("Edit this mapping", "yatra"), |
| 1415 |
children: __("Edit", "yatra") |
| 1416 |
} |
| 1417 |
), |
| 1418 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1419 |
Button, |
| 1420 |
{ |
| 1421 |
variant: "outline", |
| 1422 |
size: "sm", |
| 1423 |
onClick: () => syncMapping.mutate(m.id), |
| 1424 |
disabled: syncMapping.isPending, |
| 1425 |
title: __( |
| 1426 |
"Push inventory + pricing now", |
| 1427 |
"yatra" |
| 1428 |
), |
| 1429 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "h-3.5 w-3.5" }) |
| 1430 |
} |
| 1431 |
), |
| 1432 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1433 |
Button, |
| 1434 |
{ |
| 1435 |
variant: "outline", |
| 1436 |
size: "sm", |
| 1437 |
className: "border-red-200 text-red-700 hover:bg-red-50 dark:border-red-700 dark:text-red-300 dark:hover:bg-red-900/30", |
| 1438 |
onClick: () => setPendingDelete({ kind: "one", id: m.id }), |
| 1439 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "h-3.5 w-3.5" }) |
| 1440 |
} |
| 1441 |
) |
| 1442 |
] }) }) |
| 1443 |
] |
| 1444 |
}, |
| 1445 |
m.id |
| 1446 |
); |
| 1447 |
}) }) |
| 1448 |
] }) }) }) }), |
| 1449 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1450 |
ConfirmationDialog, |
| 1451 |
{ |
| 1452 |
isOpen: pendingDelete !== null, |
| 1453 |
onClose: () => { |
| 1454 |
if (!deleteMapping.isPending && !bulkAction.isPending) { |
| 1455 |
setPendingDelete(null); |
| 1456 |
} |
| 1457 |
}, |
| 1458 |
onConfirm: () => { |
| 1459 |
if (!pendingDelete) return; |
| 1460 |
if (pendingDelete.kind === "one") { |
| 1461 |
deleteMapping.mutate(pendingDelete.id); |
| 1462 |
} else { |
| 1463 |
bulkAction.mutate({ action: "delete", ids: pendingDelete.ids }); |
| 1464 |
} |
| 1465 |
}, |
| 1466 |
title: (pendingDelete == null ? void 0 : pendingDelete.kind) === "bulk" ? __("Delete selected mappings?", "yatra") : __("Delete mapping?", "yatra"), |
| 1467 |
description: (pendingDelete == null ? void 0 : pendingDelete.kind) === "bulk" ? __( |
| 1468 |
"Delete {n} mappings? They will stop syncing immediately. Existing bookings are unaffected.", |
| 1469 |
"yatra" |
| 1470 |
).replace("{n}", String(pendingDelete.ids.length)) : __( |
| 1471 |
"Delete this mapping? The trip will no longer sync to this channel. Existing bookings already in the inbox are not affected.", |
| 1472 |
"yatra" |
| 1473 |
), |
| 1474 |
confirmText: (pendingDelete == null ? void 0 : pendingDelete.kind) === "bulk" ? __("Delete mappings", "yatra") : __("Delete mapping", "yatra"), |
| 1475 |
cancelText: __("Cancel", "yatra"), |
| 1476 |
variant: "danger", |
| 1477 |
isLoading: deleteMapping.isPending || bulkAction.isPending |
| 1478 |
} |
| 1479 |
) |
| 1480 |
] }); |
| 1481 |
}; |
| 1482 |
const TripPicker = ({ value, onChange, placeholder }) => { |
| 1483 |
const [open, setOpen] = reactExports.useState(false); |
| 1484 |
const [search, setSearch] = reactExports.useState(""); |
| 1485 |
const [debounced, setDebounced] = reactExports.useState(""); |
| 1486 |
const wrapperRef = reactExports.useRef(null); |
| 1487 |
const searchInputRef = reactExports.useRef(null); |
| 1488 |
reactExports.useEffect(() => { |
| 1489 |
const t = setTimeout(() => setDebounced(search.trim()), 250); |
| 1490 |
return () => clearTimeout(t); |
| 1491 |
}, [search]); |
| 1492 |
reactExports.useEffect(() => { |
| 1493 |
if (!open) return; |
| 1494 |
const onDocClick = (e) => { |
| 1495 |
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) { |
| 1496 |
setOpen(false); |
| 1497 |
} |
| 1498 |
}; |
| 1499 |
document.addEventListener("mousedown", onDocClick); |
| 1500 |
return () => document.removeEventListener("mousedown", onDocClick); |
| 1501 |
}, [open]); |
| 1502 |
reactExports.useEffect(() => { |
| 1503 |
if (open && searchInputRef.current) { |
| 1504 |
searchInputRef.current.focus(); |
| 1505 |
} |
| 1506 |
}, [open]); |
| 1507 |
const { data: results = [], isFetching } = useQuery({ |
| 1508 |
queryKey: ["channel-manager-trip-picker", debounced], |
| 1509 |
queryFn: async () => { |
| 1510 |
var _a; |
| 1511 |
const params = { per_page: 25 }; |
| 1512 |
if (debounced) params.search = debounced; |
| 1513 |
const res = await apiClient.get("/trips", { params }); |
| 1514 |
const items = ((_a = res == null ? void 0 : res.data) == null ? void 0 : _a.data) || (res == null ? void 0 : res.data) || res || []; |
| 1515 |
if (!Array.isArray(items)) return []; |
| 1516 |
return items.map((t) => ({ |
| 1517 |
id: Number((t == null ? void 0 : t.id) || 0), |
| 1518 |
title: String((t == null ? void 0 : t.title) || (t == null ? void 0 : t.name) || `Trip #${t == null ? void 0 : t.id}`) |
| 1519 |
})).filter((t) => t.id > 0); |
| 1520 |
}, |
| 1521 |
enabled: open, |
| 1522 |
staleTime: 6e4 |
| 1523 |
}); |
| 1524 |
const { data: selectedTrip } = useQuery({ |
| 1525 |
queryKey: ["channel-manager-trip-picker:resolve", value], |
| 1526 |
queryFn: async () => { |
| 1527 |
var _a; |
| 1528 |
if (!value) return null; |
| 1529 |
try { |
| 1530 |
const res = await apiClient.get(`/trips/${value}`); |
| 1531 |
const t = ((_a = res == null ? void 0 : res.data) == null ? void 0 : _a.data) || (res == null ? void 0 : res.data) || res || null; |
| 1532 |
if (!t || !t.id) return null; |
| 1533 |
return { |
| 1534 |
id: Number(t.id), |
| 1535 |
title: String(t.title || t.name || `Trip #${t.id}`) |
| 1536 |
}; |
| 1537 |
} catch { |
| 1538 |
return null; |
| 1539 |
} |
| 1540 |
}, |
| 1541 |
enabled: value > 0, |
| 1542 |
staleTime: 5 * 6e4 |
| 1543 |
}); |
| 1544 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: wrapperRef, className: "relative", children: [ |
| 1545 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1546 |
"button", |
| 1547 |
{ |
| 1548 |
type: "button", |
| 1549 |
onClick: () => setOpen((o) => !o), |
| 1550 |
className: "flex h-12 w-full items-center justify-between rounded-md border-2 border-gray-300 bg-white px-4 text-left text-base text-gray-900 transition-colors hover:border-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:hover:border-gray-500", |
| 1551 |
"aria-haspopup": "listbox", |
| 1552 |
"aria-expanded": open, |
| 1553 |
children: [ |
| 1554 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex min-w-0 items-center gap-2", children: value > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ |
| 1555 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate font-medium", children: (selectedTrip == null ? void 0 : selectedTrip.title) || `Trip #${value}` }), |
| 1556 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex-shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-400", children: [ |
| 1557 |
"#", |
| 1558 |
value |
| 1559 |
] }) |
| 1560 |
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-500 dark:text-gray-400", children: placeholder || __("Search and select a trip…", "yatra") }) }), |
| 1561 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex flex-shrink-0 items-center gap-1", children: [ |
| 1562 |
value > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1563 |
"span", |
| 1564 |
{ |
| 1565 |
role: "button", |
| 1566 |
tabIndex: 0, |
| 1567 |
onClick: (e) => { |
| 1568 |
e.stopPropagation(); |
| 1569 |
onChange(0); |
| 1570 |
}, |
| 1571 |
onKeyDown: (e) => { |
| 1572 |
if (e.key === "Enter" || e.key === " ") { |
| 1573 |
e.preventDefault(); |
| 1574 |
e.stopPropagation(); |
| 1575 |
onChange(0); |
| 1576 |
} |
| 1577 |
}, |
| 1578 |
className: "rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-700", |
| 1579 |
"aria-label": __("Clear selection", "yatra"), |
| 1580 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "h-3.5 w-3.5" }) |
| 1581 |
} |
| 1582 |
), |
| 1583 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1584 |
ChevronDown, |
| 1585 |
{ |
| 1586 |
className: `h-4 w-4 text-gray-400 transition-transform ${open ? "rotate-180" : ""}` |
| 1587 |
} |
| 1588 |
) |
| 1589 |
] }) |
| 1590 |
] |
| 1591 |
} |
| 1592 |
), |
| 1593 |
open && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute z-30 mt-1 w-full overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800", children: [ |
| 1594 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-b border-gray-100 p-2 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ |
| 1595 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Search, { className: "absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" }), |
| 1596 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1597 |
"input", |
| 1598 |
{ |
| 1599 |
ref: searchInputRef, |
| 1600 |
type: "text", |
| 1601 |
value: search, |
| 1602 |
onChange: (e) => setSearch(e.target.value), |
| 1603 |
placeholder: __("Type to search trips by title…", "yatra"), |
| 1604 |
className: "w-full rounded border border-gray-200 bg-white py-1.5 pl-8 pr-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-900" |
| 1605 |
} |
| 1606 |
) |
| 1607 |
] }) }), |
| 1608 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-h-72 overflow-auto", children: isFetching ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center p-6 text-sm text-gray-500", children: [ |
| 1609 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), |
| 1610 |
__("Searching…", "yatra") |
| 1611 |
] }) : results.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-6 text-center text-sm text-gray-500 dark:text-gray-400", children: debounced ? __("No trips match that search.", "yatra") : __("No trips found.", "yatra") }) : /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { role: "listbox", className: "py-1", children: results.map((t) => { |
| 1612 |
const isSelected = t.id === value; |
| 1613 |
return /* @__PURE__ */ jsxRuntimeExports.jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1614 |
"button", |
| 1615 |
{ |
| 1616 |
type: "button", |
| 1617 |
onClick: () => { |
| 1618 |
onChange(t.id, t.title); |
| 1619 |
setOpen(false); |
| 1620 |
setSearch(""); |
| 1621 |
}, |
| 1622 |
className: `flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${isSelected ? "bg-blue-50 text-blue-900 dark:bg-blue-950/40 dark:text-blue-200" : ""}`, |
| 1623 |
children: [ |
| 1624 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [ |
| 1625 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", children: t.title }), |
| 1626 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex-shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-[11px] text-gray-500 dark:bg-gray-700 dark:text-gray-400", children: [ |
| 1627 |
"#", |
| 1628 |
t.id |
| 1629 |
] }) |
| 1630 |
] }), |
| 1631 |
isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "h-4 w-4 flex-shrink-0 text-blue-600 dark:text-blue-400" }) |
| 1632 |
] |
| 1633 |
} |
| 1634 |
) }, t.id); |
| 1635 |
}) }) }) |
| 1636 |
] }) |
| 1637 |
] }); |
| 1638 |
}; |
| 1639 |
const MappingAddForm = ({ channels, defaultChannelId, onClose }) => { |
| 1640 |
const queryClient = useQueryClient(); |
| 1641 |
const { showToast } = useToast(); |
| 1642 |
const [channelId, setChannelId] = reactExports.useState(defaultChannelId); |
| 1643 |
const [tripId, setTripId] = reactExports.useState(0); |
| 1644 |
const [externalProductId, setExternalProductId] = reactExports.useState(""); |
| 1645 |
const [externalUrl, setExternalUrl] = reactExports.useState(""); |
| 1646 |
const [offset, setOffset] = reactExports.useState(0); |
| 1647 |
const [syncInventory, setSyncInventory] = reactExports.useState(true); |
| 1648 |
const [syncPricing, setSyncPricing] = reactExports.useState(true); |
| 1649 |
const create = useMutation({ |
| 1650 |
mutationFn: () => channelManagerApi.createMapping({ |
| 1651 |
channel_id: channelId, |
| 1652 |
trip_id: tripId, |
| 1653 |
external_product_id: externalProductId, |
| 1654 |
external_url: externalUrl, |
| 1655 |
price_offset_percent: offset, |
| 1656 |
is_active: true, |
| 1657 |
sync_inventory: syncInventory, |
| 1658 |
sync_pricing: syncPricing |
| 1659 |
}), |
| 1660 |
onSuccess: () => { |
| 1661 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-mappings"] }); |
| 1662 |
showToast(__("Mapping created.", "yatra"), "success"); |
| 1663 |
onClose(); |
| 1664 |
}, |
| 1665 |
onError: (e) => showToast(extractError(e), "error") |
| 1666 |
}); |
| 1667 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 1668 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 1669 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("New trip mapping", "yatra") }), |
| 1670 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 1671 |
"Connect one of your trips to a product on the channel. You'll need the channel's product ID — check the channel's partner portal or the URL of the product page.", |
| 1672 |
"yatra" |
| 1673 |
) }) |
| 1674 |
] }), |
| 1675 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-5", children: [ |
| 1676 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-5 md:grid-cols-2", children: [ |
| 1677 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1678 |
FormField, |
| 1679 |
{ |
| 1680 |
label: __("Channel", "yatra"), |
| 1681 |
description: __("Which channel this mapping pushes to.", "yatra"), |
| 1682 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1683 |
Select, |
| 1684 |
{ |
| 1685 |
value: String(channelId), |
| 1686 |
onChange: (e) => setChannelId(Number(e.target.value)), |
| 1687 |
children: channels.map((c) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: c.id, children: c.display_name || c.channel_type }, c.id)) |
| 1688 |
} |
| 1689 |
) |
| 1690 |
} |
| 1691 |
), |
| 1692 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1693 |
FormField, |
| 1694 |
{ |
| 1695 |
label: __("Trip", "yatra"), |
| 1696 |
description: __( |
| 1697 |
"Pick the Yatra trip this mapping pushes to the channel. Start typing the trip title — the list filters as you type.", |
| 1698 |
"yatra" |
| 1699 |
), |
| 1700 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1701 |
TripPicker, |
| 1702 |
{ |
| 1703 |
value: tripId, |
| 1704 |
onChange: (id) => setTripId(id), |
| 1705 |
placeholder: __("Search and select a trip…", "yatra") |
| 1706 |
} |
| 1707 |
) |
| 1708 |
} |
| 1709 |
), |
| 1710 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1711 |
FormField, |
| 1712 |
{ |
| 1713 |
label: __("Channel product ID", "yatra"), |
| 1714 |
description: __( |
| 1715 |
"The product's ID on the channel side. For Viator this is a product code (e.g. 12345P1); for GetYourGuide it's the offer/option ID. Copy it from the channel's partner portal.", |
| 1716 |
"yatra" |
| 1717 |
), |
| 1718 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1719 |
Input, |
| 1720 |
{ |
| 1721 |
value: externalProductId, |
| 1722 |
onChange: (e) => setExternalProductId(e.target.value), |
| 1723 |
placeholder: __( |
| 1724 |
"e.g. 12345P1 (Viator) or 87654 (GetYourGuide)", |
| 1725 |
"yatra" |
| 1726 |
) |
| 1727 |
} |
| 1728 |
) |
| 1729 |
} |
| 1730 |
), |
| 1731 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1732 |
FormField, |
| 1733 |
{ |
| 1734 |
label: __("Public listing URL (optional)", "yatra"), |
| 1735 |
description: __( |
| 1736 |
"Link to the channel's public listing for this product. Shown as a quick-jump link in the mappings table so you can verify changes after syncing.", |
| 1737 |
"yatra" |
| 1738 |
), |
| 1739 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1740 |
Input, |
| 1741 |
{ |
| 1742 |
value: externalUrl, |
| 1743 |
onChange: (e) => setExternalUrl(e.target.value), |
| 1744 |
placeholder: "https://www.viator.com/..." |
| 1745 |
} |
| 1746 |
) |
| 1747 |
} |
| 1748 |
), |
| 1749 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1750 |
FormField, |
| 1751 |
{ |
| 1752 |
label: __("Per-mapping price offset (%)", "yatra"), |
| 1753 |
description: __( |
| 1754 |
"Overrides the channel's default offset for this trip only. Use 0 to inherit the channel default. Set positive to mark up, negative to discount.", |
| 1755 |
"yatra" |
| 1756 |
), |
| 1757 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1758 |
Input, |
| 1759 |
{ |
| 1760 |
type: "number", |
| 1761 |
step: "0.01", |
| 1762 |
value: offset, |
| 1763 |
onChange: (e) => setOffset(Number(e.target.value) || 0), |
| 1764 |
placeholder: "0" |
| 1765 |
} |
| 1766 |
) |
| 1767 |
} |
| 1768 |
) |
| 1769 |
] }), |
| 1770 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-4 md:grid-cols-2 pt-2 border-t border-gray-200 dark:border-gray-700", children: [ |
| 1771 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1772 |
ToggleRow, |
| 1773 |
{ |
| 1774 |
label: __("Sync inventory", "yatra"), |
| 1775 |
description: __( |
| 1776 |
"Push seat availability to this channel. Turn off if you're using the channel's own inventory system instead.", |
| 1777 |
"yatra" |
| 1778 |
), |
| 1779 |
checked: syncInventory, |
| 1780 |
onChange: setSyncInventory |
| 1781 |
} |
| 1782 |
), |
| 1783 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1784 |
ToggleRow, |
| 1785 |
{ |
| 1786 |
label: __("Sync pricing", "yatra"), |
| 1787 |
description: __( |
| 1788 |
"Push prices to this channel. Turn off when prices are managed on the channel side (e.g. for promotions you control there).", |
| 1789 |
"yatra" |
| 1790 |
), |
| 1791 |
checked: syncPricing, |
| 1792 |
onChange: setSyncPricing |
| 1793 |
} |
| 1794 |
) |
| 1795 |
] }), |
| 1796 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [ |
| 1797 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") }), |
| 1798 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1799 |
Button, |
| 1800 |
{ |
| 1801 |
onClick: () => create.mutate(), |
| 1802 |
disabled: create.isPending || channelId <= 0 || tripId <= 0 || externalProductId.trim() === "", |
| 1803 |
children: [ |
| 1804 |
create.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "mr-1.5 h-4 w-4" }), |
| 1805 |
__("Create mapping", "yatra") |
| 1806 |
] |
| 1807 |
} |
| 1808 |
) |
| 1809 |
] }) |
| 1810 |
] }) |
| 1811 |
] }); |
| 1812 |
}; |
| 1813 |
const MappingEditForm = ({ channels, existing, onClose }) => { |
| 1814 |
const queryClient = useQueryClient(); |
| 1815 |
const { showToast } = useToast(); |
| 1816 |
const [externalProductId, setExternalProductId] = reactExports.useState( |
| 1817 |
existing.external_product_id ?? "" |
| 1818 |
); |
| 1819 |
const [externalUrl, setExternalUrl] = reactExports.useState(existing.external_url ?? ""); |
| 1820 |
const [offset, setOffset] = reactExports.useState( |
| 1821 |
existing.price_offset_percent ?? 0 |
| 1822 |
); |
| 1823 |
const [isActive, setIsActive] = reactExports.useState(existing.is_active); |
| 1824 |
const [syncInventory, setSyncInventory] = reactExports.useState( |
| 1825 |
existing.sync_inventory |
| 1826 |
); |
| 1827 |
const [syncPricing, setSyncPricing] = reactExports.useState( |
| 1828 |
existing.sync_pricing |
| 1829 |
); |
| 1830 |
const channel = channels.find((c) => c.id === existing.channel_id); |
| 1831 |
const save = useMutation({ |
| 1832 |
mutationFn: () => channelManagerApi.updateMapping(existing.id, { |
| 1833 |
external_product_id: externalProductId, |
| 1834 |
external_url: externalUrl, |
| 1835 |
price_offset_percent: offset, |
| 1836 |
is_active: isActive, |
| 1837 |
sync_inventory: syncInventory, |
| 1838 |
sync_pricing: syncPricing |
| 1839 |
}), |
| 1840 |
onSuccess: () => { |
| 1841 |
queryClient.invalidateQueries({ queryKey: ["channel-manager-mappings"] }); |
| 1842 |
showToast(__("Mapping saved.", "yatra"), "success"); |
| 1843 |
onClose(); |
| 1844 |
}, |
| 1845 |
onError: (e) => showToast(extractError(e), "error") |
| 1846 |
}); |
| 1847 |
const sync = useMutation({ |
| 1848 |
mutationFn: () => channelManagerApi.syncMapping(existing.id), |
| 1849 |
onSuccess: (res) => showToast( |
| 1850 |
res.ok ? __("Synced.", "yatra") : res.error || __("Sync failed.", "yatra"), |
| 1851 |
res.ok ? "success" : "error" |
| 1852 |
), |
| 1853 |
onError: (e) => showToast(extractError(e), "error") |
| 1854 |
}); |
| 1855 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ |
| 1856 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [ |
| 1857 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 1858 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-white", children: __("Edit mapping", "yatra") }), |
| 1859 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __( |
| 1860 |
"Channel and trip are locked once a mapping exists. Delete + recreate if you need to re-point this mapping elsewhere.", |
| 1861 |
"yatra" |
| 1862 |
) }) |
| 1863 |
] }), |
| 1864 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 flex-wrap", children: [ |
| 1865 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 1866 |
Button, |
| 1867 |
{ |
| 1868 |
variant: "outline", |
| 1869 |
onClick: () => sync.mutate(), |
| 1870 |
disabled: sync.isPending, |
| 1871 |
children: [ |
| 1872 |
sync.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "mr-1.5 h-4 w-4" }), |
| 1873 |
__("Sync now", "yatra") |
| 1874 |
] |
| 1875 |
} |
| 1876 |
), |
| 1877 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Back to list", "yatra") }), |
| 1878 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => save.mutate(), disabled: save.isPending, children: [ |
| 1879 |
save.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-1.5 h-4 w-4" }), |
| 1880 |
__("Save changes", "yatra") |
| 1881 |
] }) |
| 1882 |
] }) |
| 1883 |
] }), |
| 1884 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 1885 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 1886 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Mapping link", "yatra") }), |
| 1887 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 1888 |
"How this trip maps to the channel's product. Updating the external product ID re-points subsequent syncs but does not retroactively affect bookings already received.", |
| 1889 |
"yatra" |
| 1890 |
) }) |
| 1891 |
] }), |
| 1892 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "space-y-5", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-5 md:grid-cols-2", children: [ |
| 1893 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1894 |
FormField, |
| 1895 |
{ |
| 1896 |
label: __("Channel", "yatra"), |
| 1897 |
description: __( |
| 1898 |
"Immutable. To move this trip to a different channel, delete this mapping and create a new one.", |
| 1899 |
"yatra" |
| 1900 |
), |
| 1901 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1902 |
Input, |
| 1903 |
{ |
| 1904 |
value: (channel == null ? void 0 : channel.display_name) || (channel == null ? void 0 : channel.channel_type) || `#${existing.channel_id}`, |
| 1905 |
disabled: true |
| 1906 |
} |
| 1907 |
) |
| 1908 |
} |
| 1909 |
), |
| 1910 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1911 |
FormField, |
| 1912 |
{ |
| 1913 |
label: __("Trip ID", "yatra"), |
| 1914 |
description: __("Immutable. One trip per channel.", "yatra"), |
| 1915 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Input, { value: `#${existing.trip_id}`, disabled: true }) |
| 1916 |
} |
| 1917 |
), |
| 1918 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1919 |
FormField, |
| 1920 |
{ |
| 1921 |
label: __("Channel product ID", "yatra"), |
| 1922 |
description: __( |
| 1923 |
"The product's ID on the channel side. Editing this re-points the mapping — all future syncs target the new product. Existing bookings already received are not modified.", |
| 1924 |
"yatra" |
| 1925 |
), |
| 1926 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1927 |
Input, |
| 1928 |
{ |
| 1929 |
value: externalProductId, |
| 1930 |
onChange: (e) => setExternalProductId(e.target.value) |
| 1931 |
} |
| 1932 |
) |
| 1933 |
} |
| 1934 |
), |
| 1935 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1936 |
FormField, |
| 1937 |
{ |
| 1938 |
label: __("Public listing URL (optional)", "yatra"), |
| 1939 |
description: __( |
| 1940 |
"Quick-jump link shown in the mappings table.", |
| 1941 |
"yatra" |
| 1942 |
), |
| 1943 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1944 |
Input, |
| 1945 |
{ |
| 1946 |
value: externalUrl, |
| 1947 |
onChange: (e) => setExternalUrl(e.target.value) |
| 1948 |
} |
| 1949 |
) |
| 1950 |
} |
| 1951 |
), |
| 1952 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1953 |
FormField, |
| 1954 |
{ |
| 1955 |
label: __("Per-mapping price offset (%)", "yatra"), |
| 1956 |
description: __( |
| 1957 |
"Overrides the channel's default offset for this trip only. 0 inherits the channel default.", |
| 1958 |
"yatra" |
| 1959 |
), |
| 1960 |
children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1961 |
Input, |
| 1962 |
{ |
| 1963 |
type: "number", |
| 1964 |
step: "0.01", |
| 1965 |
value: offset, |
| 1966 |
onChange: (e) => setOffset(Number(e.target.value) || 0) |
| 1967 |
} |
| 1968 |
) |
| 1969 |
} |
| 1970 |
) |
| 1971 |
] }) }) |
| 1972 |
] }), |
| 1973 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 1974 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 1975 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Sync controls", "yatra") }), |
| 1976 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 1977 |
"Pause or partially-sync this mapping without losing its configuration. Pausing keeps all settings; flipping it back resumes from where it left off.", |
| 1978 |
"yatra" |
| 1979 |
) }) |
| 1980 |
] }), |
| 1981 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [ |
| 1982 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1983 |
ToggleRow, |
| 1984 |
{ |
| 1985 |
label: __("Active", "yatra"), |
| 1986 |
description: __( |
| 1987 |
"When off, this mapping won't push inventory or pricing, and bookings received for it are still ingested (so cancellations of pre-existing bookings keep working).", |
| 1988 |
"yatra" |
| 1989 |
), |
| 1990 |
checked: isActive, |
| 1991 |
onChange: setIsActive |
| 1992 |
} |
| 1993 |
), |
| 1994 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 1995 |
ToggleRow, |
| 1996 |
{ |
| 1997 |
label: __("Sync inventory", "yatra"), |
| 1998 |
description: __( |
| 1999 |
"Push seat availability to this channel. Turn off when the channel manages its own inventory.", |
| 2000 |
"yatra" |
| 2001 |
), |
| 2002 |
checked: syncInventory, |
| 2003 |
onChange: setSyncInventory |
| 2004 |
} |
| 2005 |
), |
| 2006 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2007 |
ToggleRow, |
| 2008 |
{ |
| 2009 |
label: __("Sync pricing", "yatra"), |
| 2010 |
description: __( |
| 2011 |
"Push prices to this channel. Turn off for channels that run their own promotions.", |
| 2012 |
"yatra" |
| 2013 |
), |
| 2014 |
checked: syncPricing, |
| 2015 |
onChange: setSyncPricing |
| 2016 |
} |
| 2017 |
) |
| 2018 |
] }) |
| 2019 |
] }) |
| 2020 |
] }); |
| 2021 |
}; |
| 2022 |
const BookingsSection = () => { |
| 2023 |
const { data, isLoading } = useQuery({ |
| 2024 |
queryKey: ["channel-manager-bookings"], |
| 2025 |
queryFn: () => channelManagerApi.listBookings({ per_page: 100 }), |
| 2026 |
refetchInterval: 3e4 |
| 2027 |
}); |
| 2028 |
if (isLoading) { |
| 2029 |
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }); |
| 2030 |
} |
| 2031 |
const rows = (data == null ? void 0 : data.data) ?? []; |
| 2032 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ |
| 2033 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 2034 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2", children: [ |
| 2035 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Inbox, { className: "h-5 w-5 text-blue-500" }), |
| 2036 |
__("Bookings inbox", "yatra") |
| 2037 |
] }), |
| 2038 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-3xl", children: __( |
| 2039 |
"Every booking received from connected OTAs lands here. The flow is two-phase for safety: bookings are first staged (received), then promoted into the main booking list. Promoted bookings go through the same notifications, payments and reporting as direct sales.", |
| 2040 |
"yatra" |
| 2041 |
) }), |
| 2042 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 flex flex-wrap gap-2 text-xs", children: [ |
| 2043 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2044 |
StatusLegend, |
| 2045 |
{ |
| 2046 |
color: "blue", |
| 2047 |
label: __("received", "yatra"), |
| 2048 |
description: __("staged — not yet a real booking", "yatra") |
| 2049 |
} |
| 2050 |
), |
| 2051 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2052 |
StatusLegend, |
| 2053 |
{ |
| 2054 |
color: "green", |
| 2055 |
label: __("promoted", "yatra"), |
| 2056 |
description: __("live booking, visible everywhere", "yatra") |
| 2057 |
} |
| 2058 |
), |
| 2059 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2060 |
StatusLegend, |
| 2061 |
{ |
| 2062 |
color: "red", |
| 2063 |
label: __("failed", "yatra"), |
| 2064 |
description: __( |
| 2065 |
"promotion failed — check the error column", |
| 2066 |
"yatra" |
| 2067 |
) |
| 2068 |
} |
| 2069 |
) |
| 2070 |
] }) |
| 2071 |
] }), |
| 2072 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: rows.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-10 text-center", children: [ |
| 2073 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Inbox, { className: "h-7 w-7 text-blue-600 dark:text-blue-400" }) }), |
| 2074 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-base font-semibold text-gray-900 dark:text-white", children: __("No channel bookings yet", "yatra") }), |
| 2075 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mx-auto mt-1 max-w-md text-sm text-gray-500 dark:text-gray-400", children: __( |
| 2076 |
"When an OTA sends a booking webhook, it'll appear here within seconds. The list auto-refreshes every 30 seconds while this tab is open.", |
| 2077 |
"yatra" |
| 2078 |
) }) |
| 2079 |
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full", children: [ |
| 2080 |
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 2081 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Channel ref", "yatra") }), |
| 2082 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Customer", "yatra") }), |
| 2083 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Departure", "yatra") }), |
| 2084 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Travelers", "yatra") }), |
| 2085 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Amount", "yatra") }), |
| 2086 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Status", "yatra") }), |
| 2087 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Booking ID", "yatra") }), |
| 2088 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Received", "yatra") }) |
| 2089 |
] }) }), |
| 2090 |
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: rows.map((b) => /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 2091 |
"tr", |
| 2092 |
{ |
| 2093 |
className: "hover:bg-gray-50 dark:hover:bg-gray-800/50", |
| 2094 |
children: [ |
| 2095 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-xs font-mono", children: b.external_booking_id }), |
| 2096 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-4 py-3 text-sm", children: [ |
| 2097 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-900 dark:text-white", children: b.customer_name || "—" }), |
| 2098 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: b.customer_email }) |
| 2099 |
] }), |
| 2100 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm text-gray-700 dark:text-gray-200", children: b.departure_date || "—" }), |
| 2101 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm", children: b.travelers_total }), |
| 2102 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-4 py-3 text-sm", children: [ |
| 2103 |
b.currency, |
| 2104 |
" ", |
| 2105 |
Number(b.total_amount).toFixed(2) |
| 2106 |
] }), |
| 2107 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-4 py-3", children: [ |
| 2108 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2109 |
Badge, |
| 2110 |
{ |
| 2111 |
className: b.processing_status === "promoted" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : b.processing_status === "failed" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", |
| 2112 |
children: b.processing_status |
| 2113 |
} |
| 2114 |
), |
| 2115 |
b.error_message && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-xs text-red-600 dark:text-red-400 max-w-xs truncate", children: b.error_message }) |
| 2116 |
] }), |
| 2117 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-xs", children: b.yatra_booking_id ? `#${b.yatra_booking_id}` : "—" }), |
| 2118 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-xs text-gray-500 dark:text-gray-400", children: new Date(b.received_at).toLocaleString() }) |
| 2119 |
] |
| 2120 |
}, |
| 2121 |
b.id |
| 2122 |
)) }) |
| 2123 |
] }) }) }) }) |
| 2124 |
] }); |
| 2125 |
}; |
| 2126 |
const ReconciliationSection = () => { |
| 2127 |
const { data, isLoading, refetch, isFetching } = useQuery({ |
| 2128 |
queryKey: ["cm-reconciliation"], |
| 2129 |
queryFn: () => channelManagerApi.getReconciliation(), |
| 2130 |
refetchOnWindowFocus: false |
| 2131 |
}); |
| 2132 |
if (isLoading) { |
| 2133 |
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }); |
| 2134 |
} |
| 2135 |
const report = data == null ? void 0 : data.data; |
| 2136 |
if (!report) return null; |
| 2137 |
const t = report.totals; |
| 2138 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ |
| 2139 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [ |
| 2140 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 2141 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Reconciliation report", "yatra") }), |
| 2142 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __( |
| 2143 |
"Generated on demand from the sync log + mapping + booking tables. Refresh to recompute.", |
| 2144 |
"yatra" |
| 2145 |
) }), |
| 2146 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-400 mt-0.5", children: sprintf( |
| 2147 |
/* translators: %s: timestamp */ |
| 2148 |
__("Generated at %s", "yatra"), |
| 2149 |
report.generated_at |
| 2150 |
) }) |
| 2151 |
] }), |
| 2152 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 2153 |
Button, |
| 2154 |
{ |
| 2155 |
variant: "outline", |
| 2156 |
onClick: () => refetch(), |
| 2157 |
disabled: isFetching, |
| 2158 |
children: [ |
| 2159 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2160 |
RefreshCw, |
| 2161 |
{ |
| 2162 |
className: `h-4 w-4 mr-1 ${isFetching ? "animate-spin" : ""}` |
| 2163 |
} |
| 2164 |
), |
| 2165 |
__("Refresh", "yatra") |
| 2166 |
] |
| 2167 |
} |
| 2168 |
) |
| 2169 |
] }), |
| 2170 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-3", children: [ |
| 2171 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2172 |
KpiCard, |
| 2173 |
{ |
| 2174 |
label: __("Channels enabled", "yatra"), |
| 2175 |
value: `${t.channels.enabled} / ${t.channels.total}`, |
| 2176 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Network, { className: "h-4 w-4 text-blue-500" }) |
| 2177 |
} |
| 2178 |
), |
| 2179 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2180 |
KpiCard, |
| 2181 |
{ |
| 2182 |
label: __("Active mappings", "yatra"), |
| 2183 |
value: t.mappings_active, |
| 2184 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Link, { className: "h-4 w-4 text-indigo-500" }) |
| 2185 |
} |
| 2186 |
), |
| 2187 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2188 |
KpiCard, |
| 2189 |
{ |
| 2190 |
label: __("Stale mappings", "yatra"), |
| 2191 |
value: t.inventory_stale, |
| 2192 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), |
| 2193 |
tone: t.inventory_stale > 0 ? "warn" : "ok" |
| 2194 |
} |
| 2195 |
), |
| 2196 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2197 |
KpiCard, |
| 2198 |
{ |
| 2199 |
label: __("Open breakers", "yatra"), |
| 2200 |
value: t.breakers_open, |
| 2201 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(PowerOff, { className: "h-4 w-4 text-red-500" }), |
| 2202 |
tone: t.breakers_open > 0 ? "bad" : "ok" |
| 2203 |
} |
| 2204 |
), |
| 2205 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2206 |
KpiCard, |
| 2207 |
{ |
| 2208 |
label: __("Pending booking promotions", "yatra"), |
| 2209 |
value: t.bookings_pending, |
| 2210 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Inbox, { className: "h-4 w-4 text-purple-500" }), |
| 2211 |
tone: t.bookings_pending > 0 ? "warn" : "ok" |
| 2212 |
} |
| 2213 |
), |
| 2214 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2215 |
KpiCard, |
| 2216 |
{ |
| 2217 |
label: __("Failed booking promotions (7d)", "yatra"), |
| 2218 |
value: t.bookings_failed_7d, |
| 2219 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-4 w-4 text-red-500" }), |
| 2220 |
tone: t.bookings_failed_7d > 0 ? "bad" : "ok" |
| 2221 |
} |
| 2222 |
), |
| 2223 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2224 |
KpiCard, |
| 2225 |
{ |
| 2226 |
label: __("Syncs OK (24h)", "yatra"), |
| 2227 |
value: t.syncs_24h.success, |
| 2228 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-green-500" }) |
| 2229 |
} |
| 2230 |
), |
| 2231 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2232 |
KpiCard, |
| 2233 |
{ |
| 2234 |
label: __("Syncs failed (24h)", "yatra"), |
| 2235 |
value: t.syncs_24h.failed, |
| 2236 |
icon: /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), |
| 2237 |
tone: t.syncs_24h.failed > 0 ? "warn" : "ok" |
| 2238 |
} |
| 2239 |
) |
| 2240 |
] }), |
| 2241 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 2242 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [ |
| 2243 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: __("Per channel", "yatra") }), |
| 2244 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __( |
| 2245 |
"Channels marked “Needs attention” have stale mappings, pending bookings, recent failures, or a tripped breaker.", |
| 2246 |
"yatra" |
| 2247 |
) }) |
| 2248 |
] }), |
| 2249 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "min-w-full text-sm", children: [ |
| 2250 |
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 2251 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Channel", "yatra") }), |
| 2252 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Mappings", "yatra") }), |
| 2253 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Bookings", "yatra") }), |
| 2254 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Syncs (24h)", "yatra") }), |
| 2255 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Breaker", "yatra") }), |
| 2256 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Latest failure", "yatra") }), |
| 2257 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Status", "yatra") }) |
| 2258 |
] }) }), |
| 2259 |
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: report.channels.map((c) => /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 2260 |
"tr", |
| 2261 |
{ |
| 2262 |
className: c.needs_attention ? "bg-amber-50/40 dark:bg-amber-900/10" : "", |
| 2263 |
children: [ |
| 2264 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2", children: [ |
| 2265 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium text-gray-900 dark:text-white", children: c.channel.display_name || `#${c.channel.id}` }), |
| 2266 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 font-mono", children: c.channel.type }) |
| 2267 |
] }), |
| 2268 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2 text-xs", children: [ |
| 2269 |
sprintf(__("%d active", "yatra"), c.mappings.active), |
| 2270 |
c.mappings.stale > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-amber-600 dark:text-amber-400 mt-0.5", children: sprintf(__("%d stale", "yatra"), c.mappings.stale) }) |
| 2271 |
] }), |
| 2272 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2 text-xs", children: [ |
| 2273 |
c.bookings.pending > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-amber-600 dark:text-amber-400", children: sprintf( |
| 2274 |
__("%d pending", "yatra"), |
| 2275 |
c.bookings.pending |
| 2276 |
) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-400", children: "—" }), |
| 2277 |
c.bookings.failed_7d > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-600 dark:text-red-400", children: sprintf( |
| 2278 |
__("%d failed (7d)", "yatra"), |
| 2279 |
c.bookings.failed_7d |
| 2280 |
) }) |
| 2281 |
] }), |
| 2282 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2 text-xs", children: [ |
| 2283 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-green-700 dark:text-green-400", children: [ |
| 2284 |
c.syncs.success_24h, |
| 2285 |
" OK" |
| 2286 |
] }), |
| 2287 |
c.syncs.failed_24h > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ |
| 2288 |
" / ", |
| 2289 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-red-700 dark:text-red-400", children: [ |
| 2290 |
c.syncs.failed_24h, |
| 2291 |
" fail" |
| 2292 |
] }) |
| 2293 |
] }) |
| 2294 |
] }), |
| 2295 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-xs", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2296 |
Badge, |
| 2297 |
{ |
| 2298 |
className: c.breaker.state === "closed" ? "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300" : "bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-300", |
| 2299 |
children: c.breaker.state |
| 2300 |
} |
| 2301 |
) }), |
| 2302 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-xs text-gray-600 dark:text-gray-300 max-w-sm", children: c.latest_failure ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ |
| 2303 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono", children: [ |
| 2304 |
c.latest_failure.operation, |
| 2305 |
c.latest_failure.http_status !== null ? ` (${c.latest_failure.http_status})` : "" |
| 2306 |
] }), |
| 2307 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-500 truncate", children: c.latest_failure.error_message }), |
| 2308 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-400 text-[10px] mt-0.5", children: c.latest_failure.occurred_at }) |
| 2309 |
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-400", children: "—" }) }), |
| 2310 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2", children: c.needs_attention ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300", children: [ |
| 2311 |
/* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "h-3 w-3 mr-1" }), |
| 2312 |
__("Needs attention", "yatra") |
| 2313 |
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300", children: [ |
| 2314 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-3 w-3 mr-1" }), |
| 2315 |
__("Healthy", "yatra") |
| 2316 |
] }) }) |
| 2317 |
] |
| 2318 |
}, |
| 2319 |
c.channel.id |
| 2320 |
)) }) |
| 2321 |
] }) }) }) |
| 2322 |
] }) |
| 2323 |
] }); |
| 2324 |
}; |
| 2325 |
const KpiCard = ({ label, value, icon, tone = "ok" }) => { |
| 2326 |
const ring = tone === "bad" ? "border-red-200 dark:border-red-800" : tone === "warn" ? "border-amber-200 dark:border-amber-800" : "border-gray-200 dark:border-gray-700"; |
| 2327 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `rounded-md border ${ring} bg-white dark:bg-gray-900 p-3`, children: [ |
| 2328 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400", children: [ |
| 2329 |
icon, |
| 2330 |
label |
| 2331 |
] }), |
| 2332 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-2xl font-semibold text-gray-900 dark:text-white", children: value }) |
| 2333 |
] }); |
| 2334 |
}; |
| 2335 |
const LatencySection = () => { |
| 2336 |
const { data, isLoading, refetch, isFetching } = useQuery({ |
| 2337 |
queryKey: ["cm-latency"], |
| 2338 |
queryFn: () => channelManagerApi.getLatency(), |
| 2339 |
refetchOnWindowFocus: false |
| 2340 |
}); |
| 2341 |
if (isLoading) { |
| 2342 |
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }); |
| 2343 |
} |
| 2344 |
const report = data == null ? void 0 : data.data; |
| 2345 |
if (!report) return null; |
| 2346 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ |
| 2347 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [ |
| 2348 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 2349 |
/* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Sync latency", "yatra") }), |
| 2350 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: [ |
| 2351 |
__( |
| 2352 |
"Percentiles over the duration_ms column of the sync log. Nearest-rank percentiles capped at ", |
| 2353 |
"yatra" |
| 2354 |
), |
| 2355 |
report.sample_cap, |
| 2356 |
" ", |
| 2357 |
__("recent samples per channel per window.", "yatra") |
| 2358 |
] }) |
| 2359 |
] }), |
| 2360 |
/* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 2361 |
Button, |
| 2362 |
{ |
| 2363 |
variant: "outline", |
| 2364 |
onClick: () => refetch(), |
| 2365 |
disabled: isFetching, |
| 2366 |
children: [ |
| 2367 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2368 |
RefreshCw, |
| 2369 |
{ |
| 2370 |
className: `h-4 w-4 mr-1 ${isFetching ? "animate-spin" : ""}` |
| 2371 |
} |
| 2372 |
), |
| 2373 |
__("Refresh", "yatra") |
| 2374 |
] |
| 2375 |
} |
| 2376 |
) |
| 2377 |
] }), |
| 2378 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2379 |
LatencyTable, |
| 2380 |
{ |
| 2381 |
title: __("Global", "yatra"), |
| 2382 |
rows: [ |
| 2383 |
{ |
| 2384 |
label: __("Last 24h", "yatra"), |
| 2385 |
window: report.global.last_24h |
| 2386 |
}, |
| 2387 |
{ |
| 2388 |
label: __("Last 7d", "yatra"), |
| 2389 |
window: report.global.last_7d |
| 2390 |
} |
| 2391 |
] |
| 2392 |
} |
| 2393 |
), |
| 2394 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2395 |
LatencyTable, |
| 2396 |
{ |
| 2397 |
title: __("Per provider", "yatra"), |
| 2398 |
rows: report.per_provider.flatMap((p) => [ |
| 2399 |
{ |
| 2400 |
label: `${p.provider} · ${__("24h", "yatra")}`, |
| 2401 |
window: p.last_24h |
| 2402 |
}, |
| 2403 |
{ label: `${p.provider} · ${__("7d", "yatra")}`, window: p.last_7d } |
| 2404 |
]) |
| 2405 |
} |
| 2406 |
), |
| 2407 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2408 |
LatencyTable, |
| 2409 |
{ |
| 2410 |
title: __("Per channel", "yatra"), |
| 2411 |
rows: report.per_channel.flatMap((c) => [ |
| 2412 |
{ |
| 2413 |
label: `${c.channel.display_name || `#${c.channel.id}`} · ${__("24h", "yatra")}`, |
| 2414 |
window: c.last_24h |
| 2415 |
}, |
| 2416 |
{ |
| 2417 |
label: `${c.channel.display_name || `#${c.channel.id}`} · ${__("7d", "yatra")}`, |
| 2418 |
window: c.last_7d |
| 2419 |
} |
| 2420 |
]) |
| 2421 |
} |
| 2422 |
) |
| 2423 |
] }); |
| 2424 |
}; |
| 2425 |
const LatencyTable = ({ title, rows }) => { |
| 2426 |
if (rows.length === 0) return null; |
| 2427 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [ |
| 2428 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-base", children: title }) }), |
| 2429 |
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "min-w-full text-sm", children: [ |
| 2430 |
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 2431 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-300", children: __("Scope", "yatra") }), |
| 2432 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-right font-medium text-gray-700 dark:text-gray-300", children: __("Samples", "yatra") }), |
| 2433 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-right font-medium text-gray-700 dark:text-gray-300", children: __("Avg", "yatra") }), |
| 2434 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-right font-medium text-gray-700 dark:text-gray-300", children: "p50" }), |
| 2435 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-right font-medium text-gray-700 dark:text-gray-300", children: "p95" }), |
| 2436 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-right font-medium text-gray-700 dark:text-gray-300", children: "p99" }), |
| 2437 |
/* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-2 text-right font-medium text-gray-700 dark:text-gray-300", children: __("OK / Fail", "yatra") }) |
| 2438 |
] }) }), |
| 2439 |
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: rows.map((r, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 2440 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-gray-900 dark:text-white", children: r.label }), |
| 2441 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: r.window.samples }), |
| 2442 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: fmtMs(r.window.avg_ms) }), |
| 2443 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: fmtMs(r.window.p50_ms) }), |
| 2444 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: fmtMs(r.window.p95_ms) }), |
| 2445 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: fmtMs(r.window.p99_ms) }), |
| 2446 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2 text-right text-xs", children: [ |
| 2447 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-green-700 dark:text-green-400", children: r.window.success_count }), |
| 2448 |
" / ", |
| 2449 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-700 dark:text-red-400", children: r.window.fail_count }) |
| 2450 |
] }) |
| 2451 |
] }, `${r.label}-${idx}`)) }) |
| 2452 |
] }) }) }) |
| 2453 |
] }); |
| 2454 |
}; |
| 2455 |
function fmtMs(v) { |
| 2456 |
if (v === null || v === void 0) return "—"; |
| 2457 |
if (v >= 1e3) return `${(v / 1e3).toFixed(2)}s`; |
| 2458 |
return `${v} ms`; |
| 2459 |
} |
| 2460 |
const LogsSection = () => { |
| 2461 |
const { data, isLoading } = useQuery({ |
| 2462 |
queryKey: ["channel-manager-logs"], |
| 2463 |
queryFn: () => channelManagerApi.listLogs({ per_page: 200 }), |
| 2464 |
refetchInterval: 15e3 |
| 2465 |
}); |
| 2466 |
if (isLoading) { |
| 2467 |
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }); |
| 2468 |
} |
| 2469 |
const rows = (data == null ? void 0 : data.data) ?? []; |
| 2470 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ |
| 2471 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ |
| 2472 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2", children: [ |
| 2473 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "h-5 w-5 text-blue-500" }), |
| 2474 |
__("Sync activity", "yatra") |
| 2475 |
] }), |
| 2476 |
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-3xl", children: __( |
| 2477 |
"Every push (you → channel), pull (channel → you), and inbound webhook from the last 90 days. Filter by failed status when debugging a sync; the error column carries the exact response from the OTA's API.", |
| 2478 |
"yatra" |
| 2479 |
) }), |
| 2480 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-2 flex items-start gap-1.5", children: [ |
| 2481 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0" }), |
| 2482 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: __( |
| 2483 |
"Auto-refreshes every 15 seconds while open. Rows older than 90 days are purged automatically by a daily cron.", |
| 2484 |
"yatra" |
| 2485 |
) }) |
| 2486 |
] }) |
| 2487 |
] }), |
| 2488 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0", children: rows.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-8 text-center text-gray-500 dark:text-gray-400", children: __( |
| 2489 |
"No activity yet — enable a channel and create a mapping to start seeing events here.", |
| 2490 |
"yatra" |
| 2491 |
) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full", children: [ |
| 2492 |
/* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ |
| 2493 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("When", "yatra") }), |
| 2494 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Channel", "yatra") }), |
| 2495 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Operation", "yatra") }), |
| 2496 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Status", "yatra") }), |
| 2497 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Duration", "yatra") }), |
| 2498 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Th, { children: __("Summary / Error", "yatra") }) |
| 2499 |
] }) }), |
| 2500 |
/* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: rows.map((row) => /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 2501 |
"tr", |
| 2502 |
{ |
| 2503 |
className: "hover:bg-gray-50 dark:hover:bg-gray-800/50", |
| 2504 |
children: [ |
| 2505 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: new Date(row.created_at).toLocaleString() }), |
| 2506 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-sm", children: row.channel_id ? `#${row.channel_id}` : "—" }), |
| 2507 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400", children: [ |
| 2508 |
row.direction, |
| 2509 |
"/", |
| 2510 |
row.operation |
| 2511 |
] }) }), |
| 2512 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2513 |
Badge, |
| 2514 |
{ |
| 2515 |
className: row.status === "success" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", |
| 2516 |
children: row.status |
| 2517 |
} |
| 2518 |
) }), |
| 2519 |
/* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-3 text-xs text-gray-500 dark:text-gray-400", children: row.duration_ms != null ? `${row.duration_ms}ms` : "—" }), |
| 2520 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-4 py-3 text-xs text-gray-700 dark:text-gray-200 max-w-md", children: [ |
| 2521 |
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "truncate", children: row.error_message || row.summary || "—" }), |
| 2522 |
row.http_status != null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-0.5 text-[10px] text-gray-400", children: [ |
| 2523 |
"HTTP ", |
| 2524 |
row.http_status |
| 2525 |
] }) |
| 2526 |
] }) |
| 2527 |
] |
| 2528 |
}, |
| 2529 |
row.id |
| 2530 |
)) }) |
| 2531 |
] }) }) }) }) |
| 2532 |
] }); |
| 2533 |
}; |
| 2534 |
const FormField = ({ label, description, children }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1.5", children: [ |
| 2535 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: label }), |
| 2536 |
children, |
| 2537 |
description && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs leading-relaxed text-gray-500 dark:text-gray-400 flex items-start gap-1.5", children: [ |
| 2538 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "h-3.5 w-3.5 mt-0.5 flex-shrink-0 text-gray-400" }), |
| 2539 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: description }) |
| 2540 |
] }) |
| 2541 |
] }); |
| 2542 |
const ToggleRow = ({ label, description, checked, onChange, tone = "default" }) => { |
| 2543 |
const baseBg = tone === "amber" && checked ? "border-amber-300 bg-amber-50/60 dark:border-amber-700 dark:bg-amber-950/30" : "border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"; |
| 2544 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs( |
| 2545 |
"label", |
| 2546 |
{ |
| 2547 |
className: `flex items-start gap-3 cursor-pointer rounded-md border px-3 py-2.5 transition-colors ${baseBg}`, |
| 2548 |
children: [ |
| 2549 |
/* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2550 |
"input", |
| 2551 |
{ |
| 2552 |
type: "checkbox", |
| 2553 |
checked, |
| 2554 |
onChange: (e) => onChange(e.target.checked), |
| 2555 |
className: "mt-0.5" |
| 2556 |
} |
| 2557 |
), |
| 2558 |
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex-1", children: [ |
| 2559 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-sm font-medium text-gray-900 dark:text-white", children: label }), |
| 2560 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: description }) |
| 2561 |
] }) |
| 2562 |
] |
| 2563 |
} |
| 2564 |
); |
| 2565 |
}; |
| 2566 |
const StatusLegend = ({ color, label, description }) => { |
| 2567 |
const cls = color === "green" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : color === "red" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"; |
| 2568 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5", children: [ |
| 2569 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: cls, children: label }), |
| 2570 |
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-500 dark:text-gray-400", children: description }) |
| 2571 |
] }); |
| 2572 |
}; |
| 2573 |
const Th = ({ children, align = "left" }) => /* @__PURE__ */ jsxRuntimeExports.jsx( |
| 2574 |
"th", |
| 2575 |
{ |
| 2576 |
className: `px-4 py-3 text-${align} text-xs font-medium text-gray-500 dark:text-gray-400 uppercase`, |
| 2577 |
children |
| 2578 |
} |
| 2579 |
); |
| 2580 |
const HealthBadge = ({ health }) => { |
| 2581 |
if (!health) { |
| 2582 |
return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: "—" }); |
| 2583 |
} |
| 2584 |
const { status, breaker, recent } = health; |
| 2585 |
const cls = status === "green" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : status === "amber" ? "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" : "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"; |
| 2586 |
const breakerLabel = breaker.state === "open" ? __("circuit open", "yatra") : breaker.state === "half_open" ? __("recovering", "yatra") : ""; |
| 2587 |
const tooltip = [ |
| 2588 |
recent.total > 0 ? __("Last {total}: {ok} OK, {fail} failed.", "yatra").replace("{total}", String(recent.total)).replace("{ok}", String(recent.ok)).replace("{fail}", String(recent.fail)) : __("No recent activity.", "yatra"), |
| 2589 |
breaker.state !== "closed" ? __("Circuit: {state}.", "yatra").replace("{state}", breaker.state) + (breaker.last_failure ? ` — ${breaker.last_failure}` : "") : "" |
| 2590 |
].filter(Boolean).join(" "); |
| 2591 |
const label = status === "green" ? __("Healthy", "yatra") : status === "amber" ? __("Degraded", "yatra") : __("Failing", "yatra"); |
| 2592 |
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1.5", title: tooltip, children: [ |
| 2593 |
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: cls, children: [ |
| 2594 |
/* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "mr-1 h-3 w-3" }), |
| 2595 |
label |
| 2596 |
] }), |
| 2597 |
breakerLabel && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-red-600 dark:text-red-400", children: breakerLabel }) |
| 2598 |
] }); |
| 2599 |
}; |
| 2600 |
function extractError(e) { |
| 2601 |
var _a; |
| 2602 |
if (!e) return "Request failed."; |
| 2603 |
const data = ((_a = e == null ? void 0 : e.response) == null ? void 0 : _a.data) ?? (e == null ? void 0 : e.data) ?? null; |
| 2604 |
if (data && typeof data === "object" && typeof data.message === "string") { |
| 2605 |
return data.message; |
| 2606 |
} |
| 2607 |
return (e == null ? void 0 : e.message) || "Request failed."; |
| 2608 |
} |
| 2609 |
export { |
| 2610 |
ChannelManager as default |
| 2611 |
}; |
| 2612 |
//# sourceMappingURL=ChannelManager-DZ64KNZZ.js.map |
| 2613 |
|