PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / Dashboard.tsx

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

754 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Dashboard Page
3 * Clean, minimal SaaS-style dashboard with proper alignment
4 */
5
6 import React from "react";
7 import { useQuery } from "@tanstack/react-query";
8 import {
9 MapPin,
10 Calendar,
11 DollarSign,
12 Users,
13 Plane,
14 TrendingUp,
15 Info,
16 Activity,
17 Clock,
18 CheckCircle,
19 // AlertCircle available for future use
20 } from "lucide-react";
21 import { __ } from "../lib/i18n";
22 import { usePermissions } from "../hooks/usePermissions";
23 import { StatCard } from "../components/common/StatCard";
24 import {
25 Card,
26 CardContent,
27 CardHeader,
28 CardTitle,
29 } from "../components/ui/card";
30 import { Button } from "../components/ui/button";
31 import { ConditionalRender } from "../components/ui/conditional-render";
32 import { Skeleton } from "../components/ui/skeleton";
33 import { SimpleBarChart } from "../components/charts/SimpleBarChart";
34 import BookingsOverviewChart from "../components/charts/BookingsOverviewChart";
35 import BookingStatusChart from "../components/charts/BookingStatusChart";
36 import { UpcomingDepartures } from "../components/dashboard/UpcomingDepartures";
37 import { PendingPayments } from "../components/dashboard/PendingPayments";
38 import { RecentBookings } from "../components/dashboard/RecentBookings";
39 import { apiClient } from "../lib/api-client";
40 import { getCurrencySymbol } from "../data/currencies";
41
42 // Skeleton components
43 const SkeletonStatCard = () => (
44 <Card>
45 <CardContent className="pt-6">
46 <div className="flex items-center justify-between">
47 <div className="flex-1">
48 <Skeleton className="h-4 w-24 mb-2" />
49 <Skeleton className="h-8 w-32" />
50 </div>
51 <Skeleton className="w-12 h-12 rounded-full" />
52 </div>
53 </CardContent>
54 </Card>
55 );
56
57 const SkeletonQuickStat = () => (
58 <div className="flex items-center gap-3">
59 <Skeleton className="w-10 h-10 rounded-full" />
60 <div className="flex-1">
61 <Skeleton className="h-3 w-16 mb-1" />
62 <Skeleton className="h-5 w-12" />
63 </div>
64 </div>
65 );
66
67 const Dashboard: React.FC = () => {
68 const { can } = usePermissions();
69
70 const defaultCurrency =
71 (window as any)?.yatraAdmin?.currency ||
72 (window as any)?.yatraBookingData?.currency ||
73 "USD";
74
75 const currencyPosition =
76 (window as any)?.yatraAdmin?.currency_position ||
77 (window as any)?.yatraBookingData?.currency_position ||
78 "left";
79
80 const currencyDecimalsRaw =
81 (window as any)?.yatraAdmin?.currency_decimals ||
82 (window as any)?.yatraBookingData?.currency_decimals;
83 const currencyDecimals = Number.isFinite(Number(currencyDecimalsRaw))
84 ? Number(currencyDecimalsRaw)
85 : 2;
86
87 // Get additional currency settings
88 const thousandSeparator =
89 (window as any)?.yatraAdmin?.thousandSeparator || ",";
90 const decimalSeparator = (window as any)?.yatraAdmin?.decimalSeparator || ".";
91
92 // Comprehensive currency formatting function
93 const formatCurrencyAmount = (amount: number) => {
94 if (!amount || amount === 0)
95 return getCurrencySymbol(defaultCurrency) + "0";
96
97 const numPrice = Number(amount) || 0;
98
99 // Format the number with proper separators
100 const formattedAmount = new Intl.NumberFormat(undefined, {
101 minimumFractionDigits: currencyDecimals,
102 maximumFractionDigits: currencyDecimals,
103 })
104 .format(numPrice)
105 .replace(/,/g, "TEMP_THOUSAND")
106 .replace(/\./g, decimalSeparator)
107 .replace(/TEMP_THOUSAND/g, thousandSeparator);
108
109 // Get currency symbol
110 const currencySymbol = getCurrencySymbol(defaultCurrency);
111
112 // Apply currency position
113 if (currencyPosition === "after" || currencyPosition === "right") {
114 return `${formattedAmount} ${currencySymbol}`;
115 } else {
116 return `${currencySymbol}${formattedAmount}`;
117 }
118 };
119
120 // Fetch booking statistics (totals, revenue, status breakdown, upcoming)
121 const { data: bookingStats, isLoading } = useQuery({
122 queryKey: ["dashboard-booking-stats"],
123 queryFn: async () => {
124 const response = await apiClient.get("/bookings/stats");
125 // REST returns a flat stats object; some endpoints nest under `data` — prefer the flat body.
126 return response?.data ?? response ?? {};
127 },
128 });
129
130 // Fetch total trips count
131 const { data: tripsSummary } = useQuery({
132 queryKey: ["dashboard-trips-total"],
133 queryFn: async () => {
134 const response = await apiClient.get("/trips", {
135 params: { per_page: 1 },
136 });
137 return { total: response?.total ?? 0 };
138 },
139 enabled: can("yatra_view_trips"),
140 });
141
142 // Fetch total customers count
143 const { data: customersSummary } = useQuery({
144 queryKey: ["dashboard-customers-total"],
145 queryFn: async () => {
146 const response = await apiClient.get("/customers", {
147 params: { per_page: 1 },
148 });
149 return { total: response?.total ?? 0 };
150 },
151 enabled: can("yatra_view_bookings"),
152 });
153
154 // Fetch bookings chart data: bookings per month for the last 6 months (including current)
155 const { data: bookingsData } = useQuery({
156 queryKey: ["bookings-chart"],
157 queryFn: async () => {
158 const now = new Date();
159 const pad = (n: number) => String(n).padStart(2, "0");
160
161 // Get a recent batch of bookings and aggregate by created_at month
162 const response = await apiClient.get("/bookings", {
163 params: {
164 per_page: 500,
165 },
166 });
167
168 const items: any[] = response?.data || [];
169 const counts: Record<string, number> = {};
170 const amounts: Record<string, number> = {};
171
172 items.forEach((b) => {
173 // Group by created_at month; fall back to travel_date
174 const dateStr = b.created_at || b.travel_date;
175 const date = dateStr ? new Date(dateStr) : null;
176 if (!date || Number.isNaN(date.getTime())) return;
177 const ym = `${date.getFullYear()}-${pad(date.getMonth() + 1)}`;
178 counts[ym] = (counts[ym] || 0) + 1;
179 const amount = Number(b.total_amount ?? 0) || 0;
180 amounts[ym] = (amounts[ym] || 0) + amount;
181 });
182
183 // Build the last 6 calendar months (including current), oldest first
184 const months: { label: string; count: number; amount: number }[] = [];
185 for (let i = 5; i >= 0; i--) {
186 const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
187 const ym = `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
188 const label = d.toLocaleDateString(undefined, {
189 month: "short",
190 year: "numeric",
191 });
192 months.push({
193 label,
194 count: counts[ym] || 0,
195 amount: amounts[ym] || 0,
196 });
197 }
198
199 return months;
200 },
201 enabled: can("yatra_view_bookings"),
202 });
203
204 // Derive booking status breakdown from bookingStats.by_status
205 // MUST be before any conditional returns (React Hooks rule)
206 const statusData = React.useMemo(() => {
207 const byStatus = (bookingStats as any)?.by_status || {};
208
209 const getCount = (key: string) => {
210 const entry = byStatus[key];
211 if (!entry) return 0;
212 // entry is an object like { status: 'pending', count: '5' }
213 const raw = (entry as any).count;
214 const n = typeof raw === "string" ? parseInt(raw, 10) : Number(raw ?? 0);
215 return Number.isNaN(n) ? 0 : n;
216 };
217
218 return [
219 {
220 label: __("Pending", "yatra"),
221 value: getCount("pending"),
222 color: "#f59e0b",
223 },
224 {
225 label: __("Confirmed", "yatra"),
226 value: getCount("confirmed"),
227 color: "#10b981",
228 },
229 {
230 label: __("Completed", "yatra"),
231 value: getCount("completed"),
232 color: "#3b82f6",
233 },
234 {
235 label: __("Cancelled", "yatra"),
236 value: getCount("cancelled"),
237 color: "#ef4444",
238 },
239 {
240 label: __("Refunded", "yatra"),
241 value: getCount("refunded"),
242 color: "#a855f7",
243 },
244 ];
245 }, [bookingStats]);
246
247 // Fetch popular destinations (aggregate from trips API)
248 const { data: destinationsData } = useQuery({
249 queryKey: ["popular-destinations"],
250 queryFn: async () => {
251 const response = await apiClient.get("/trips", {
252 params: {
253 per_page: 50,
254 },
255 });
256
257 const trips = response?.data || [];
258 const counts: Record<string, number> = {};
259
260 // Each trip may have destinations array (from TripController prepare_item_for_response)
261 trips.forEach((trip: any) => {
262 const destinations = trip.destinations || [];
263 destinations.forEach((dest: any) => {
264 const name = dest?.name || dest?.destination_name || "";
265 if (!name) return;
266 counts[name] = (counts[name] || 0) + 1;
267 });
268 });
269
270 const palette = [
271 "#3b82f6",
272 "#10b981",
273 "#f59e0b",
274 "#ef4444",
275 "#6366f1",
276 "#14b8a6",
277 ];
278 const entries = Object.entries(counts)
279 .sort((a, b) => b[1] - a[1])
280 .slice(0, 6);
281
282 return entries.map(([name, value], index) => ({
283 label: name,
284 value,
285 color: palette[index % palette.length],
286 }));
287 },
288 enabled: can("yatra_view_trips"),
289 });
290
291 // Fetch upcoming departures (real data from /departures)
292 const { data: departures } = useQuery({
293 queryKey: ["upcoming-departures"],
294 queryFn: async () => {
295 const today = new Date();
296 const todayStr = today.toISOString().split("T")[0];
297
298 const response = await apiClient.get("/departures", {
299 params: {
300 status: "upcoming",
301 date_from: todayStr,
302 include_past: false,
303 },
304 });
305
306 const items = response?.data || [];
307
308 // Map API departures into widget-friendly shape
309 return items.map((d: any) => {
310 const tripTitle = d?.trip?.title || d?.trip_title || d?.title || "";
311 const destination =
312 (d?.trip?.destinations && d.trip.destinations[0]?.name) ||
313 d?.destination ||
314 undefined;
315 const totalSpots =
316 d?.total_spots ??
317 d?.capacity ??
318 d?.total_seats ??
319 d?.max_travelers ??
320 0;
321 const availableSpots =
322 d?.available_spots ??
323 d?.available_seats ??
324 d?.remaining_slots ??
325 totalSpots - (d?.bookings_count || 0);
326
327 return {
328 id: d.id,
329 trip_id: d.trip_id || d?.trip?.id,
330 trip_title: tripTitle,
331 departure_date: d.start_date || d.date || d.departure_date,
332 available_spots: Number.isFinite(availableSpots) ? availableSpots : 0,
333 total_spots: Number.isFinite(totalSpots) ? totalSpots : 0,
334 status: d.status || "upcoming",
335 destination,
336 };
337 });
338 },
339 enabled: can("yatra_view_trips"),
340 });
341
342 // Fetch pending payments (real data)
343 const { data: pendingPayments } = useQuery({
344 queryKey: ["pending-payments"],
345 queryFn: async () => {
346 const response = await apiClient.get("/payments", {
347 params: {
348 status: "pending",
349 per_page: 5,
350 },
351 });
352 return response?.data || [];
353 },
354 enabled: can("yatra_view_bookings"),
355 });
356
357 // Fetch recent bookings (real data, latest 5) and map to widget shape
358 const { data: recentBookings } = useQuery({
359 queryKey: ["recent-bookings"],
360 queryFn: async () => {
361 const response = await apiClient.get("/bookings", {
362 params: {
363 per_page: 5,
364 },
365 });
366 const items = response?.data || [];
367
368 return items.map((b: any) => ({
369 id: b.id,
370 booking_id: b.reference || `BK-${b.id}`,
371 customer_name: b.customer_name || b.contact_first_name || "",
372 trip_title: b.trip_title || "",
373 // Prefer created_at, fallback to travel_date, otherwise empty string
374 booking_date: b.created_at || b.travel_date || "",
375 total_amount: b.total_amount ?? 0,
376 status: (b.status || "pending") as
377 | "confirmed"
378 | "pending"
379 | "cancelled"
380 | "completed",
381 }));
382 },
383 enabled: can("yatra_view_bookings"),
384 });
385
386 // Show skeleton while loading - AFTER all hooks
387 if (isLoading) {
388 return (
389 <div className="space-y-6">
390 {/* Skeleton for KPI Cards */}
391 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
392 <SkeletonStatCard />
393 <SkeletonStatCard />
394 <SkeletonStatCard />
395 <SkeletonStatCard />
396 </div>
397
398 {/* Skeleton for Quick Stats */}
399 <Card>
400 <CardHeader>
401 <Skeleton className="h-6 w-32" />
402 </CardHeader>
403 <CardContent>
404 <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
405 <SkeletonQuickStat />
406 <SkeletonQuickStat />
407 <SkeletonQuickStat />
408 <SkeletonQuickStat />
409 </div>
410 </CardContent>
411 </Card>
412
413 {/* Skeleton for Charts */}
414 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
415 <Card>
416 <CardHeader>
417 <Skeleton className="h-6 w-40" />
418 </CardHeader>
419 <CardContent>
420 <Skeleton className="h-64 w-full" />
421 </CardContent>
422 </Card>
423 <Card>
424 <CardHeader>
425 <Skeleton className="h-6 w-40" />
426 </CardHeader>
427 <CardContent>
428 <Skeleton className="h-64 w-full" />
429 </CardContent>
430 </Card>
431 </div>
432
433 {/* Skeleton for Widgets */}
434 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
435 <Card>
436 <CardHeader>
437 <Skeleton className="h-6 w-40" />
438 </CardHeader>
439 <CardContent>
440 <div className="space-y-4">
441 <Skeleton className="h-16 w-full" />
442 <Skeleton className="h-16 w-full" />
443 <Skeleton className="h-16 w-full" />
444 </div>
445 </CardContent>
446 </Card>
447 <Card>
448 <CardHeader>
449 <Skeleton className="h-6 w-40" />
450 </CardHeader>
451 <CardContent>
452 <div className="space-y-4">
453 <Skeleton className="h-16 w-full" />
454 <Skeleton className="h-16 w-full" />
455 <Skeleton className="h-16 w-full" />
456 </div>
457 </CardContent>
458 </Card>
459 <Card>
460 <CardHeader>
461 <Skeleton className="h-6 w-40" />
462 </CardHeader>
463 <CardContent>
464 <div className="space-y-4">
465 <Skeleton className="h-16 w-full" />
466 <Skeleton className="h-16 w-full" />
467 <Skeleton className="h-16 w-full" />
468 </div>
469 </CardContent>
470 </Card>
471 </div>
472 </div>
473 );
474 }
475
476 return (
477 <div className="space-y-3">
478 {/* Enhanced Welcome Message */}
479 <Card className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800">
480 <CardContent className="p-4">
481 <div className="flex items-start justify-between">
482 <div className="flex items-start gap-3">
483 <Info className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
484 <div>
485 <h3 className="font-semibold text-gray-900 dark:text-white mb-1">
486 {__("Welcome to Yatra Dashboard", "yatra")}
487 </h3>
488 <p className="text-sm text-gray-600 dark:text-gray-300">
489 {__(
490 "Real-time insights for your travel booking business. Monitor performance, track bookings, and manage operations efficiently.",
491 "yatra",
492 )}
493 </p>
494 </div>
495 </div>
496 <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
497 <Activity className="w-4 h-4" />
498 <span>{__("Live Data", "yatra")}</span>
499 </div>
500 </div>
501 </CardContent>
502 </Card>
503
504 {/* Key Metrics - Top Row: single horizontal row (scrolls on small screens) */}
505 <div className="flex flex-nowrap gap-3 overflow-x-auto pb-1">
506 <div className="flex-1 min-w-0">
507 <StatCard
508 title={__("Total Trips", "yatra")}
509 value={tripsSummary?.total || 0}
510 icon={MapPin}
511 color="blue"
512 loading={isLoading}
513 />
514 </div>
515
516 <div className="flex-1 min-w-0">
517 <StatCard
518 title={__("Total Bookings", "yatra")}
519 value={bookingStats?.total || 0}
520 icon={Calendar}
521 color="green"
522 loading={isLoading}
523 />
524 </div>
525
526 <div className="flex-1 min-w-0">
527 <StatCard
528 title={__("Booked Revenue", "yatra")}
529 value={formatCurrencyAmount(bookingStats?.total_revenue || 0)}
530 icon={DollarSign}
531 color="purple"
532 loading={isLoading}
533 />
534 </div>
535
536 <div className="flex-1 min-w-0">
537 <StatCard
538 title={__("Collected Revenue", "yatra")}
539 value={formatCurrencyAmount(bookingStats?.total_collected || 0)}
540 icon={DollarSign}
541 color="green"
542 loading={isLoading}
543 />
544 </div>
545
546 <div className="flex-1 min-w-0">
547 <StatCard
548 title={__("Total Customers", "yatra")}
549 value={customersSummary?.total || 0}
550 icon={Users}
551 color="orange"
552 loading={isLoading}
553 />
554 </div>
555
556 <div className="flex-1 min-w-0">
557 <StatCard
558 title={__("Confirmed Bookings", "yatra")}
559 value={(bookingStats as any)?.by_status?.confirmed?.count || 0}
560 icon={CheckCircle}
561 color="green"
562 loading={isLoading}
563 />
564 </div>
565
566 <div className="flex-1 min-w-0">
567 <StatCard
568 title={__("Pending Bookings", "yatra")}
569 value={(bookingStats as any)?.by_status?.pending?.count || 0}
570 icon={Clock}
571 color="orange"
572 loading={isLoading}
573 />
574 </div>
575 </div>
576
577 {/* Main Content Grid - Optimized Layout */}
578 <div className="grid grid-cols-1 lg:grid-cols-12 gap-3">
579 {/* Left Column - Charts (7 columns) */}
580 <div className="lg:col-span-7 space-y-3">
581 {/* Bookings Overview - Full Width */}
582 <ConditionalRender capability="yatra_view_bookings">
583 <Card>
584 <CardHeader>
585 <CardTitle>{__("Bookings Overview", "yatra")}</CardTitle>
586 </CardHeader>
587 <CardContent className="pb-2">
588 <BookingsOverviewChart
589 data={bookingsData || []}
590 currency={defaultCurrency}
591 currencyPosition={currencyPosition}
592 currencyDecimals={currencyDecimals}
593 />
594 </CardContent>
595 </Card>
596 </ConditionalRender>
597
598 {/* Booking Status and Popular Destinations - Side by Side */}
599 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
600 <ConditionalRender capability="yatra_view_bookings">
601 <Card>
602 <CardHeader>
603 <CardTitle>{__("Booking Status", "yatra")}</CardTitle>
604 </CardHeader>
605 <CardContent className="pb-2">
606 <BookingStatusChart data={statusData || []} />
607 </CardContent>
608 </Card>
609 </ConditionalRender>
610
611 <ConditionalRender capability="yatra_view_trips">
612 <Card>
613 <CardHeader>
614 <CardTitle>{__("Popular Destinations", "yatra")}</CardTitle>
615 </CardHeader>
616 <CardContent className="pb-2">
617 <SimpleBarChart
618 data={destinationsData || []}
619 title=""
620 height={180}
621 showValues={true}
622 />
623 </CardContent>
624 </Card>
625 </ConditionalRender>
626 </div>
627
628 {/* Additional Stats - Integrated into main grid */}
629 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
630 <ConditionalRender capability="yatra_view_bookings">
631 <StatCard
632 title={__("Pending Bookings", "yatra")}
633 value={(bookingStats as any)?.by_status?.pending?.count || 0}
634 icon={TrendingUp}
635 color="orange"
636 loading={isLoading}
637 />
638 </ConditionalRender>
639
640 <ConditionalRender capability="yatra_view_trips">
641 <StatCard
642 title={__("Upcoming Departures", "yatra")}
643 value={bookingStats?.upcoming || 0}
644 icon={Plane}
645 color="green"
646 loading={isLoading}
647 />
648 </ConditionalRender>
649 </div>
650
651 {/* Recent Bookings - Fill the gap */}
652 <ConditionalRender capability="yatra_view_bookings">
653 <RecentBookings
654 bookings={recentBookings || []}
655 loading={isLoading}
656 onView={(booking) => {
657 const admin = (window as any)?.yatraAdmin;
658 const baseUrl = admin?.siteUrl || "";
659 window.location.href = `${baseUrl}/wp-admin/admin.php?page=yatra&subpage=bookings&action=view&id=${booking.id}`;
660 }}
661 />
662 </ConditionalRender>
663
664 {/* Quick Actions */}
665 <Card>
666 <CardHeader>
667 <CardTitle className="flex items-center gap-2">
668 <TrendingUp className="w-5 h-5" />
669 {__("Quick Actions", "yatra")}
670 </CardTitle>
671 </CardHeader>
672 <CardContent>
673 <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
674 <Button
675 variant="outline"
676 className="h-auto p-4 flex flex-col items-center gap-2"
677 onClick={() => {
678 const admin = (window as any)?.yatraAdmin;
679 const baseUrl = admin?.siteUrl || "";
680 window.location.href = `${baseUrl}/wp-admin/admin.php?page=yatra&subpage=trips&action=add`;
681 }}
682 >
683 <MapPin className="w-5 h-5" />
684 <span className="text-xs">{__("Add Trip", "yatra")}</span>
685 </Button>
686
687 <Button
688 variant="outline"
689 className="h-auto p-4 flex flex-col items-center gap-2"
690 onClick={() => {
691 const admin = (window as any)?.yatraAdmin;
692 const baseUrl = admin?.siteUrl || "";
693 window.location.href = `${baseUrl}/wp-admin/admin.php?page=yatra&subpage=bookings`;
694 }}
695 >
696 <Calendar className="w-5 h-5" />
697 <span className="text-xs">
698 {__("View Bookings", "yatra")}
699 </span>
700 </Button>
701
702 <Button
703 variant="outline"
704 className="h-auto p-4 flex flex-col items-center gap-2"
705 onClick={() => {
706 const admin = (window as any)?.yatraAdmin;
707 const baseUrl = admin?.siteUrl || "";
708 window.location.href = `${baseUrl}/wp-admin/admin.php?page=yatra&subpage=customers`;
709 }}
710 >
711 <Users className="w-5 h-5" />
712 <span className="text-xs">{__("Customers", "yatra")}</span>
713 </Button>
714
715 <Button
716 variant="outline"
717 className="h-auto p-4 flex flex-col items-center gap-2"
718 onClick={() => {
719 const admin = (window as any)?.yatraAdmin;
720 const baseUrl = admin?.siteUrl || "";
721 window.location.href = `${baseUrl}/wp-admin/admin.php?page=yatra&subpage=reports`;
722 }}
723 >
724 <Activity className="w-5 h-5" />
725 <span className="text-xs">{__("Reports", "yatra")}</span>
726 </Button>
727 </div>
728 </CardContent>
729 </Card>
730 </div>
731
732 {/* Right Column - Widgets (5 columns) */}
733 <div className="lg:col-span-5 space-y-3">
734 <ConditionalRender capability="yatra_view_trips">
735 <UpcomingDepartures
736 departures={departures || []}
737 loading={isLoading}
738 />
739 </ConditionalRender>
740
741 <ConditionalRender capability="yatra_view_bookings">
742 <PendingPayments
743 payments={pendingPayments || []}
744 loading={isLoading}
745 />
746 </ConditionalRender>
747 </div>
748 </div>
749 </div>
750 );
751 };
752
753 export default Dashboard;
754