PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / assets / dist / js / ChannelManager-DZ64KNZZ.js

ChannelManager-DZ64KNZZ.js in Yatra – Travel Booking & Tour Operator Software 3.0.16, at assets/dist/js/ChannelManager-DZ64KNZZ.js

2,613 lines 133.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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