PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / Reports.tsx

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

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