PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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 / resources / js / pages / Reports.tsx

Reports.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.6, at resources/js/pages/Reports.tsx

3,107 lines 126.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * YATRA TRAVEL BOOKING REPORTS
3 * Essential reports for travel booking businesses
4 * Based on deep understanding of Yatra business model
5 */
6
7 import React, { useState, useMemo } from "react";
8 import { useQuery } from "@tanstack/react-query";
9 import { __ } from "../lib/i18n";
10 import { apiClient, apiService } from "../lib/api-client";
11 import { unwrapApiPayload } from "../lib/unwrap-api-payload";
12 import { useToast } from "../components/ui/toast";
13 import {
14 Card,
15 CardContent,
16 CardHeader,
17 CardTitle,
18 CardDescription,
19 } from "../components/ui/card";
20 import { Button } from "../components/ui/button";
21 import { Select } from "../components/ui/select";
22 import BookingsOverviewChart from "../components/charts/BookingsOverviewChart";
23 import BookingStatusChart from "../components/charts/BookingStatusChart";
24 import { formatYatraMoney } from "../lib/currency-display";
25 import {
26 bucketSeries,
27 bucketStatusSeries,
28 buildCsv,
29 csvFilename,
30 downloadCsv,
31 type SeriesPoint,
32 type StatusPoint,
33 type TrendView,
34 } from "../lib/report-series";
35 import { isModuleActive, isProPluginActive } from "../lib/plugin-utils";
36 import { canCap } from "../hooks/useCapabilities";
37
38 // Skeleton Loading Components
39 const SkeletonCard = () => (
40 <Card className="p-4">
41 <div className="flex items-center justify-between">
42 <div className="flex-1">
43 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2"></div>
44 <div className="h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-20"></div>
45 </div>
46 <div className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse"></div>
47 </div>
48 </Card>
49 );
50
51 const SkeletonReportSection = () => (
52 <div className="space-y-4">
53 <div className="h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-48"></div>
54 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
55 {[...Array(4)].map((_, i) => (
56 <div key={i} className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
57 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2"></div>
58 <div className="h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-16"></div>
59 </div>
60 ))}
61 </div>
62 </div>
63 );
64
65 // SVG Icons for Travel Reports
66 const SVGIcons = {
67 Calendar: () => (
68 <svg
69 className="w-6 h-6"
70 fill="none"
71 stroke="currentColor"
72 viewBox="0 0 24 24"
73 >
74 <path
75 strokeLinecap="round"
76 strokeLinejoin="round"
77 strokeWidth={2}
78 d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
79 />
80 </svg>
81 ),
82 DollarSign: () => (
83 <svg
84 className="w-6 h-6"
85 fill="none"
86 stroke="currentColor"
87 viewBox="0 0 24 24"
88 >
89 <path
90 strokeLinecap="round"
91 strokeLinejoin="round"
92 strokeWidth={2}
93 d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
94 />
95 </svg>
96 ),
97 MapPin: () => (
98 <svg
99 className="w-6 h-6"
100 fill="none"
101 stroke="currentColor"
102 viewBox="0 0 24 24"
103 >
104 <path
105 strokeLinecap="round"
106 strokeLinejoin="round"
107 strokeWidth={2}
108 d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
109 />
110 <path
111 strokeLinecap="round"
112 strokeLinejoin="round"
113 strokeWidth={2}
114 d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
115 />
116 </svg>
117 ),
118 Truck: () => (
119 <svg
120 className="w-6 h-6"
121 fill="none"
122 stroke="currentColor"
123 viewBox="0 0 24 24"
124 >
125 <path
126 strokeLinecap="round"
127 strokeLinejoin="round"
128 strokeWidth={2}
129 d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM21 17a2 2 0 11-4 0 2 2 0 014 0z"
130 />
131 <path
132 strokeLinecap="round"
133 strokeLinejoin="round"
134 strokeWidth={2}
135 d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"
136 />
137 </svg>
138 ),
139 Users: () => (
140 <svg
141 className="w-6 h-6"
142 fill="none"
143 stroke="currentColor"
144 viewBox="0 0 24 24"
145 >
146 <path
147 strokeLinecap="round"
148 strokeLinejoin="round"
149 strokeWidth={2}
150 d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a4 4 0 11-8 0 4 4 0 018 0z"
151 />
152 </svg>
153 ),
154 Activity: () => (
155 <svg
156 className="w-6 h-6"
157 fill="none"
158 stroke="currentColor"
159 viewBox="0 0 24 24"
160 >
161 <path
162 strokeLinecap="round"
163 strokeLinejoin="round"
164 strokeWidth={2}
165 d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
166 />
167 </svg>
168 ),
169 Facebook: () => (
170 <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
171 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
172 </svg>
173 ),
174 Google: () => (
175 <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
176 <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
177 <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
178 <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
179 <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
180 </svg>
181 ),
182 Target: () => (
183 <svg
184 className="w-6 h-6"
185 fill="none"
186 stroke="currentColor"
187 viewBox="0 0 24 24"
188 >
189 <path
190 strokeLinecap="round"
191 strokeLinejoin="round"
192 strokeWidth={2}
193 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
194 />
195 </svg>
196 ),
197 BarChart: () => (
198 <svg
199 className="w-6 h-6"
200 fill="none"
201 stroke="currentColor"
202 viewBox="0 0 24 24"
203 >
204 <path
205 strokeLinecap="round"
206 strokeLinejoin="round"
207 strokeWidth={2}
208 d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
209 />
210 </svg>
211 ),
212 XCircle: () => (
213 <svg
214 className="w-6 h-6"
215 fill="none"
216 stroke="currentColor"
217 viewBox="0 0 24 24"
218 >
219 <path
220 strokeLinecap="round"
221 strokeLinejoin="round"
222 strokeWidth={2}
223 d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
224 />
225 </svg>
226 ),
227 };
228
229 // Travel Business Report Categories
230 // Each category declares the capability that gates it. The Reports
231 // page filters this list by `usePermissions().can()` at render time
232 // so an Accountant only sees Revenue + Operational tabs, a Marketing
233 // role sees Customer + Operational + Pixel + GA, etc. Categories
234 // without a `cap` field are visible to anyone who can see Reports
235 // (the page itself is already gated at the sidebar layer).
236 const TravelReportCategories: Array<{
237 id: string;
238 title: string;
239 icon: string;
240 description: string;
241 cap: string;
242 }> = [
243 {
244 id: "booking-overview",
245 title: "Booking Overview",
246 icon: "Calendar",
247 description: "Booking volume, status distribution, trends",
248 cap: "yatra_view_operational_reports",
249 },
250 {
251 id: "revenue-analysis",
252 title: "Revenue Analysis",
253 icon: "DollarSign",
254 description: "Revenue trends, payment status, profitability",
255 cap: "yatra_view_financial_reports",
256 },
257 {
258 id: "trip-performance",
259 title: "Trip Performance",
260 icon: "MapPin",
261 description: "Trip popularity, occupancy rates, capacity utilization",
262 cap: "yatra_view_operational_reports",
263 },
264 {
265 id: "departure-management",
266 title: "Departure Management",
267 icon: "Truck",
268 description: "Upcoming departures, capacity planning, scheduling",
269 cap: "yatra_view_departures",
270 },
271 {
272 id: "customer-insights",
273 title: "Customer Insights",
274 icon: "Users",
275 description: "Customer behavior, retention, demographics",
276 cap: "yatra_view_customers",
277 },
278 {
279 id: "operational-metrics",
280 title: "Operational Metrics",
281 icon: "Activity",
282 description: "Lead times, cancellations, efficiency metrics",
283 cap: "yatra_view_operational_reports",
284 },
285 {
286 id: "facebook-pixel",
287 title: "Facebook Pixel",
288 icon: "Facebook",
289 description: "Conversion tracking, event analytics, pixel performance",
290 cap: "yatra_view_operational_reports",
291 },
292 {
293 id: "google-analytics",
294 title: "Google Analytics 4",
295 icon: "Google",
296 description:
297 "Enhanced e-commerce tracking, Measurement Protocol, visitor analytics",
298 cap: "yatra_view_operational_reports",
299 },
300 ];
301
302 // Detailed Breakdown Chart Component
303 //
304 // REWRITE NOTE (3.0.5):
305 // The previous implementation fabricated daily/weekly/monthly values by
306 // multiplying period totals by a `seasonalFactor` so the chart "looked
307 // like" historical data. Operators were making decisions on numbers
308 // that didn't exist in the database. This version reads day-level
309 // trends straight from `/reports` (booking_trend / revenue_trend /
310 // occupancy_trend) and buckets them via `report-series.bucketSeries`
311 // when the operator picks Weekly or Monthly. The numbers always come
312 // from real bookings.
313 const DetailedBreakdownChart: React.FC<{
314 viewType: string;
315 dateRange: string;
316 selectedCategory: string;
317 reportData?: any;
318 }> = ({ viewType, selectedCategory, reportData }) => {
319 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
320 const formatCurrencyAmount = (amount: number) =>
321 formatYatraMoney(Number(amount) || 0, globalCurrency, {
322 zeroAsUnknown: false,
323 });
324
325 // Pick the right source series for the selected report category.
326 // Each is already day-aligned and gap-filled by the backend.
327 const sourceSeries: SeriesPoint[] = useMemo(() => {
328 if (!reportData) return [];
329 if (selectedCategory === "revenue-analysis") {
330 return (reportData.revenue_trend as SeriesPoint[]) || [];
331 }
332 if (selectedCategory === "departure-management") {
333 // occupancy_trend stores rate-per-day (already aggregated). For a
334 // bar chart we want a comparable absolute (counts of departures),
335 // so we derive from departures_table when present. Fall back to
336 // booking_trend so the chart never goes empty.
337 const dep = reportData.departures_table || [];
338 if (Array.isArray(dep) && dep.length > 0) {
339 const byDay = new Map<string, number>();
340 for (const d of dep) {
341 const date = (d?.date || "").slice(0, 10);
342 if (!date) continue;
343 byDay.set(date, (byDay.get(date) || 0) + 1);
344 }
345 return Array.from(byDay.entries()).map(([date, value]) => ({
346 date,
347 label: date,
348 value,
349 }));
350 }
351 return (reportData.booking_trend as SeriesPoint[]) || [];
352 }
353 return (reportData.booking_trend as SeriesPoint[]) || [];
354 }, [reportData, selectedCategory]);
355
356 // viewType can be "summary" (a synthetic UI option that maps to
357 // "daily" for the chart) or any TrendView. Coerce defensively.
358 const view: TrendView =
359 viewType === "weekly" || viewType === "monthly" ? viewType : "daily";
360 const bucketed = useMemo(
361 () => bucketSeries(sourceSeries, view),
362 [sourceSeries, view],
363 );
364
365 // Empty-state guard: the chart formerly fell back to fabricated
366 // values; now we render an honest "no data" instead. Better the
367 // operator sees the truth than a synthetic 12-bar chart.
368 if (!reportData || bucketed.length === 0) {
369 return (
370 <div className="py-10 text-center text-sm text-gray-500 dark:text-gray-400">
371 {__("No data in this period yet.", "yatra")}
372 </div>
373 );
374 }
375
376 // Find max for bar-width normalisation. Guard against zero.
377 const maxValue = Math.max(1, ...bucketed.map((p) => p.value));
378 const isRevenue = selectedCategory === "revenue-analysis";
379 const isDepartures = selectedCategory === "departure-management";
380
381 const barClass = isRevenue
382 ? "bg-emerald-500"
383 : isDepartures
384 ? "bg-purple-500"
385 : "bg-blue-500";
386
387 const valueLabel = (v: number) =>
388 isRevenue ? formatCurrencyAmount(v) : v.toLocaleString();
389
390 return (
391 <div className="space-y-4">
392 <div className="text-center">
393 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
394 {isRevenue
395 ? __("Revenue Trend", "yatra")
396 : isDepartures
397 ? __("Departures Trend", "yatra")
398 : __("Bookings Trend", "yatra")}
399 </h4>
400 </div>
401
402 {/* Bucketed bar chart — values are real data straight from the
403 backend trend arrays. Bar widths normalised against the max
404 in the bucketed set so a single outlier doesn't squash the
405 rest into invisibility. */}
406 <div className="space-y-2">
407 {bucketed.map((item, index) => (
408 <div
409 key={`${item.date}-${index}`}
410 className="flex items-center gap-2"
411 >
412 <div className="w-20 shrink-0 text-right text-xs text-gray-600 dark:text-gray-400">
413 {item.label}
414 </div>
415 <div className="relative h-4 flex-1 rounded-full bg-gray-100 dark:bg-gray-700">
416 <div
417 className={`flex h-4 items-center justify-end rounded-full pr-1.5 ${barClass}`}
418 style={{
419 width: `${Math.max(2, (item.value / maxValue) * 100)}%`,
420 }}
421 >
422 <span className="text-[11px] font-medium text-white">
423 {valueLabel(item.value)}
424 </span>
425 </div>
426 </div>
427 </div>
428 ))}
429 </div>
430
431 <div className="flex items-center gap-1 text-xs">
432 <div className={`h-3 w-3 rounded ${barClass}`}></div>
433 <span className="text-gray-600 dark:text-gray-400">
434 {isRevenue
435 ? __("Revenue", "yatra")
436 : isDepartures
437 ? __("Departures", "yatra")
438 : __("Bookings", "yatra")}
439 </span>
440 </div>
441 </div>
442 );
443 };
444
445 // Detailed Breakdown Table Component
446 //
447 // REWRITE NOTE (3.0.5):
448 // Replaces the previous synthetic data path that:
449 // - Fabricated daily/weekly/monthly bookings + revenue from period
450 // totals * a `seasonalFactor`
451 // - Made up confirmed/pending/cancelled splits (80/15/5 fixed ratio)
452 // - Made up customer satisfaction (90 * factor), efficiency (85 *
453 // factor), lead time (7..14 * inverse-factor)
454 // - Cycled "topTrip" through trip_performance[i % 3] regardless of
455 // which period the row represented
456 //
457 // New version reads four trend arrays from the backend, all day-aligned:
458 // - booking_trend : { date, label, value } total bookings/day
459 // - revenue_trend : { date, label, value } revenue/day
460 // - status_trend : { date, label, confirmed, pending, cancelled,
461 // completed } per-day status split
462 // - traveler_segments.trend : { date, label, value } travellers/day
463 //
464 // We bucket those into the operator's selected view (daily/weekly/
465 // monthly) using report-series.bucketSeries / bucketStatusSeries and
466 // render only columns we can stand behind. Columns whose source data
467 // doesn't exist (customer satisfaction, efficiency, top-trip-per-
468 // period) are removed — better than rendering invented numbers.
469 const DetailedBreakdownTable: React.FC<{
470 viewType: string;
471 dateRange: string;
472 selectedCategory: string;
473 reportData: any;
474 }> = ({ viewType, selectedCategory, reportData }) => {
475 // Suppress lints — these props are intentionally received for API
476 // stability with the parent but the view bucket pipeline doesn't
477 // need dateRange directly; backend already handed us the slice.
478 // (Recovery of formatCurrencyAmount happens later in the component.)
479 // viewType can be "summary" (UI-only) or a real TrendView. Coerce
480 // anything that's not weekly/monthly to "daily" — same rule the
481 // chart uses, so chart + table always agree on bucketing.
482 const effectiveView: TrendView =
483 viewType === "weekly" || viewType === "monthly" ? viewType : "daily";
484
485 const bookingTrend: SeriesPoint[] =
486 (reportData?.booking_trend as SeriesPoint[]) || [];
487 const revenueTrend: SeriesPoint[] =
488 (reportData?.revenue_trend as SeriesPoint[]) || [];
489 const statusTrend: StatusPoint[] =
490 (reportData?.status_trend as StatusPoint[]) || [];
491 const occupancyTrend: SeriesPoint[] =
492 (reportData?.occupancy_trend as SeriesPoint[]) || [];
493
494 // Bucket each series identically so rows in the same bucket line up.
495 const bookingsBucketed = bucketSeries(bookingTrend, effectiveView);
496 const revenueBucketed = bucketSeries(revenueTrend, effectiveView);
497 const statusBucketed = bucketStatusSeries(statusTrend, effectiveView);
498
499 // For occupancy per period: we have a per-day rate (%), and rates
500 // don't sum across days — they average. Recompute properly using
501 // departures_table when bucketing.
502 const departuresTable = (reportData?.departures_table as any[]) || [];
503 type OccupancyAgg = { booked: number; capacity: number };
504 const occByDay = new Map<string, OccupancyAgg>();
505 for (const d of departuresTable) {
506 const date = (d?.date || "").slice(0, 10);
507 if (!date) continue;
508 const acc = occByDay.get(date) || { booked: 0, capacity: 0 };
509 acc.booked += Number(d?.bookedSeats ?? d?.booked ?? 0);
510 acc.capacity += Number(d?.maxSeats ?? d?.capacity ?? 0);
511 occByDay.set(date, acc);
512 }
513 // Same bucket-key strategy used by bucketSeries — re-derive labels.
514 const occBookingSeries: SeriesPoint[] = Array.from(occByDay.entries()).map(
515 ([date, agg]) => ({ date, label: date, value: agg.booked }),
516 );
517 const occCapacitySeries: SeriesPoint[] = Array.from(occByDay.entries()).map(
518 ([date, agg]) => ({ date, label: date, value: agg.capacity }),
519 );
520 const occBookedBucketed = bucketSeries(occBookingSeries, effectiveView);
521 const occCapacityBucketed = bucketSeries(occCapacitySeries, effectiveView);
522
523 // Build the unified row set the table iterates. Each row carries
524 // every field a category column might want; unsupported fields stay
525 // null so the renderer can show "—" instead of a fabricated number.
526 type Row = {
527 period: string;
528 fullDate: string;
529 // Booking columns
530 bookings: number;
531 confirmed: number;
532 pending: number;
533 cancelled: number;
534 // Revenue
535 revenue: number;
536 avgBookingValue: number;
537 // Trip performance
538 occupancyRate: number;
539 booked: number;
540 capacity: number;
541 // Departures
542 departures: number;
543 };
544 const breakdownData: Row[] = bookingsBucketed.map((bRow, i) => {
545 const rev = revenueBucketed[i]?.value ?? 0;
546 const status = statusBucketed[i];
547 const ob = occBookedBucketed.find((p) => p.label === bRow.label);
548 const oc = occCapacityBucketed.find((p) => p.label === bRow.label);
549 const bookedSeats = ob?.value ?? 0;
550 const capacitySeats = oc?.value ?? 0;
551 const occRate =
552 capacitySeats > 0
553 ? Math.round((bookedSeats / capacitySeats) * 1000) / 10
554 : 0;
555 return {
556 period: bRow.label,
557 fullDate: bRow.date,
558 bookings: bRow.value,
559 confirmed: status?.confirmed ?? 0,
560 pending: status?.pending ?? 0,
561 cancelled: status?.cancelled ?? 0,
562 revenue: rev,
563 avgBookingValue: bRow.value > 0 ? Math.round(rev / bRow.value) : 0,
564 occupancyRate: occRate,
565 booked: bookedSeats,
566 capacity: capacitySeats,
567 departures: capacitySeats > 0 ? 1 : 0, // count via departures_table when available
568 };
569 });
570
571 // Restore departures count if departures_table has them by bucket.
572 if (departuresTable.length > 0) {
573 const depCountSeries: SeriesPoint[] = (() => {
574 const m = new Map<string, number>();
575 for (const d of departuresTable) {
576 const date = (d?.date || "").slice(0, 10);
577 if (!date) continue;
578 m.set(date, (m.get(date) || 0) + 1);
579 }
580 return Array.from(m.entries()).map(([date, value]) => ({
581 date,
582 label: date,
583 value,
584 }));
585 })();
586 const depBucketed = bucketSeries(depCountSeries, effectiveView);
587 breakdownData.forEach((row) => {
588 const hit = depBucketed.find((p) => p.label === row.period);
589 row.departures = hit?.value ?? 0;
590 });
591 }
592
593 // Suppress legacy unused references to avoid TS dead-code warnings
594 // until the rest of the table renderer is also pruned.
595 void occupancyTrend;
596
597 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
598 const formatCurrencyAmount = (amount: number) =>
599 formatYatraMoney(Number(amount) || 0, globalCurrency, {
600 zeroAsUnknown: false,
601 });
602
603 // Render different table headers and columns based on category
604 const renderTableHeaders = () => {
605 switch (selectedCategory) {
606 case "booking-overview":
607 return (
608 <tr>
609 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
610 {__("Period", "yatra")}
611 </th>
612 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
613 {__("Total Bookings", "yatra")}
614 </th>
615 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
616 {__("Confirmed", "yatra")}
617 </th>
618 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
619 {__("Pending", "yatra")}
620 </th>
621 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
622 {__("Cancelled", "yatra")}
623 </th>
624 </tr>
625 );
626 case "revenue-analysis":
627 // Dropped "Collected" / "Outstanding" columns: we don't track
628 // per-period collected vs. outstanding at the trend level. The
629 // payment_status block in the Revenue Analysis section shows
630 // the period-aggregate paid/pending/refunded split — that's
631 // the right place for it.
632 return (
633 <tr>
634 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
635 {__("Period", "yatra")}
636 </th>
637 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
638 {__("Total Revenue", "yatra")}
639 </th>
640 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
641 {__("Bookings", "yatra")}
642 </th>
643 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
644 {__("Avg Booking Value", "yatra")}
645 </th>
646 </tr>
647 );
648 case "trip-performance":
649 // Dropped "Top Trip" column: top trip is a period-level concept,
650 // not a per-bucket one. The Top Trips card above the table
651 // shows the real ranking.
652 return (
653 <tr>
654 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
655 {__("Period", "yatra")}
656 </th>
657 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
658 {__("Trip Bookings", "yatra")}
659 </th>
660 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
661 {__("Occupancy Rate", "yatra")}
662 </th>
663 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
664 {__("Revenue", "yatra")}
665 </th>
666 </tr>
667 );
668 case "departure-management":
669 return (
670 <tr>
671 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
672 {__("Period", "yatra")}
673 </th>
674 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
675 {__("Departures", "yatra")}
676 </th>
677 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
678 {__("Total Capacity", "yatra")}
679 </th>
680 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
681 {__("Booked", "yatra")}
682 </th>
683 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
684 {__("Utilization", "yatra")}
685 </th>
686 </tr>
687 );
688 case "customer-insights":
689 return (
690 <tr>
691 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
692 {__("Period", "yatra")}
693 </th>
694 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
695 {__("New Customers", "yatra")}
696 </th>
697 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
698 {__("Returning Customers", "yatra")}
699 </th>
700 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
701 {__("Satisfaction", "yatra")}
702 </th>
703 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
704 {__("Total Revenue", "yatra")}
705 </th>
706 </tr>
707 );
708 case "operational-metrics":
709 return (
710 <tr>
711 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
712 {__("Period", "yatra")}
713 </th>
714 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
715 {__("Lead Time (days)", "yatra")}
716 </th>
717 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
718 {__("Cancellation Rate", "yatra")}
719 </th>
720 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
721 {__("Efficiency", "yatra")}
722 </th>
723 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
724 {__("Bookings", "yatra")}
725 </th>
726 </tr>
727 );
728 default:
729 return (
730 <tr>
731 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
732 {__("Period", "yatra")}
733 </th>
734 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
735 {__("Bookings", "yatra")}
736 </th>
737 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
738 {__("Revenue", "yatra")}
739 </th>
740 </tr>
741 );
742 }
743 };
744
745 const renderTableRows = () => {
746 return breakdownData.map((row, index) => (
747 <tr key={index} className="hover:bg-gray-50 dark:hover:bg-gray-800">
748 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
749 {row.period}
750 </td>
751 {selectedCategory === "booking-overview" && (
752 <>
753 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
754 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400">
755 {row.bookings}
756 </span>
757 </td>
758 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
759 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-400">
760 {row.confirmed}
761 </span>
762 </td>
763 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
764 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-400">
765 {row.pending}
766 </span>
767 </td>
768 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
769 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-400">
770 {row.cancelled}
771 </span>
772 </td>
773 </>
774 )}
775 {selectedCategory === "revenue-analysis" && (
776 <>
777 <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-green-600 dark:text-green-400">
778 {formatCurrencyAmount(row.revenue)}
779 </td>
780 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
781 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400">
782 {row.bookings}
783 </span>
784 </td>
785 <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-purple-600 dark:text-purple-400">
786 {formatCurrencyAmount(row.avgBookingValue)}
787 </td>
788 </>
789 )}
790 {selectedCategory === "trip-performance" && (
791 <>
792 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
793 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400">
794 {row.bookings}
795 </span>
796 </td>
797 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
798 <div className="flex items-center">
799 <div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 mr-2">
800 <div
801 className="bg-green-500 h-2 rounded-full"
802 style={{ width: `${row.occupancyRate}%` }}
803 ></div>
804 </div>
805 <span className="text-xs font-medium">
806 {row.occupancyRate}%
807 </span>
808 </div>
809 </td>
810 <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-green-600 dark:text-green-400">
811 {formatCurrencyAmount(row.revenue)}
812 </td>
813 </>
814 )}
815 {selectedCategory === "departure-management" && (
816 <>
817 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
818 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-400">
819 {row.departures}
820 </span>
821 </td>
822 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
823 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300">
824 {row.capacity}
825 </span>
826 </td>
827 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
828 <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400">
829 {row.booked}
830 </span>
831 </td>
832 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
833 <div className="flex items-center">
834 <div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 mr-2">
835 <div
836 className="bg-green-500 h-2 rounded-full"
837 style={{
838 width:
839 row.capacity > 0
840 ? `${Math.round((row.booked / row.capacity) * 100)}%`
841 : "0%",
842 }}
843 ></div>
844 </div>
845 <span className="text-xs font-medium">
846 {row.capacity > 0
847 ? Math.round((row.booked / row.capacity) * 100)
848 : 0}
849 %
850 </span>
851 </div>
852 </td>
853 </>
854 )}
855 </tr>
856 ));
857 };
858
859 // Customer-Insights + Operational-Metrics no longer ship a per-period
860 // breakdown table — we don't aggregate those metrics day-by-day yet,
861 // and the previous version filled them with synthetic numbers. Show
862 // a calm note pointing the operator at the summary cards above.
863 const categoriesWithoutBreakdown = new Set([
864 "customer-insights",
865 "operational-metrics",
866 ]);
867 if (categoriesWithoutBreakdown.has(selectedCategory)) {
868 return (
869 <div className="rounded-lg border border-dashed border-gray-300 dark:border-gray-700 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
870 {__(
871 "Per-period breakdown isn't available for this section. The summary cards above show the aggregate for the selected date range.",
872 "yatra",
873 )}
874 </div>
875 );
876 }
877
878 // Empty-state for ranges with no data — better than a 0-bar table.
879 if (breakdownData.length === 0) {
880 return (
881 <div className="rounded-lg border border-dashed border-gray-300 dark:border-gray-700 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
882 {__("No data in this date range yet.", "yatra")}
883 </div>
884 );
885 }
886
887 return (
888 <div className="overflow-x-auto">
889 <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
890 <thead className="bg-gray-50 dark:bg-gray-800">
891 {renderTableHeaders()}
892 </thead>
893 <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
894 {renderTableRows()}
895 </tbody>
896 </table>
897
898 {/* Summary Row */}
899 <div className="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
900 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
901 <div>
902 <p className="text-sm text-gray-600 dark:text-gray-400">
903 {__("Total Bookings", "yatra")}
904 </p>
905 <p className="text-lg font-bold text-blue-600 dark:text-blue-400">
906 {breakdownData.reduce((sum, row) => sum + row.bookings, 0)}
907 </p>
908 </div>
909 <div>
910 <p className="text-sm text-gray-600 dark:text-gray-400">
911 {__("Total Revenue", "yatra")}
912 </p>
913 <p className="text-lg font-bold text-green-600 dark:text-green-400">
914 {formatCurrencyAmount(
915 breakdownData.reduce((sum, row) => sum + row.revenue, 0),
916 )}
917 </p>
918 </div>
919 <div>
920 <p className="text-sm text-gray-600 dark:text-gray-400">
921 {__("Total Departures", "yatra")}
922 </p>
923 <p className="text-lg font-bold text-purple-600 dark:text-purple-400">
924 {breakdownData.reduce((sum, row) => sum + row.departures, 0)}
925 </p>
926 </div>
927 <div>
928 <p className="text-sm text-gray-600 dark:text-gray-400">
929 {__("Avg Occupancy", "yatra")}
930 </p>
931 <p className="text-lg font-bold text-orange-600 dark:text-orange-400">
932 {(
933 breakdownData.reduce((sum, row) => sum + row.occupancyRate, 0) /
934 breakdownData.length
935 ).toFixed(1)}
936 %
937 </p>
938 </div>
939 </div>
940 </div>
941 </div>
942 );
943 };
944
945 // Facebook Pixel Reports Component
946 const FacebookPixelReports: React.FC = () => {
947 const [clearingLogs, setClearingLogs] = useState(false);
948 const { showToast } = useToast();
949
950 // Fetch fresh Facebook Pixel data
951 const {
952 data: freshPixelData,
953 refetch: refetchPixelData,
954 isLoading: isPixelLoading,
955 } = useQuery({
956 queryKey: ["facebook-pixel-status"],
957 queryFn: async () => {
958 const response = await apiService.getFacebookPixelSettings();
959 return response?.data || {};
960 },
961 refetchInterval: 30000, // Refresh every 30 seconds
962 });
963
964 // Use fresh data if available, fallback to cached data
965 const facebookPixelData =
966 freshPixelData || (window as any).yatraAdmin?.facebookPixel || {};
967
968 const getEventStats = () => {
969 const logs = facebookPixelData.eventLogs || [];
970 return {
971 success: logs.filter((log: any) => log.status === "success").length,
972 errors: logs.filter((log: any) => log.status === "error").length,
973 total: logs.length,
974 };
975 };
976
977 const getRecentEvents = () => {
978 const logs = facebookPixelData.eventLogs || [];
979 return logs.slice(-10).reverse(); // Show last 10 events, newest first
980 };
981
982 const clearPixelLogs = async () => {
983 setClearingLogs(true);
984 try {
985 const response = await apiService.clearFacebookPixelEventLogs();
986 if (response.success) {
987 showToast(__("Event logs cleared successfully.", "yatra"), "success");
988 // Refetch fresh data to update the UI
989 await refetchPixelData();
990 }
991 } catch (error: any) {
992 showToast(error.message || __("Failed to clear logs.", "yatra"), "error");
993 } finally {
994 setClearingLogs(false);
995 }
996 };
997
998 const eventStats = getEventStats();
999 const recentEvents = getRecentEvents();
1000
1001 // Empty-state gate.
1002 // Three conditions all funnel into the same "Not Configured" screen
1003 // because, from the operator's point of view, they look the same:
1004 // 1. Pro plugin isn't installed/activated at all
1005 // 2. Pro is installed but the Facebook Pixel module is toggled off
1006 // under Modules — pre-existing pixel_id stays in the DB and
1007 // would otherwise let this tab render with stale data
1008 // 3. Module is on but no Pixel ID has been saved yet
1009 const fbModuleActive =
1010 isProPluginActive() && isModuleActive("facebook_pixel");
1011 if (!fbModuleActive || !facebookPixelData.pixel_id) {
1012 return (
1013 <div className="text-center py-12">
1014 <div className="mx-auto w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center mb-4">
1015 <svg
1016 className="w-8 h-8 text-gray-400"
1017 fill="currentColor"
1018 viewBox="0 0 24 24"
1019 >
1020 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
1021 </svg>
1022 </div>
1023 <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
1024 {__("Facebook Pixel Not Configured", "yatra")}
1025 </h3>
1026 <p className="text-gray-600 dark:text-gray-400 mb-4">
1027 {!fbModuleActive
1028 ? __(
1029 "Enable the Facebook Pixel module under Modules and add your Pixel ID in Settings to start tracking conversion events.",
1030 "yatra",
1031 )
1032 : __(
1033 "Configure your Facebook Pixel in Settings to start tracking conversion events.",
1034 "yatra",
1035 )}
1036 </p>
1037 <a
1038 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=${
1039 !fbModuleActive ? "modules" : "settings#integration"
1040 }`}
1041 className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1042 >
1043 {!fbModuleActive
1044 ? __("Open Modules", "yatra")
1045 : __("Configure Facebook Pixel", "yatra")}
1046 </a>
1047 </div>
1048 );
1049 }
1050
1051 return (
1052 <div className="space-y-6">
1053 {/* Connection Status */}
1054 <div className="flex items-center justify-between mb-4">
1055 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1056 {__("Connection Status", "yatra")}
1057 </h3>
1058 <Button
1059 type="button"
1060 onClick={() => {
1061 refetchPixelData();
1062 showToast(__("Status refreshed successfully!", "yatra"), "success");
1063 }}
1064 variant="outline"
1065 size="sm"
1066 disabled={isPixelLoading}
1067 >
1068 {isPixelLoading ? (
1069 <>
1070 <svg
1071 className="animate-spin -ml-1 mr-2 h-4 w-4"
1072 fill="none"
1073 viewBox="0 0 24 24"
1074 >
1075 <circle
1076 className="opacity-25"
1077 cx="12"
1078 cy="12"
1079 r="10"
1080 stroke="currentColor"
1081 strokeWidth="4"
1082 ></circle>
1083 <path
1084 className="opacity-75"
1085 fill="currentColor"
1086 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
1087 ></path>
1088 </svg>
1089 {__("Refreshing...", "yatra")}
1090 </>
1091 ) : (
1092 <>
1093 <svg
1094 className="-ml-1 mr-2 h-4 w-4"
1095 fill="none"
1096 viewBox="0 0 24 24"
1097 stroke="currentColor"
1098 >
1099 <path
1100 strokeLinecap="round"
1101 strokeLinejoin="round"
1102 strokeWidth={2}
1103 d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
1104 />
1105 </svg>
1106 {__("Refresh Status", "yatra")}
1107 </>
1108 )}
1109 </Button>
1110 </div>
1111
1112 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1113 <div
1114 className={`p-4 rounded-lg border ${
1115 facebookPixelData.connectionStatus?.pixelConnected
1116 ? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
1117 : "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
1118 }`}
1119 >
1120 <div className="flex items-center justify-between">
1121 <div>
1122 <p className="text-sm font-medium text-gray-900 dark:text-white">
1123 {__("Pixel Connection", "yatra")}
1124 </p>
1125 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
1126 {facebookPixelData.connectionStatus?.pixelConnected
1127 ? __("Connected", "yatra")
1128 : __("Not Connected", "yatra")}
1129 </p>
1130 </div>
1131 <div
1132 className={`w-8 h-8 rounded-full flex items-center justify-center ${
1133 facebookPixelData.connectionStatus?.pixelConnected
1134 ? "bg-green-100 dark:bg-green-900/30"
1135 : "bg-red-100 dark:bg-red-900/30"
1136 }`}
1137 >
1138 {facebookPixelData.connectionStatus?.pixelConnected ? (
1139 <svg
1140 className="w-4 h-4 text-green-600 dark:text-green-400"
1141 fill="none"
1142 stroke="currentColor"
1143 viewBox="0 0 24 24"
1144 >
1145 <path
1146 strokeLinecap="round"
1147 strokeLinejoin="round"
1148 strokeWidth={2}
1149 d="M5 13l4 4L19 7"
1150 />
1151 </svg>
1152 ) : (
1153 <svg
1154 className="w-4 h-4 text-red-600 dark:text-red-400"
1155 fill="none"
1156 stroke="currentColor"
1157 viewBox="0 0 24 24"
1158 >
1159 <path
1160 strokeLinecap="round"
1161 strokeLinejoin="round"
1162 strokeWidth={2}
1163 d="M6 18L18 6M6 6l12 12"
1164 />
1165 </svg>
1166 )}
1167 </div>
1168 </div>
1169 </div>
1170
1171 <div
1172 className={`p-4 rounded-lg border ${
1173 facebookPixelData.connectionStatus?.tokenConnected
1174 ? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
1175 : "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
1176 }`}
1177 >
1178 <div className="flex items-center justify-between">
1179 <div>
1180 <p className="text-sm font-medium text-gray-900 dark:text-white">
1181 {__("API Token", "yatra")}
1182 </p>
1183 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
1184 {facebookPixelData.connectionStatus?.tokenConnected
1185 ? __("Valid", "yatra")
1186 : __("Invalid", "yatra")}
1187 </p>
1188 </div>
1189 <div
1190 className={`w-8 h-8 rounded-full flex items-center justify-center ${
1191 facebookPixelData.connectionStatus?.tokenConnected
1192 ? "bg-green-100 dark:bg-green-900/30"
1193 : "bg-red-100 dark:bg-red-900/30"
1194 }`}
1195 >
1196 {facebookPixelData.connectionStatus?.tokenConnected ? (
1197 <svg
1198 className="w-4 h-4 text-green-600 dark:text-green-400"
1199 fill="none"
1200 stroke="currentColor"
1201 viewBox="0 0 24 24"
1202 >
1203 <path
1204 strokeLinecap="round"
1205 strokeLinejoin="round"
1206 strokeWidth={2}
1207 d="M5 13l4 4L19 7"
1208 />
1209 </svg>
1210 ) : (
1211 <svg
1212 className="w-4 h-4 text-red-600 dark:text-red-400"
1213 fill="none"
1214 stroke="currentColor"
1215 viewBox="0 0 24 24"
1216 >
1217 <path
1218 strokeLinecap="round"
1219 strokeLinejoin="round"
1220 strokeWidth={2}
1221 d="M6 18L18 6M6 6l12 12"
1222 />
1223 </svg>
1224 )}
1225 </div>
1226 </div>
1227 </div>
1228
1229 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
1230 <div className="flex items-center justify-between">
1231 <div>
1232 <p className="text-sm font-medium text-gray-900 dark:text-white">
1233 {__("Pixel ID", "yatra")}
1234 </p>
1235 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
1236 {facebookPixelData.pixel_id || __("Not Set", "yatra")}
1237 </p>
1238 </div>
1239 <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
1240 <svg
1241 className="w-4 h-4 text-blue-600 dark:text-blue-400"
1242 fill="currentColor"
1243 viewBox="0 0 24 24"
1244 >
1245 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
1246 </svg>
1247 </div>
1248 </div>
1249 </div>
1250 </div>
1251
1252 {/* Event Statistics */}
1253 <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
1254 <div className="p-6 border-b border-gray-200 dark:border-gray-700">
1255 <div className="flex items-center justify-between">
1256 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1257 {__("Event Statistics", "yatra")}
1258 </h3>
1259 <Button
1260 type="button"
1261 onClick={() => clearPixelLogs()}
1262 variant="outline"
1263 size="sm"
1264 disabled={clearingLogs}
1265 >
1266 {clearingLogs
1267 ? __("Clearing...", "yatra")
1268 : __("Clear Logs", "yatra")}
1269 </Button>
1270 </div>
1271 </div>
1272
1273 <div className="p-6">
1274 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
1275 <div className="text-center">
1276 <div className="text-3xl font-bold text-green-600 dark:text-green-400">
1277 {eventStats.success}
1278 </div>
1279 <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1280 {__("Successful Events", "yatra")}
1281 </div>
1282 </div>
1283 <div className="text-center">
1284 <div className="text-3xl font-bold text-red-600 dark:text-red-400">
1285 {eventStats.errors}
1286 </div>
1287 <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1288 {__("Failed Events", "yatra")}
1289 </div>
1290 </div>
1291 <div className="text-center">
1292 <div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
1293 {eventStats.total}
1294 </div>
1295 <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1296 {__("Total Events", "yatra")}
1297 </div>
1298 </div>
1299 </div>
1300 </div>
1301 </div>
1302
1303 {/* Recent Events */}
1304 <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
1305 <div className="p-6 border-b border-gray-200 dark:border-gray-700">
1306 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1307 {__("Recent Activity", "yatra")}
1308 </h3>
1309 </div>
1310
1311 <div className="p-6">
1312 {recentEvents.length > 0 ? (
1313 <div className="space-y-3">
1314 {recentEvents.map((log: any, index: number) => (
1315 <div
1316 key={index}
1317 className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
1318 >
1319 <div className="flex items-center gap-3">
1320 <div
1321 className={`w-8 h-8 rounded-full flex items-center justify-center ${
1322 log.status === "success"
1323 ? "bg-green-100 dark:bg-green-900/30"
1324 : log.status === "error"
1325 ? "bg-red-100 dark:bg-red-900/30"
1326 : "bg-blue-100 dark:bg-blue-900/30"
1327 }`}
1328 >
1329 {log.status === "success" && (
1330 <svg
1331 className="w-4 h-4 text-green-600 dark:text-green-400"
1332 fill="none"
1333 stroke="currentColor"
1334 viewBox="0 0 24 24"
1335 >
1336 <path
1337 strokeLinecap="round"
1338 strokeLinejoin="round"
1339 strokeWidth={2}
1340 d="M5 13l4 4L19 7"
1341 />
1342 </svg>
1343 )}
1344 {log.status === "error" && (
1345 <svg
1346 className="w-4 h-4 text-red-600 dark:text-red-400"
1347 fill="none"
1348 stroke="currentColor"
1349 viewBox="0 0 24 24"
1350 >
1351 <path
1352 strokeLinecap="round"
1353 strokeLinejoin="round"
1354 strokeWidth={2}
1355 d="M6 18L18 6M6 6l12 12"
1356 />
1357 </svg>
1358 )}
1359 {log.status === "logged" && (
1360 <svg
1361 className="w-4 h-4 text-blue-600 dark:text-blue-400"
1362 fill="none"
1363 stroke="currentColor"
1364 viewBox="0 0 24 24"
1365 >
1366 <path
1367 strokeLinecap="round"
1368 strokeLinejoin="round"
1369 strokeWidth={2}
1370 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1371 />
1372 </svg>
1373 )}
1374 </div>
1375 <div>
1376 <div className="font-medium text-gray-900 dark:text-white">
1377 {log.event_name}
1378 </div>
1379 <div className="text-sm text-gray-600 dark:text-gray-400">
1380 {log.event_data?.trip_name ? (
1381 <div>
1382 <span>{__("Trip:", "yatra")} </span>
1383 {log.event_data?.trip_url ? (
1384 <a
1385 href={log.event_data.trip_url}
1386 target="_blank"
1387 rel="noopener noreferrer"
1388 className="text-blue-600 hover:text-blue-800 underline"
1389 >
1390 {log.event_data.trip_name}
1391 </a>
1392 ) : (
1393 <span>{log.event_data.trip_name}</span>
1394 )}
1395 </div>
1396 ) : (
1397 <span>{log.event_type || "Frontend"}</span>
1398 )}
1399 </div>
1400 {log.event_data?.value && (
1401 <div className="text-xs text-gray-500 dark:text-gray-500">
1402 {__("Value:", "yatra")}{" "}
1403 {log.event_data.currency || "USD"}{" "}
1404 {log.event_data.value}
1405 </div>
1406 )}
1407 </div>
1408 </div>
1409 <div className="text-right">
1410 <div className="text-sm text-gray-900 dark:text-white">
1411 {new Date(log.timestamp).toLocaleTimeString()}
1412 </div>
1413 <div className="text-xs text-gray-600 dark:text-gray-400">
1414 {new Date(log.timestamp).toLocaleDateString()}
1415 </div>
1416 </div>
1417 </div>
1418 ))}
1419 </div>
1420 ) : (
1421 <div className="text-center py-8">
1422 <div className="mx-auto w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center mb-4">
1423 <svg
1424 className="w-6 h-6 text-gray-400"
1425 fill="none"
1426 stroke="currentColor"
1427 viewBox="0 0 24 24"
1428 >
1429 <path
1430 strokeLinecap="round"
1431 strokeLinejoin="round"
1432 strokeWidth={2}
1433 d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
1434 />
1435 </svg>
1436 </div>
1437 <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
1438 {__("No Events Yet", "yatra")}
1439 </h3>
1440 <p className="text-gray-600 dark:text-gray-400">
1441 {__(
1442 "Events will appear here once users start interacting with your site.",
1443 "yatra",
1444 )}
1445 </p>
1446 </div>
1447 )}
1448 </div>
1449 </div>
1450
1451 {/* Quick Links */}
1452 <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6">
1453 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
1454 {__("Quick Links", "yatra")}
1455 </h3>
1456 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1457 <a
1458 href={`https://www.facebook.com/events_manager2/list/pixel/${facebookPixelData.pixel_id}/test_events`}
1459 target="_blank"
1460 rel="noopener noreferrer"
1461 className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
1462 >
1463 <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
1464 <svg
1465 className="w-4 h-4 text-blue-600 dark:text-blue-400"
1466 fill="currentColor"
1467 viewBox="0 0 24 24"
1468 >
1469 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
1470 </svg>
1471 </div>
1472 <div>
1473 <div className="font-medium text-gray-900 dark:text-white">
1474 {__("Test Events", "yatra")}
1475 </div>
1476 <div className="text-sm text-gray-600 dark:text-gray-400">
1477 {__("Facebook Events Manager", "yatra")}
1478 </div>
1479 </div>
1480 </a>
1481
1482 <a
1483 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=settings#integration`}
1484 className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
1485 >
1486 <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
1487 <svg
1488 className="w-4 h-4 text-blue-600 dark:text-blue-400"
1489 fill="none"
1490 stroke="currentColor"
1491 viewBox="0 0 24 24"
1492 >
1493 <path
1494 strokeLinecap="round"
1495 strokeLinejoin="round"
1496 strokeWidth={2}
1497 d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
1498 />
1499 <path
1500 strokeLinecap="round"
1501 strokeLinejoin="round"
1502 strokeWidth={2}
1503 d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
1504 />
1505 </svg>
1506 </div>
1507 <div>
1508 <div className="font-medium text-gray-900 dark:text-white">
1509 {__("Pixel Settings", "yatra")}
1510 </div>
1511 <div className="text-sm text-gray-600 dark:text-gray-400">
1512 {__("Configuration & Options", "yatra")}
1513 </div>
1514 </div>
1515 </a>
1516 </div>
1517 </div>
1518 </div>
1519 );
1520 };
1521
1522 // Google Analytics 4 Reports Component
1523 const GoogleAnalyticsReports: React.FC = () => {
1524 const [clearingLogs, setClearingLogs] = useState(false);
1525 const { showToast } = useToast();
1526
1527 // Fetch fresh Google Analytics data
1528 const {
1529 data: freshGAData,
1530 refetch: refetchGAData,
1531 isLoading: isGALoading,
1532 } = useQuery({
1533 queryKey: ["google-analytics-status"],
1534 queryFn: async () => {
1535 const raw = await apiService.getGoogleAnalyticsSettings();
1536 const payload = unwrapApiPayload<Record<string, unknown>>(raw);
1537 return (payload && typeof payload === "object" ? payload : {}) as Record<
1538 string,
1539 unknown
1540 >;
1541 },
1542 refetchInterval: 30000, // Refresh every 30 seconds
1543 });
1544
1545 // Always use fresh data from API
1546 const googleAnalyticsData = (freshGAData || {}) as {
1547 measurement_id?: string;
1548 eventLogs?: Array<{ status?: string; [key: string]: unknown }>;
1549 connectionStatus?: {
1550 measurementConnected?: boolean;
1551 apiSecretConnected?: boolean;
1552 };
1553 [key: string]: unknown;
1554 };
1555
1556 const getEventStats = () => {
1557 const logs = googleAnalyticsData.eventLogs || [];
1558 return {
1559 success: logs.filter((log) => log.status === "success").length,
1560 errors: logs.filter((log) => log.status === "error").length,
1561 total: logs.length,
1562 };
1563 };
1564
1565 const getRecentEvents = () => {
1566 const logs = googleAnalyticsData.eventLogs || [];
1567 return logs.slice(-10).reverse(); // Show last 10 events, newest first
1568 };
1569
1570 const clearGALogs = async () => {
1571 setClearingLogs(true);
1572 try {
1573 const raw = (await apiService.clearGoogleAnalyticsEventLogs()) as {
1574 success?: boolean;
1575 };
1576 if (raw?.success) {
1577 showToast(__("Event logs cleared successfully.", "yatra"), "success");
1578 // Refetch fresh data to update the UI
1579 await refetchGAData();
1580 }
1581 } catch (error: any) {
1582 showToast(error.message || __("Failed to clear logs.", "yatra"), "error");
1583 } finally {
1584 setClearingLogs(false);
1585 }
1586 };
1587
1588 const eventStats = getEventStats();
1589 const recentEvents = getRecentEvents();
1590
1591 // Empty-state gate. Mirrors the Facebook Pixel tab: same three
1592 // funnel paths (no Pro, module disabled, no Measurement ID) → same
1593 // visual treatment. Splits the CTA + body between
1594 // "Enable the module" vs. "Configure in Settings" based on which
1595 // condition tripped.
1596 const gaModuleActive =
1597 isProPluginActive() && isModuleActive("google_analytics");
1598 if (!gaModuleActive || !googleAnalyticsData.measurement_id) {
1599 return (
1600 <div className="text-center py-12">
1601 <div className="mx-auto w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center mb-4">
1602 <svg
1603 className="w-8 h-8 text-gray-400"
1604 fill="currentColor"
1605 viewBox="0 0 24 24"
1606 >
1607 <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
1608 <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
1609 <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
1610 <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
1611 </svg>
1612 </div>
1613 <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
1614 {__("Google Analytics 4 Not Configured", "yatra")}
1615 </h3>
1616 <p className="text-gray-600 dark:text-gray-400 mb-4">
1617 {!gaModuleActive
1618 ? __(
1619 "Enable the Google Analytics 4 module under Modules and add your Measurement ID in Settings to start tracking conversion events.",
1620 "yatra",
1621 )
1622 : __(
1623 "Configure your Google Analytics 4 in Settings to start tracking conversion events.",
1624 "yatra",
1625 )}
1626 </p>
1627 <a
1628 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=${
1629 !gaModuleActive ? "modules" : "settings#integration"
1630 }`}
1631 className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1632 >
1633 {!gaModuleActive
1634 ? __("Open Modules", "yatra")
1635 : __("Configure Google Analytics 4", "yatra")}
1636 </a>
1637 </div>
1638 );
1639 }
1640
1641 return (
1642 <div className="space-y-6">
1643 {/* Connection Status */}
1644 <div className="flex items-center justify-between mb-4">
1645 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1646 {__("Connection Status", "yatra")}
1647 </h3>
1648 <Button
1649 type="button"
1650 onClick={() => {
1651 refetchGAData();
1652 showToast(__("Status refreshed successfully!", "yatra"), "success");
1653 }}
1654 variant="outline"
1655 size="sm"
1656 disabled={isGALoading}
1657 >
1658 {isGALoading ? (
1659 <>
1660 <svg
1661 className="animate-spin -ml-1 mr-2 h-4 w-4"
1662 fill="none"
1663 viewBox="0 0 24 24"
1664 >
1665 <circle
1666 className="opacity-25"
1667 cx="12"
1668 cy="12"
1669 r="10"
1670 stroke="currentColor"
1671 strokeWidth="4"
1672 ></circle>
1673 <path
1674 className="opacity-75"
1675 fill="currentColor"
1676 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
1677 ></path>
1678 </svg>
1679 {__("Refreshing...", "yatra")}
1680 </>
1681 ) : (
1682 <>
1683 <svg
1684 className="-ml-1 mr-2 h-4 w-4"
1685 fill="none"
1686 viewBox="0 0 24 24"
1687 stroke="currentColor"
1688 >
1689 <path
1690 strokeLinecap="round"
1691 strokeLinejoin="round"
1692 strokeWidth={2}
1693 d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
1694 />
1695 </svg>
1696 {__("Refresh Status", "yatra")}
1697 </>
1698 )}
1699 </Button>
1700 </div>
1701
1702 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1703 <div
1704 className={`p-4 rounded-lg border ${
1705 googleAnalyticsData.connectionStatus?.measurementConnected
1706 ? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
1707 : "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
1708 }`}
1709 >
1710 <div className="flex items-center justify-between">
1711 <div>
1712 <p className="text-sm font-medium text-gray-900 dark:text-white">
1713 {__("Measurement ID", "yatra")}
1714 </p>
1715 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
1716 {googleAnalyticsData.connectionStatus?.measurementConnected
1717 ? __("Connected", "yatra")
1718 : __("Not Connected", "yatra")}
1719 </p>
1720 </div>
1721 <div
1722 className={`w-8 h-8 rounded-full flex items-center justify-center ${
1723 googleAnalyticsData.connectionStatus?.measurementConnected
1724 ? "bg-green-100 dark:bg-green-900/30"
1725 : "bg-red-100 dark:bg-red-900/30"
1726 }`}
1727 >
1728 {googleAnalyticsData.connectionStatus?.measurementConnected ? (
1729 <svg
1730 className="w-4 h-4 text-green-600 dark:text-green-400"
1731 fill="none"
1732 stroke="currentColor"
1733 viewBox="0 0 24 24"
1734 >
1735 <path
1736 strokeLinecap="round"
1737 strokeLinejoin="round"
1738 strokeWidth={2}
1739 d="M5 13l4 4L19 7"
1740 />
1741 </svg>
1742 ) : (
1743 <svg
1744 className="w-4 h-4 text-red-600 dark:text-red-400"
1745 fill="none"
1746 stroke="currentColor"
1747 viewBox="0 0 24 24"
1748 >
1749 <path
1750 strokeLinecap="round"
1751 strokeLinejoin="round"
1752 strokeWidth={2}
1753 d="M6 18L18 6M6 6l12 12"
1754 />
1755 </svg>
1756 )}
1757 </div>
1758 </div>
1759 </div>
1760
1761 <div
1762 className={`p-4 rounded-lg border ${
1763 googleAnalyticsData.connectionStatus?.apiSecretConnected
1764 ? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
1765 : "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800"
1766 }`}
1767 >
1768 <div className="flex items-center justify-between">
1769 <div>
1770 <p className="text-sm font-medium text-gray-900 dark:text-white">
1771 {__("API Secret", "yatra")}
1772 </p>
1773 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
1774 {googleAnalyticsData.connectionStatus?.apiSecretConnected
1775 ? __("Valid", "yatra")
1776 : __("Invalid", "yatra")}
1777 </p>
1778 </div>
1779 <div
1780 className={`w-8 h-8 rounded-full flex items-center justify-center ${
1781 googleAnalyticsData.connectionStatus?.apiSecretConnected
1782 ? "bg-green-100 dark:bg-green-900/30"
1783 : "bg-red-100 dark:bg-red-900/30"
1784 }`}
1785 >
1786 {googleAnalyticsData.connectionStatus?.apiSecretConnected ? (
1787 <svg
1788 className="w-4 h-4 text-green-600 dark:text-green-400"
1789 fill="none"
1790 stroke="currentColor"
1791 viewBox="0 0 24 24"
1792 >
1793 <path
1794 strokeLinecap="round"
1795 strokeLinejoin="round"
1796 strokeWidth={2}
1797 d="M5 13l4 4L19 7"
1798 />
1799 </svg>
1800 ) : (
1801 <svg
1802 className="w-4 h-4 text-red-600 dark:text-red-400"
1803 fill="none"
1804 stroke="currentColor"
1805 viewBox="0 0 24 24"
1806 >
1807 <path
1808 strokeLinecap="round"
1809 strokeLinejoin="round"
1810 strokeWidth={2}
1811 d="M6 18L18 6M6 6l12 12"
1812 />
1813 </svg>
1814 )}
1815 </div>
1816 </div>
1817 </div>
1818
1819 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
1820 <div className="flex items-center justify-between">
1821 <div>
1822 <p className="text-sm font-medium text-gray-900 dark:text-white">
1823 {__("Measurement Protocol", "yatra")}
1824 </p>
1825 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
1826 {googleAnalyticsData.use_measurement_protocol
1827 ? __("Enabled", "yatra")
1828 : __("Disabled", "yatra")}
1829 </p>
1830 </div>
1831 <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
1832 <svg
1833 className="w-4 h-4 text-blue-600 dark:text-blue-400"
1834 fill="currentColor"
1835 viewBox="0 0 24 24"
1836 >
1837 <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
1838 <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
1839 </svg>
1840 </div>
1841 </div>
1842 </div>
1843 </div>
1844
1845 {/* Event Statistics */}
1846 <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
1847 <div className="p-6 border-b border-gray-200 dark:border-gray-700">
1848 <div className="flex items-center justify-between">
1849 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1850 {__("Event Statistics", "yatra")}
1851 </h3>
1852 <Button
1853 type="button"
1854 onClick={() => clearGALogs()}
1855 variant="outline"
1856 size="sm"
1857 disabled={clearingLogs}
1858 >
1859 {clearingLogs
1860 ? __("Clearing...", "yatra")
1861 : __("Clear Logs", "yatra")}
1862 </Button>
1863 </div>
1864 </div>
1865
1866 <div className="p-6">
1867 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
1868 <div className="text-center">
1869 <div className="text-3xl font-bold text-green-600 dark:text-green-400">
1870 {eventStats.success}
1871 </div>
1872 <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1873 {__("Successful Events", "yatra")}
1874 </div>
1875 </div>
1876 <div className="text-center">
1877 <div className="text-3xl font-bold text-red-600 dark:text-red-400">
1878 {eventStats.errors}
1879 </div>
1880 <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1881 {__("Failed Events", "yatra")}
1882 </div>
1883 </div>
1884 <div className="text-center">
1885 <div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
1886 {eventStats.total}
1887 </div>
1888 <div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1889 {__("Total Events", "yatra")}
1890 </div>
1891 </div>
1892 </div>
1893 </div>
1894 </div>
1895
1896 {/* Recent Events */}
1897 <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
1898 <div className="p-6 border-b border-gray-200 dark:border-gray-700">
1899 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1900 {__("Recent Activity", "yatra")}
1901 </h3>
1902 </div>
1903
1904 <div className="p-6">
1905 {recentEvents.length > 0 ? (
1906 <div className="space-y-3">
1907 {recentEvents.map((log: any, index: number) => (
1908 <div
1909 key={index}
1910 className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
1911 >
1912 <div className="flex items-center gap-3">
1913 <div
1914 className={`w-8 h-8 rounded-full flex items-center justify-center ${
1915 log.status === "success"
1916 ? "bg-green-100 dark:bg-green-900/30"
1917 : log.status === "error"
1918 ? "bg-red-100 dark:bg-red-900/30"
1919 : "bg-blue-100 dark:bg-blue-900/30"
1920 }`}
1921 >
1922 {log.status === "success" && (
1923 <svg
1924 className="w-4 h-4 text-green-600 dark:text-green-400"
1925 fill="none"
1926 stroke="currentColor"
1927 viewBox="0 0 24 24"
1928 >
1929 <path
1930 strokeLinecap="round"
1931 strokeLinejoin="round"
1932 strokeWidth={2}
1933 d="M5 13l4 4L19 7"
1934 />
1935 </svg>
1936 )}
1937 {log.status === "error" && (
1938 <svg
1939 className="w-4 h-4 text-red-600 dark:text-red-400"
1940 fill="none"
1941 stroke="currentColor"
1942 viewBox="0 0 24 24"
1943 >
1944 <path
1945 strokeLinecap="round"
1946 strokeLinejoin="round"
1947 strokeWidth={2}
1948 d="M6 18L18 6M6 6l12 12"
1949 />
1950 </svg>
1951 )}
1952 {log.status === "logged" && (
1953 <svg
1954 className="w-4 h-4 text-blue-600 dark:text-blue-400"
1955 fill="none"
1956 stroke="currentColor"
1957 viewBox="0 0 24 24"
1958 >
1959 <path
1960 strokeLinecap="round"
1961 strokeLinejoin="round"
1962 strokeWidth={2}
1963 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1964 />
1965 </svg>
1966 )}
1967 </div>
1968 <div>
1969 <div className="font-medium text-gray-900 dark:text-white">
1970 {log.event_name}
1971 </div>
1972 <div className="text-sm text-gray-600 dark:text-gray-400">
1973 {log.event_data?.trip_name ? (
1974 <div>
1975 <span>{__("Trip:", "yatra")} </span>
1976 {log.event_data?.trip_url ? (
1977 <a
1978 href={log.event_data.trip_url}
1979 target="_blank"
1980 rel="noopener noreferrer"
1981 className="text-blue-600 hover:text-blue-800 underline"
1982 >
1983 {log.event_data.trip_name}
1984 </a>
1985 ) : (
1986 <span>{log.event_data.trip_name}</span>
1987 )}
1988 </div>
1989 ) : (
1990 <span>{log.event_type || "Frontend"}</span>
1991 )}
1992 </div>
1993 {log.event_data?.value && (
1994 <div className="text-xs text-gray-500 dark:text-gray-500">
1995 {__("Value:", "yatra")}{" "}
1996 {log.event_data.currency || "USD"}{" "}
1997 {log.event_data.value}
1998 </div>
1999 )}
2000 </div>
2001 </div>
2002 <div className="text-right">
2003 <div className="text-sm text-gray-900 dark:text-white">
2004 {new Date(log.timestamp).toLocaleTimeString()}
2005 </div>
2006 <div className="text-xs text-gray-600 dark:text-gray-400">
2007 {new Date(log.timestamp).toLocaleDateString()}
2008 </div>
2009 </div>
2010 </div>
2011 ))}
2012 </div>
2013 ) : (
2014 <div className="text-center py-8">
2015 <div className="mx-auto w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center mb-4">
2016 <svg
2017 className="w-6 h-6 text-gray-400"
2018 fill="none"
2019 stroke="currentColor"
2020 viewBox="0 0 24 24"
2021 >
2022 <path
2023 strokeLinecap="round"
2024 strokeLinejoin="round"
2025 strokeWidth={2}
2026 d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
2027 />
2028 </svg>
2029 </div>
2030 <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
2031 {__("No Events Yet", "yatra")}
2032 </h3>
2033 <p className="text-gray-600 dark:text-gray-400">
2034 {__(
2035 "Events will appear here once users start interacting with your site.",
2036 "yatra",
2037 )}
2038 </p>
2039 </div>
2040 )}
2041 </div>
2042 </div>
2043
2044 {/* Quick Links */}
2045 <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6">
2046 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2047 {__("Quick Links", "yatra")}
2048 </h3>
2049 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
2050 <a
2051 href={`https://analytics.google.com/analytics/web/#/debugview`}
2052 target="_blank"
2053 rel="noopener noreferrer"
2054 className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
2055 >
2056 <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
2057 <svg
2058 className="w-4 h-4 text-blue-600 dark:text-blue-400"
2059 fill="currentColor"
2060 viewBox="0 0 24 24"
2061 >
2062 <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
2063 <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
2064 </svg>
2065 </div>
2066 <div>
2067 <div className="font-medium text-gray-900 dark:text-white">
2068 {__("Debug View", "yatra")}
2069 </div>
2070 <div className="text-sm text-gray-600 dark:text-gray-400">
2071 {__("Google Analytics Debug", "yatra")}
2072 </div>
2073 </div>
2074 </a>
2075
2076 <a
2077 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=settings#integration`}
2078 className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
2079 >
2080 <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
2081 <svg
2082 className="w-4 h-4 text-blue-600 dark:text-blue-400"
2083 fill="none"
2084 stroke="currentColor"
2085 viewBox="0 0 24 24"
2086 >
2087 <path
2088 strokeLinecap="round"
2089 strokeLinejoin="round"
2090 strokeWidth={2}
2091 d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
2092 />
2093 <path
2094 strokeLinecap="round"
2095 strokeLinejoin="round"
2096 strokeWidth={2}
2097 d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
2098 />
2099 </svg>
2100 </div>
2101 <div>
2102 <div className="font-medium text-gray-900 dark:text-white">
2103 {__("GA4 Settings", "yatra")}
2104 </div>
2105 <div className="text-sm text-gray-600 dark:text-gray-400">
2106 {__("Configuration & Options", "yatra")}
2107 </div>
2108 </div>
2109 </a>
2110 </div>
2111 </div>
2112 </div>
2113 );
2114 };
2115
2116 const TravelBookingReports: React.FC = () => {
2117 // Cap-filter the category list. canCap honors the WP admin fallback
2118 // (admins see everything), the Team module's userCaps array (granular
2119 // role-based), and falls back to default-allow when no team module is
2120 // installed (so non-Team installs keep their pre-existing behavior).
2121 const visibleCategories = useMemo(
2122 () => TravelReportCategories.filter((c) => canCap(c.cap)),
2123 [],
2124 );
2125
2126 const [selectedCategory, setSelectedCategory] = useState(
2127 () => visibleCategories[0]?.id || "booking-overview",
2128 );
2129 const [dateRange, setDateRange] = useState("last_30_days");
2130 const [viewType, setViewType] = useState("summary"); // 'summary', 'daily', 'weekly', 'monthly'
2131
2132 // If the operator's currently-selected tab becomes invalid (e.g.
2133 // role changed mid-session), fall back to the first visible tab.
2134 React.useEffect(() => {
2135 if (!visibleCategories.some((c) => c.id === selectedCategory)) {
2136 const fallback = visibleCategories[0]?.id;
2137 if (fallback) setSelectedCategory(fallback);
2138 }
2139 }, [visibleCategories, selectedCategory]);
2140
2141 // Fetch real data from Yatra ReportsController using apiClient
2142 const { data: reportData, isLoading } = useQuery({
2143 queryKey: ["yatra-travel-reports", dateRange],
2144 queryFn: async () => {
2145 const params = getDateRangeParams(dateRange);
2146 const response = await apiClient.get(
2147 `/reports?date_from=${params.start}&date_to=${params.end}`,
2148 );
2149 return response?.data || {};
2150 },
2151 });
2152
2153 // Calculate date range parameters
2154 function getDateRangeParams(range: string) {
2155 const today = new Date();
2156 const start = new Date();
2157
2158 switch (range) {
2159 case "today":
2160 start.setHours(0, 0, 0, 0);
2161 break;
2162 case "last_7_days":
2163 start.setDate(today.getDate() - 7);
2164 break;
2165 case "last_30_days":
2166 start.setDate(today.getDate() - 30);
2167 break;
2168 case "last_90_days":
2169 start.setDate(today.getDate() - 90);
2170 break;
2171 case "this_year":
2172 start.setMonth(0, 1);
2173 break;
2174 default:
2175 start.setDate(today.getDate() - 30);
2176 }
2177
2178 return {
2179 start: start.toISOString().split("T")[0],
2180 end: today.toISOString().split("T")[0],
2181 };
2182 }
2183
2184 // Travel Business KPIs
2185 const travelKPIs = useMemo(() => {
2186 if (!reportData)
2187 return {
2188 totalBookings: 0,
2189 totalRevenue: 0,
2190 occupancyRate: 0,
2191 avgBookingValue: 0,
2192 cancellationRate: 0,
2193 upcomingDepartures: 0,
2194 };
2195
2196 return {
2197 totalBookings: reportData.booking_stats?.total || 0,
2198 totalRevenue: reportData.revenue_stats?.total || 0,
2199 occupancyRate: reportData.operational_stats?.occupancyRate || 0,
2200 avgBookingValue: reportData.revenue_stats?.average || 0,
2201 cancellationRate: reportData.booking_stats?.cancellationRate || 0,
2202 upcomingDepartures: reportData.operational_stats?.upcomingDepartures || 0,
2203 };
2204 }, [reportData]);
2205
2206 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
2207 const formatCurrencyAmount = (amount: number) =>
2208 formatYatraMoney(Number(amount) || 0, globalCurrency, {
2209 zeroAsUnknown: false,
2210 });
2211
2212 // CSV export — bundles the visible breakdown (or summary KPIs when
2213 // no breakdown is appropriate) into a downloadable file scoped to
2214 // the picked date range. Doesn't hit the network; everything we
2215 // already have in memory is enough.
2216 const handleExportCsv = () => {
2217 const params = getDateRangeParams(dateRange);
2218 const rows: (string | number | null | undefined)[][] = [];
2219
2220 // Top KPI summary first — operators paste this into the email
2221 // before the detail table.
2222 rows.push(["Yatra Reports — Summary"]);
2223 rows.push([`Date range: ${params.start} to ${params.end}`]);
2224 rows.push([]);
2225 rows.push(["Metric", "Value"]);
2226 rows.push(["Total bookings", travelKPIs.totalBookings]);
2227 rows.push(["Total revenue", travelKPIs.totalRevenue]);
2228 rows.push(["Avg booking value", travelKPIs.avgBookingValue]);
2229 rows.push(["Occupancy rate (%)", travelKPIs.occupancyRate]);
2230 rows.push(["Cancellation rate (%)", travelKPIs.cancellationRate]);
2231 rows.push(["Upcoming departures", travelKPIs.upcomingDepartures]);
2232 rows.push([]);
2233
2234 // Booking trend (day-level) if the operator has revenue/booking data.
2235 const trend: any[] = reportData?.booking_trend || [];
2236 const revenueTrend: any[] = reportData?.revenue_trend || [];
2237 if (trend.length) {
2238 rows.push(["Day", "Bookings", "Revenue"]);
2239 trend.forEach((row: any, i: number) => {
2240 rows.push([
2241 row.date || row.label,
2242 row.value,
2243 revenueTrend[i]?.value ?? "",
2244 ]);
2245 });
2246 rows.push([]);
2247 }
2248
2249 // Top trips
2250 const tp = reportData?.trip_performance || [];
2251 if (tp.length) {
2252 rows.push(["Trip", "Bookings", "Revenue", "Occupancy %"]);
2253 tp.forEach((t: any) => {
2254 rows.push([t.label, t.value, t.revenue, t.occupancy]);
2255 });
2256 rows.push([]);
2257 }
2258
2259 // Payment methods
2260 const pm = reportData?.payment_methods || [];
2261 if (pm.length) {
2262 rows.push(["Payment method", "Count", "Revenue"]);
2263 pm.forEach((m: any) => {
2264 rows.push([m.method, m.count, m.revenue]);
2265 });
2266 rows.push([]);
2267 }
2268
2269 // Top destinations
2270 const dest = reportData?.top_destinations || [];
2271 if (dest.length) {
2272 rows.push(["Destination", "Bookings", "Revenue"]);
2273 dest.forEach((d: any) => {
2274 rows.push([d.label, d.value, d.revenue]);
2275 });
2276 rows.push([]);
2277 }
2278
2279 const blob = buildCsv(rows);
2280 downloadCsv(blob, csvFilename("reports", params.start, params.end));
2281 };
2282
2283 return (
2284 <div className="space-y-6">
2285 {/* ── HEADER ──────────────────────────────────────────────────── */}
2286 <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
2287 <div>
2288 <h1 className="flex items-center gap-3 text-2xl font-bold text-gray-900 dark:text-white">
2289 <SVGIcons.BarChart />
2290 {__("Travel Booking Reports", "yatra")}
2291 </h1>
2292 <p className="text-sm text-gray-600 dark:text-gray-400">
2293 {__(
2294 "Essential analytics for your travel booking business.",
2295 "yatra",
2296 )}
2297 </p>
2298 </div>
2299
2300 <div className="flex flex-wrap items-center gap-2">
2301 <select
2302 value={dateRange}
2303 onChange={(e) => setDateRange(e.target.value)}
2304 className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
2305 aria-label={__("Date range", "yatra")}
2306 >
2307 <option value="today">{__("Today", "yatra")}</option>
2308 <option value="last_7_days">{__("Last 7 days", "yatra")}</option>
2309 <option value="last_30_days">{__("Last 30 days", "yatra")}</option>
2310 <option value="last_90_days">{__("Last 90 days", "yatra")}</option>
2311 <option value="this_year">{__("This year", "yatra")}</option>
2312 </select>
2313
2314 <select
2315 value={viewType}
2316 onChange={(e) => setViewType(e.target.value)}
2317 className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
2318 aria-label={__("Breakdown view", "yatra")}
2319 >
2320 <option value="summary">{__("Summary View", "yatra")}</option>
2321 <option value="daily">{__("Daily Breakdown", "yatra")}</option>
2322 <option value="weekly">{__("Weekly Breakdown", "yatra")}</option>
2323 <option value="monthly">{__("Monthly Breakdown", "yatra")}</option>
2324 </select>
2325
2326 <Button
2327 type="button"
2328 variant="outline"
2329 size="sm"
2330 onClick={handleExportCsv}
2331 disabled={isLoading || !reportData}
2332 title={__("Download this report's data as CSV", "yatra")}
2333 >
2334 {__("Export CSV", "yatra")}
2335 </Button>
2336 </div>
2337 </div>
2338
2339 {/* Key Performance Indicators */}
2340 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
2341 {isLoading ? (
2342 <>
2343 {[...Array(6)].map((_, i) => (
2344 <SkeletonCard key={i} />
2345 ))}
2346 </>
2347 ) : (
2348 <>
2349 <Card className="p-4">
2350 <div className="flex items-center justify-between">
2351 <div>
2352 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
2353 {__("Total Bookings", "yatra")}
2354 </p>
2355 <p className="text-xl font-bold text-gray-900 dark:text-white">
2356 {travelKPIs.totalBookings}
2357 </p>
2358 </div>
2359 <div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
2360 <SVGIcons.Calendar />
2361 </div>
2362 </div>
2363 </Card>
2364
2365 <Card className="p-4">
2366 <div className="flex items-center justify-between">
2367 <div>
2368 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
2369 {__("Total Revenue", "yatra")}
2370 </p>
2371 <p className="text-xl font-bold text-emerald-600 dark:text-emerald-400">
2372 {formatCurrencyAmount(travelKPIs.totalRevenue)}
2373 </p>
2374 </div>
2375 <div className="p-2 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
2376 <SVGIcons.DollarSign />
2377 </div>
2378 </div>
2379 </Card>
2380
2381 <Card className="p-4">
2382 <div className="flex items-center justify-between">
2383 <div>
2384 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
2385 {__("Occupancy Rate", "yatra")}
2386 </p>
2387 <p className="text-xl font-bold text-purple-600 dark:text-purple-400">
2388 {travelKPIs.occupancyRate.toFixed(1)}%
2389 </p>
2390 </div>
2391 <div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
2392 <SVGIcons.Target />
2393 </div>
2394 </div>
2395 </Card>
2396
2397 <Card className="p-4">
2398 <div className="flex items-center justify-between">
2399 <div>
2400 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
2401 {__("Avg Booking Value", "yatra")}
2402 </p>
2403 <p className="text-xl font-bold text-amber-600 dark:text-amber-400">
2404 {formatCurrencyAmount(travelKPIs.avgBookingValue)}
2405 </p>
2406 </div>
2407 <div className="p-2 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
2408 <SVGIcons.BarChart />
2409 </div>
2410 </div>
2411 </Card>
2412
2413 <Card className="p-4">
2414 <div className="flex items-center justify-between">
2415 <div>
2416 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
2417 {__("Cancellation Rate", "yatra")}
2418 </p>
2419 <p className="text-xl font-bold text-red-600 dark:text-red-400">
2420 {travelKPIs.cancellationRate.toFixed(1)}%
2421 </p>
2422 </div>
2423 <div className="p-2 bg-red-50 dark:bg-red-900/20 rounded-lg">
2424 <SVGIcons.XCircle />
2425 </div>
2426 </div>
2427 </Card>
2428
2429 <Card className="p-4">
2430 <div className="flex items-center justify-between">
2431 <div>
2432 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
2433 {__("Upcoming Departures", "yatra")}
2434 </p>
2435 <p className="text-xl font-bold text-blue-600 dark:text-blue-400">
2436 {travelKPIs.upcomingDepartures}
2437 </p>
2438 </div>
2439 <div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
2440 <SVGIcons.Truck />
2441 </div>
2442 </div>
2443 </Card>
2444 </>
2445 )}
2446 </div>
2447
2448 {/* Report Categories - Tab Navigation */}
2449 <Card>
2450 <CardHeader>
2451 <CardTitle className="flex items-center gap-2">
2452 <SVGIcons.BarChart />
2453 {__("Travel Business Reports", "yatra")}
2454 </CardTitle>
2455 <CardDescription>
2456 {__(
2457 "Comprehensive analytics for your travel booking operations",
2458 "yatra",
2459 )}
2460 </CardDescription>
2461 </CardHeader>
2462
2463 {/* Tab Navigation: dropdown on small screens; scrollable pill row on md+ */}
2464 <div className="border-b border-gray-200 dark:border-gray-700 min-w-0">
2465 <div className="px-4 pt-2 pb-3 md:hidden">
2466 <label
2467 htmlFor="yatra-report-section-select"
2468 className="mb-1.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
2469 >
2470 {__("Report section", "yatra")}
2471 </label>
2472 <Select
2473 id="yatra-report-section-select"
2474 value={selectedCategory}
2475 onChange={(e) => setSelectedCategory(e.target.value)}
2476 aria-label={__("Report section", "yatra")}
2477 >
2478 {visibleCategories.map((category) => (
2479 <option key={category.id} value={category.id}>
2480 {__(category.title, "yatra")}
2481 </option>
2482 ))}
2483 </Select>
2484 </div>
2485
2486 <nav
2487 className="hidden md:flex min-w-0 flex-nowrap items-stretch gap-1 overflow-x-auto overflow-y-hidden scroll-smooth px-4 pe-6 pb-1 sm:px-6 sm:pe-8 [scrollbar-width:thin] [-webkit-overflow-scrolling:touch]"
2488 aria-label={__("Report sections", "yatra")}
2489 >
2490 {visibleCategories.map((category) => (
2491 <button
2492 key={category.id}
2493 type="button"
2494 onClick={() => setSelectedCategory(category.id)}
2495 className={`shrink-0 snap-start inline-flex max-w-none items-center gap-2 rounded-t-md border-b-2 px-3 py-3 text-left text-sm font-medium whitespace-nowrap transition-colors sm:px-4 sm:py-3.5 ${
2496 selectedCategory === category.id
2497 ? "border-blue-500 text-blue-600 dark:text-blue-400 bg-blue-50/60 dark:bg-blue-950/30"
2498 : "border-transparent text-gray-600 dark:text-gray-400 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:hover:border-gray-600 dark:hover:bg-gray-800/50 dark:hover:text-gray-200"
2499 }`}
2500 >
2501 <span className="flex shrink-0 [&_svg]:h-5 [&_svg]:w-5">
2502 {React.createElement(
2503 SVGIcons[category.icon as keyof typeof SVGIcons],
2504 )}
2505 </span>
2506 <span className="leading-tight">
2507 {__(category.title, "yatra")}
2508 </span>
2509 </button>
2510 ))}
2511 </nav>
2512 </div>
2513 </Card>
2514
2515 {/* Report Content */}
2516 <Card>
2517 <CardContent>
2518 {isLoading ? (
2519 <SkeletonReportSection />
2520 ) : (
2521 <div className="space-y-6">
2522 {selectedCategory === "booking-overview" && (
2523 <div className="space-y-6">
2524 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2525 {__("Booking Overview", "yatra")}
2526 </h3>
2527
2528 {/* KPI Cards */}
2529 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
2530 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2531 <p className="text-sm text-gray-600 dark:text-gray-400">
2532 {__("Confirmed Bookings", "yatra")}
2533 </p>
2534 <p className="text-xl font-bold text-green-600">
2535 {reportData?.booking_stats?.confirmed || 0}
2536 </p>
2537 </div>
2538 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2539 <p className="text-sm text-gray-600 dark:text-gray-400">
2540 {__("Pending Bookings", "yatra")}
2541 </p>
2542 <p className="text-xl font-bold text-yellow-600">
2543 {reportData?.booking_stats?.pending || 0}
2544 </p>
2545 </div>
2546 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2547 <p className="text-sm text-gray-600 dark:text-gray-400">
2548 {__("Cancelled Bookings", "yatra")}
2549 </p>
2550 <p className="text-xl font-bold text-red-600">
2551 {reportData?.booking_stats?.cancelled || 0}
2552 </p>
2553 </div>
2554 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2555 <p className="text-sm text-gray-600 dark:text-gray-400">
2556 {__("Completed Bookings", "yatra")}
2557 </p>
2558 <p className="text-xl font-bold text-blue-600">
2559 {reportData?.booking_stats?.completed || 0}
2560 </p>
2561 </div>
2562 </div>
2563
2564 {/* Charts Section */}
2565 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
2566 {/* Booking Status Chart */}
2567 <Card>
2568 <CardHeader>
2569 <CardTitle>
2570 {__("Booking Status Distribution", "yatra")}
2571 </CardTitle>
2572 </CardHeader>
2573 <CardContent>
2574 <BookingStatusChart
2575 data={[
2576 {
2577 label: "Confirmed",
2578 value: reportData?.booking_stats?.confirmed || 0,
2579 color: "#10b981",
2580 },
2581 {
2582 label: "Pending",
2583 value: reportData?.booking_stats?.pending || 0,
2584 color: "#f59e0b",
2585 },
2586 {
2587 label: "Cancelled",
2588 value: reportData?.booking_stats?.cancelled || 0,
2589 color: "#ef4444",
2590 },
2591 {
2592 label: "Completed",
2593 value: reportData?.booking_stats?.completed || 0,
2594 color: "#3b82f6",
2595 },
2596 ]}
2597 />
2598 </CardContent>
2599 </Card>
2600
2601 {/* Revenue Trend Chart */}
2602 <Card>
2603 <CardHeader>
2604 <CardTitle>{__("Revenue Trend", "yatra")}</CardTitle>
2605 </CardHeader>
2606 <CardContent>
2607 <BookingsOverviewChart
2608 data={reportData?.revenue_trend || []}
2609 currency={globalCurrency}
2610 />
2611 </CardContent>
2612 </Card>
2613 </div>
2614 </div>
2615 )}
2616
2617 {selectedCategory === "revenue-analysis" && (
2618 <div>
2619 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2620 Revenue Analysis
2621 </h3>
2622 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
2623 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2624 <p className="text-sm text-gray-600 dark:text-gray-400">
2625 Total Revenue
2626 </p>
2627 <p className="text-xl font-bold text-green-600">
2628 {formatCurrencyAmount(
2629 reportData?.revenue_stats?.total || 0,
2630 )}
2631 </p>
2632 </div>
2633 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2634 <p className="text-sm text-gray-600 dark:text-gray-400">
2635 Average Booking Value
2636 </p>
2637 <p className="text-xl font-bold text-blue-600">
2638 {formatCurrencyAmount(
2639 reportData?.revenue_stats?.average || 0,
2640 )}
2641 </p>
2642 </div>
2643 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2644 <p className="text-sm text-gray-600 dark:text-gray-400">
2645 Revenue Lost (Cancellations)
2646 </p>
2647 <p className="text-xl font-bold text-red-600">
2648 {formatCurrencyAmount(
2649 reportData?.cancellations?.revenueLost || 0,
2650 )}
2651 </p>
2652 </div>
2653 </div>
2654 </div>
2655 )}
2656
2657 {selectedCategory === "trip-performance" && (
2658 <div>
2659 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2660 Trip Performance
2661 </h3>
2662 <div className="space-y-4">
2663 {(reportData?.trip_performance || [])
2664 .slice(0, 5)
2665 .map((trip: any, index: number) => (
2666 <div
2667 key={index}
2668 className="flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"
2669 >
2670 <div>
2671 <p className="font-medium text-gray-900 dark:text-white">
2672 {trip.label}
2673 </p>
2674 <p className="text-sm text-gray-600 dark:text-gray-400">
2675 {trip.value} bookings
2676 </p>
2677 </div>
2678 <div className="text-right">
2679 <p className="font-bold text-green-600">
2680 {formatCurrencyAmount(trip.revenue || 0)}
2681 </p>
2682 <p className="text-sm text-gray-600 dark:text-gray-400">
2683 Revenue
2684 </p>
2685 </div>
2686 </div>
2687 ))}
2688 </div>
2689 </div>
2690 )}
2691
2692 {selectedCategory === "departure-management" && (
2693 <div>
2694 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2695 Departure Management
2696 </h3>
2697 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
2698 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2699 <p className="text-sm text-gray-600 dark:text-gray-400">
2700 Upcoming Departures
2701 </p>
2702 <p className="text-xl font-bold text-blue-600">
2703 {reportData?.operational_stats?.upcomingDepartures || 0}
2704 </p>
2705 </div>
2706 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2707 <p className="text-sm text-gray-600 dark:text-gray-400">
2708 Total Capacity
2709 </p>
2710 <p className="text-xl font-bold text-purple-600">
2711 {reportData?.operational_stats?.totalCapacity || 0}
2712 </p>
2713 </div>
2714 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2715 <p className="text-sm text-gray-600 dark:text-gray-400">
2716 Booked Capacity
2717 </p>
2718 <p className="text-xl font-bold text-green-600">
2719 {reportData?.operational_stats?.bookedCapacity || 0}
2720 </p>
2721 </div>
2722 </div>
2723 </div>
2724 )}
2725
2726 {selectedCategory === "customer-insights" && (
2727 <div>
2728 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2729 Customer Insights
2730 </h3>
2731 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
2732 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2733 <p className="text-sm text-gray-600 dark:text-gray-400">
2734 Total Customers
2735 </p>
2736 <p className="text-xl font-bold text-blue-600">
2737 {reportData?.customer_analytics?.totalCustomers || 0}
2738 </p>
2739 </div>
2740 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2741 <p className="text-sm text-gray-600 dark:text-gray-400">
2742 New Customers
2743 </p>
2744 <p className="text-xl font-bold text-green-600">
2745 {reportData?.customer_analytics?.newCustomers || 0}
2746 </p>
2747 </div>
2748 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2749 <p className="text-sm text-gray-600 dark:text-gray-400">
2750 Returning Customers
2751 </p>
2752 <p className="text-xl font-bold text-purple-600">
2753 {reportData?.customer_analytics?.returningCustomers ||
2754 0}
2755 </p>
2756 </div>
2757 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2758 <p className="text-sm text-gray-600 dark:text-gray-400">
2759 Customer Lifetime Value
2760 </p>
2761 <p className="text-xl font-bold text-orange-600">
2762 {formatCurrencyAmount(
2763 reportData?.customer_analytics
2764 ?.customerLifetimeValue || 0,
2765 )}
2766 </p>
2767 </div>
2768 </div>
2769 </div>
2770 )}
2771
2772 {selectedCategory === "operational-metrics" && (
2773 <div>
2774 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
2775 Operational Metrics
2776 </h3>
2777 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
2778 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2779 <p className="text-sm text-gray-600 dark:text-gray-400">
2780 Occupancy Rate
2781 </p>
2782 <p className="text-xl font-bold text-green-600">
2783 {(
2784 reportData?.operational_stats?.occupancyRate || 0
2785 ).toFixed(1)}
2786 %
2787 </p>
2788 </div>
2789 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2790 <p className="text-sm text-gray-600 dark:text-gray-400">
2791 Average Group Size
2792 </p>
2793 <p className="text-xl font-bold text-blue-600">
2794 {(
2795 reportData?.operational_stats?.averageGroupSize || 0
2796 ).toFixed(1)}
2797 </p>
2798 </div>
2799 <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
2800 <p className="text-sm text-gray-600 dark:text-gray-400">
2801 Cancellation Rate
2802 </p>
2803 <p className="text-xl font-bold text-red-600">
2804 {(
2805 reportData?.booking_stats?.cancellationRate || 0
2806 ).toFixed(1)}
2807 %
2808 </p>
2809 </div>
2810 </div>
2811 </div>
2812 )}
2813
2814 {/* ── REVENUE ANALYSIS — Payment method breakdown ─────────
2815 Operators routinely want to know which gateways are
2816 pulling weight (and which they could turn off). The
2817 data is computed in the backend per request. */}
2818 {selectedCategory === "revenue-analysis" &&
2819 Array.isArray(reportData?.payment_methods) &&
2820 reportData.payment_methods.length > 0 && (
2821 <div className="mt-6">
2822 <h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">
2823 {__("Payment Methods", "yatra")}
2824 </h3>
2825 <p className="mb-3 text-xs text-gray-500 dark:text-gray-400">
2826 {__(
2827 "Bookings and gross revenue split by payment gateway. Ranked by revenue.",
2828 "yatra",
2829 )}
2830 </p>
2831 <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
2832 {reportData.payment_methods.slice(0, 6).map((m: any) => (
2833 <div
2834 key={m.method}
2835 className="rounded-lg border border-gray-100 bg-white p-3 dark:border-gray-700 dark:bg-gray-800"
2836 >
2837 <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
2838 {m.method}
2839 </p>
2840 <p className="mt-1 text-lg font-semibold tabular-nums text-emerald-600 dark:text-emerald-400">
2841 {formatCurrencyAmount(m.revenue)}
2842 </p>
2843 <p className="text-xs text-gray-500 dark:text-gray-400">
2844 {m.count} {__("bookings", "yatra")}
2845 </p>
2846 </div>
2847 ))}
2848 </div>
2849 </div>
2850 )}
2851
2852 {/* ── TRIP PERFORMANCE — Top destinations ────────────────
2853 Geographic concentration. Useful when paired with
2854 Top Trips: a single trip can dominate a destination,
2855 or a destination can have a long tail of small wins. */}
2856 {selectedCategory === "trip-performance" &&
2857 Array.isArray(reportData?.top_destinations) &&
2858 reportData.top_destinations.length > 0 && (
2859 <div className="mt-6">
2860 <h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">
2861 {__("Top Destinations", "yatra")}
2862 </h3>
2863 <p className="mb-3 text-xs text-gray-500 dark:text-gray-400">
2864 {__(
2865 "Booking count and revenue by primary destination. Useful for spotting geographic concentration.",
2866 "yatra",
2867 )}
2868 </p>
2869 <div className="space-y-2">
2870 {reportData.top_destinations.map((d: any) => {
2871 const max = Math.max(
2872 1,
2873 ...reportData.top_destinations.map(
2874 (x: any) => x.value,
2875 ),
2876 );
2877 return (
2878 <div
2879 key={d.label}
2880 className="flex items-center gap-3"
2881 >
2882 <div className="w-32 truncate text-sm text-gray-700 dark:text-gray-200">
2883 {d.label}
2884 </div>
2885 <div className="relative h-3 flex-1 rounded-full bg-gray-100 dark:bg-gray-700">
2886 <div
2887 className="h-3 rounded-full bg-blue-500"
2888 style={{
2889 width: `${Math.max(2, (d.value / max) * 100)}%`,
2890 }}
2891 />
2892 </div>
2893 <div className="w-16 text-right text-sm tabular-nums text-gray-700 dark:text-gray-200">
2894 {d.value}
2895 </div>
2896 <div className="w-24 text-right text-sm font-medium tabular-nums text-emerald-600 dark:text-emerald-400">
2897 {formatCurrencyAmount(d.revenue || 0)}
2898 </div>
2899 </div>
2900 );
2901 })}
2902 </div>
2903 </div>
2904 )}
2905
2906 {/* ── OPERATIONAL METRICS — Lead time histogram ──────────
2907 How far in advance customers book. Same-day = last-
2908 minute demand. >quarter = need solid deposit policy. */}
2909 {selectedCategory === "operational-metrics" &&
2910 reportData?.lead_time && (
2911 <div className="mt-6">
2912 <h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">
2913 {__("Booking Lead Time", "yatra")}
2914 </h3>
2915 <p className="mb-3 text-xs text-gray-500 dark:text-gray-400">
2916 {__(
2917 "Time between booking creation and travel date. Average:",
2918 "yatra",
2919 )}{" "}
2920 <span className="font-medium text-gray-700 dark:text-gray-200">
2921 {Number(reportData.lead_time.averageDays || 0).toFixed(
2922 1,
2923 )}{" "}
2924 {__("days", "yatra")}
2925 </span>{" "}
2926 ({reportData.lead_time.sampleSize}{" "}
2927 {__("bookings sampled", "yatra")})
2928 </p>
2929 <div className="space-y-2">
2930 {(reportData.lead_time.buckets || []).map((b: any) => {
2931 const total = (
2932 reportData.lead_time.buckets || []
2933 ).reduce((s: number, x: any) => s + x.value, 0);
2934 const pct =
2935 total > 0 ? Math.round((b.value / total) * 100) : 0;
2936 return (
2937 <div
2938 key={b.label}
2939 className="flex items-center gap-3"
2940 >
2941 <div className="w-44 truncate text-sm text-gray-700 dark:text-gray-200">
2942 {b.label}
2943 </div>
2944 <div className="relative h-3 flex-1 rounded-full bg-gray-100 dark:bg-gray-700">
2945 <div
2946 className="h-3 rounded-full"
2947 style={{
2948 width: `${Math.max(2, pct)}%`,
2949 background: b.color || "#3b82f6",
2950 }}
2951 />
2952 </div>
2953 <div className="w-16 text-right text-sm tabular-nums text-gray-700 dark:text-gray-200">
2954 {b.value}
2955 </div>
2956 <div className="w-12 text-right text-xs text-gray-500 dark:text-gray-400">
2957 {pct}%
2958 </div>
2959 </div>
2960 );
2961 })}
2962 </div>
2963 </div>
2964 )}
2965
2966 {/* ── OPERATIONAL METRICS — Refunds summary ──────────────
2967 Refunds are distinct from cancellations: cancelling
2968 doesn't necessarily refund (deposit policy). Track
2969 separately so finance has a clean view.
2970
2971 Renders unconditionally on this tab — falls back to
2972 zero KPIs when the backend hasn't shipped the
2973 `refunds` block yet (e.g. PHP-FPM cached an older
2974 copy of the controller). Visible empty-state beats
2975 silently hiding the section. */}
2976 {selectedCategory === "operational-metrics" && (
2977 <div className="mt-6">
2978 <h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">
2979 {__("Refunds", "yatra")}
2980 </h3>
2981 <div className="grid grid-cols-1 gap-3 md:grid-cols-4">
2982 <div className="rounded-lg border border-gray-100 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
2983 <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
2984 {__("Refunds issued", "yatra")}
2985 </p>
2986 <p className="mt-1 text-lg font-semibold tabular-nums text-red-600 dark:text-red-400">
2987 {reportData?.refunds?.count ?? 0}
2988 </p>
2989 </div>
2990 <div className="rounded-lg border border-gray-100 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
2991 <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
2992 {__("Refund total", "yatra")}
2993 </p>
2994 <p className="mt-1 text-lg font-semibold tabular-nums text-red-600 dark:text-red-400">
2995 {formatCurrencyAmount(
2996 Number(reportData?.refunds?.total) || 0,
2997 )}
2998 </p>
2999 </div>
3000 <div className="rounded-lg border border-gray-100 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
3001 <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
3002 {__("Refund rate", "yatra")}
3003 </p>
3004 <p className="mt-1 text-lg font-semibold tabular-nums text-amber-600 dark:text-amber-400">
3005 {Number(reportData?.refunds?.refundRate || 0).toFixed(
3006 1,
3007 )}
3008 %
3009 </p>
3010 </div>
3011 <div className="rounded-lg border border-gray-100 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
3012 <p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
3013 {__("Avg refund", "yatra")}
3014 </p>
3015 <p className="mt-1 text-lg font-semibold tabular-nums text-gray-700 dark:text-gray-200">
3016 {formatCurrencyAmount(
3017 Number(reportData?.refunds?.avgRefund) || 0,
3018 )}
3019 </p>
3020 </div>
3021 </div>
3022 </div>
3023 )}
3024
3025 {selectedCategory === "facebook-pixel" && (
3026 <FacebookPixelReports />
3027 )}
3028
3029 {selectedCategory === "google-analytics" && (
3030 <GoogleAnalyticsReports />
3031 )}
3032 </div>
3033 )}
3034 </CardContent>
3035 </Card>
3036
3037 {/* Detailed Breakdown Section */}
3038 {viewType !== "summary" && (
3039 <Card className="mt-6">
3040 <CardHeader>
3041 <CardTitle className="flex items-center gap-2">
3042 <svg
3043 className="w-5 h-5"
3044 fill="none"
3045 stroke="currentColor"
3046 viewBox="0 0 24 24"
3047 >
3048 <path
3049 strokeLinecap="round"
3050 strokeLinejoin="round"
3051 strokeWidth={2}
3052 d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
3053 />
3054 </svg>
3055 {__("Detailed", "yatra")}{" "}
3056 {viewType.charAt(0).toUpperCase() + viewType.slice(1)}{" "}
3057 {__("Report", "yatra")}
3058 </CardTitle>
3059 <CardDescription>
3060 {viewType === "daily" &&
3061 __(
3062 "Daily breakdown of bookings, revenue, and departures",
3063 "yatra",
3064 )}
3065 {viewType === "weekly" &&
3066 __(
3067 "Weekly breakdown of bookings, revenue, and departures",
3068 "yatra",
3069 )}
3070 {viewType === "monthly" &&
3071 __(
3072 "Monthly breakdown of bookings, revenue, and departures",
3073 "yatra",
3074 )}
3075 </CardDescription>
3076 </CardHeader>
3077 <CardContent>
3078 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
3079 {/* Table Section - Takes 2/3 width on large screens */}
3080 <div className="lg:col-span-2">
3081 <DetailedBreakdownTable
3082 viewType={viewType}
3083 dateRange={dateRange}
3084 selectedCategory={selectedCategory}
3085 reportData={reportData}
3086 />
3087 </div>
3088
3089 {/* Chart Section - Takes 1/3 width on large screens */}
3090 <div className="lg:col-span-1">
3091 <DetailedBreakdownChart
3092 viewType={viewType}
3093 dateRange={dateRange}
3094 selectedCategory={selectedCategory}
3095 reportData={reportData}
3096 />
3097 </div>
3098 </div>
3099 </CardContent>
3100 </Card>
3101 )}
3102 </div>
3103 );
3104 };
3105
3106 export default TravelBookingReports;
3107