PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 / ViewDeparture.tsx

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

981 lines 39.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * YATRA DEPARTURE MANAGEMENT
3 * Comprehensive departure view for travel booking businesses
4 * Complete operational insights for tour operators
5 */
6
7 import React, { useState } from "react";
8 import { useQuery } from "@tanstack/react-query";
9 import { __ } from "../lib/i18n";
10 import { formatDate as formatDateUtil } from "../lib/dateFormat";
11 import { apiClient } from "../lib/api-client";
12 import {
13 Card,
14 CardContent,
15 CardHeader,
16 CardTitle,
17 CardDescription,
18 } from "../components/ui/card";
19 // Button available for future use
20 // import { Button } from '../components/ui/button';
21 import { Badge } from "../components/ui/badge";
22
23 // Skeleton Loading Components
24 const SkeletonMetricCard = () => (
25 <div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
26 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2 w-16 mx-auto"></div>
27 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-20 mx-auto"></div>
28 </div>
29 );
30
31 // SkeletonTabContent available for future use - exported to suppress unused warning
32 export const SkeletonTabContent = () => (
33 <div className="space-y-6">
34 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
35 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
36 <div className="h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-4 w-48"></div>
37 <div className="space-y-3">
38 {[...Array(4)].map((_, i) => (
39 <div key={i} className="flex justify-between">
40 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-24"></div>
41 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-32"></div>
42 </div>
43 ))}
44 </div>
45 </div>
46 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
47 <div className="h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-4 w-48"></div>
48 <div className="space-y-4">
49 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-full"></div>
50 <div className="grid grid-cols-3 gap-4">
51 {[...Array(3)].map((_, i) => (
52 <div key={i} className="text-center">
53 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-1"></div>
54 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
55 </div>
56 ))}
57 </div>
58 </div>
59 </div>
60 </div>
61 </div>
62 );
63
64 // SVG Icons for ViewDeparture
65 const SVGIcons = {
66 Truck: () => (
67 <svg
68 className="w-6 h-6"
69 fill="none"
70 stroke="currentColor"
71 viewBox="0 0 24 24"
72 >
73 <path
74 strokeLinecap="round"
75 strokeLinejoin="round"
76 strokeWidth={2}
77 d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM21 17a2 2 0 11-4 0 2 2 0 014 0z"
78 />
79 <path
80 strokeLinecap="round"
81 strokeLinejoin="round"
82 strokeWidth={2}
83 d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"
84 />
85 </svg>
86 ),
87 Calendar: () => (
88 <svg
89 className="w-6 h-6"
90 fill="none"
91 stroke="currentColor"
92 viewBox="0 0 24 24"
93 >
94 <path
95 strokeLinecap="round"
96 strokeLinejoin="round"
97 strokeWidth={2}
98 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"
99 />
100 </svg>
101 ),
102 Users: () => (
103 <svg
104 className="w-6 h-6"
105 fill="none"
106 stroke="currentColor"
107 viewBox="0 0 24 24"
108 >
109 <path
110 strokeLinecap="round"
111 strokeLinejoin="round"
112 strokeWidth={2}
113 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"
114 />
115 </svg>
116 ),
117 DollarSign: () => (
118 <svg
119 className="w-6 h-6"
120 fill="none"
121 stroke="currentColor"
122 viewBox="0 0 24 24"
123 >
124 <path
125 strokeLinecap="round"
126 strokeLinejoin="round"
127 strokeWidth={2}
128 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"
129 />
130 </svg>
131 ),
132 Activity: () => (
133 <svg
134 className="w-6 h-6"
135 fill="none"
136 stroke="currentColor"
137 viewBox="0 0 24 24"
138 >
139 <path
140 strokeLinecap="round"
141 strokeLinejoin="round"
142 strokeWidth={2}
143 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"
144 />
145 </svg>
146 ),
147 BarChart: () => (
148 <svg
149 className="w-6 h-6"
150 fill="none"
151 stroke="currentColor"
152 viewBox="0 0 24 24"
153 >
154 <path
155 strokeLinecap="round"
156 strokeLinejoin="round"
157 strokeWidth={2}
158 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"
159 />
160 </svg>
161 ),
162 MapPin: () => (
163 <svg
164 className="w-6 h-6"
165 fill="none"
166 stroke="currentColor"
167 viewBox="0 0 24 24"
168 >
169 <path
170 strokeLinecap="round"
171 strokeLinejoin="round"
172 strokeWidth={2}
173 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"
174 />
175 <path
176 strokeLinecap="round"
177 strokeLinejoin="round"
178 strokeWidth={2}
179 d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
180 />
181 </svg>
182 ),
183 FileText: () => (
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 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
195 />
196 </svg>
197 ),
198 Info: () => (
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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
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 const ViewDeparture: React.FC = () => {
231 const [activeTab, setActiveTab] = useState("overview");
232
233 // Get departure ID from URL
234 const params = new URLSearchParams(window.location.search);
235 const idParam = params.get("id");
236 const tripIdParam = params.get("trip_id");
237
238 const id = idParam ? parseInt(idParam, 10) : NaN;
239 const tripId = tripIdParam ? parseInt(tripIdParam, 10) : NaN;
240
241 // Fetch departure data using apiClient
242 const {
243 data: departureData,
244 isLoading,
245 error,
246 } = useQuery({
247 queryKey: ["departure-details", tripId, id],
248 enabled: Number.isFinite(id) && Number.isFinite(tripId),
249 queryFn: async () => {
250 if (!Number.isFinite(id) || !Number.isFinite(tripId)) {
251 throw new Error(__("Invalid departure or trip ID", "yatra"));
252 }
253 const response = await apiClient.get(`/trips/${tripId}/departures/${id}`);
254 const payload = response?.data ?? response;
255 return payload && typeof payload === "object" ? payload : {};
256 },
257 });
258
259 const formatCurrency = (amount: number) => {
260 return new Intl.NumberFormat("en-US", {
261 style: "currency",
262 currency: "USD",
263 }).format(amount);
264 };
265
266 const formatDate = (dateString: string) => {
267 return formatDateUtil(dateString);
268 };
269
270 const formatTime = (timeString: string) => {
271 return new Date(`2000-01-01T${timeString}`).toLocaleTimeString("en-US", {
272 hour: "numeric",
273 minute: "2-digit",
274 hour12: true,
275 });
276 };
277
278 if (!Number.isFinite(id) || !Number.isFinite(tripId)) {
279 return (
280 <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6">
281 <h2 className="text-xl font-bold text-red-800 dark:text-red-400 flex items-center gap-2">
282 <SVGIcons.XCircle />
283 {__("Departure Not Found", "yatra")}
284 </h2>
285 <p className="text-red-600 dark:text-red-400 mt-2">
286 {__(
287 "Missing or invalid departure ID or trip ID in the URL.",
288 "yatra",
289 )}
290 </p>
291 </div>
292 );
293 }
294
295 if (isLoading) {
296 return (
297 <div className="space-y-6">
298 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
299 <div className="animate-pulse">
300 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
301 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3 mb-6"></div>
302 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
303 <div className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
304 <div className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
305 <div className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
306 </div>
307 </div>
308 </div>
309 </div>
310 );
311 }
312
313 if (error) {
314 return (
315 <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6">
316 <h2 className="text-xl font-bold text-red-800 dark:text-red-400 flex items-center gap-2">
317 <SVGIcons.XCircle />
318 {__("Error Loading Departure", "yatra")}
319 </h2>
320 <p className="text-red-600 dark:text-red-400 mt-2">
321 {(error as any)?.message ||
322 __("Unable to load departure details.", "yatra")}
323 </p>
324 </div>
325 );
326 }
327
328 const departure = departureData || {};
329 const trip = departure.trip || {};
330
331 // Calculate metrics
332 const maxCapacity = departure.max_capacity || 0;
333 const bookedCount = departure.booked_count || 0;
334 const availableSpots = maxCapacity - bookedCount;
335 const occupancyRate = maxCapacity > 0 ? (bookedCount / maxCapacity) * 100 : 0;
336
337 // Status styling
338 const getStatusStyle = (status: string) => {
339 switch (status) {
340 case "confirmed":
341 return "bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-400 border-green-200 dark:border-green-800";
342 case "upcoming":
343 return "bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400 border-blue-200 dark:border-blue-800";
344 case "full":
345 return "bg-orange-100 dark:bg-orange-900/20 text-orange-800 dark:text-orange-400 border-orange-200 dark:border-orange-800";
346 case "past":
347 return "bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300 border-gray-200 dark:border-gray-700";
348 case "cancelled":
349 return "bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-400 border-red-200 dark:border-red-800";
350 default:
351 return "bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300 border-gray-200 dark:border-gray-700";
352 }
353 };
354
355 const tabs = [
356 { id: "overview", label: "Overview", icon: "BarChart" },
357 { id: "bookings", label: "Bookings", icon: "Calendar" },
358 { id: "travelers", label: "Travelers", icon: "Users" },
359 { id: "financials", label: "Financials", icon: "DollarSign" },
360 { id: "operations", label: "Operations", icon: "Activity" },
361 ];
362
363 return (
364 <div className="space-y-6">
365 {/* Header */}
366 <Card>
367 <CardHeader>
368 <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
369 <div>
370 <CardTitle className="flex items-center gap-3">
371 <SVGIcons.Truck />
372 {trip.title || __("Departure Details", "yatra")}
373 </CardTitle>
374 <CardDescription>
375 {departure.start_date && formatDate(departure.start_date)}
376 {departure.time &&
377 ` ${__("at", "yatra")} ${formatTime(departure.time)}`}
378 </CardDescription>
379 </div>
380 <div className="flex items-center gap-3">
381 <Badge
382 variant={
383 departure.status === "confirmed"
384 ? "success"
385 : departure.status === "cancelled"
386 ? "error"
387 : "default"
388 }
389 >
390 {departure.status || __("upcoming", "yatra")}
391 </Badge>
392 <span className="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-gray-200 dark:border-gray-700">
393 ID: {departure.id}
394 </span>
395 </div>
396 </div>
397 </CardHeader>
398
399 {/* Key Metrics */}
400 <CardContent>
401 {isLoading ? (
402 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
403 {[...Array(4)].map((_, i) => (
404 <SkeletonMetricCard key={i} />
405 ))}
406 </div>
407 ) : (
408 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
409 <div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
410 <div className="text-3xl font-bold text-blue-600">
411 {bookedCount}
412 </div>
413 <div className="text-sm text-blue-800 mt-1">
414 {__("Bookings", "yatra")}
415 </div>
416 </div>
417
418 <div className="text-center p-4 bg-green-50 rounded-lg border border-green-200">
419 <div className="text-3xl font-bold text-green-600">
420 {maxCapacity}
421 </div>
422 <div className="text-sm text-green-800 mt-1">
423 {__("Total Capacity", "yatra")}
424 </div>
425 </div>
426
427 <div className="text-center p-4 bg-purple-50 rounded-lg border border-purple-200">
428 <div className="text-3xl font-bold text-purple-600">
429 {occupancyRate.toFixed(1)}%
430 </div>
431 <div className="text-sm text-purple-800 mt-1">
432 {__("Occupancy Rate", "yatra")}
433 </div>
434 </div>
435
436 <div className="text-center p-4 bg-orange-50 rounded-lg border border-orange-200">
437 <div className="text-3xl font-bold text-orange-600">
438 {formatCurrency(departure.total_revenue || 0)}
439 </div>
440 <div className="text-sm text-orange-800 mt-1">
441 {__("Total Revenue", "yatra")}
442 </div>
443 </div>
444 </div>
445 )}
446 </CardContent>
447 </Card>
448
449 {/* Tab Navigation */}
450 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
451 <div className="border-b border-gray-200 dark:border-gray-700">
452 <nav className="flex space-x-8 px-6">
453 {tabs.map((tab) => (
454 <button
455 key={tab.id}
456 onClick={() => setActiveTab(tab.id)}
457 className={`py-4 px-3 border-b-2 font-medium text-sm transition-colors flex items-center justify-between min-w-[120px] ${
458 activeTab === tab.id
459 ? "border-blue-500 text-blue-600 dark:text-blue-400"
460 : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
461 }`}
462 >
463 <span className="flex items-center">
464 {React.createElement(
465 SVGIcons[tab.icon as keyof typeof SVGIcons],
466 )}
467 </span>
468 <span className="ml-2">{tab.label}</span>
469 </button>
470 ))}
471 </nav>
472 </div>
473
474 <div className="p-6">
475 {/* Overview Tab */}
476 {activeTab === "overview" && (
477 <div className="space-y-6">
478 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
479 {/* Departure Details */}
480 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
481 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
482 <SVGIcons.Calendar />
483 {__("Departure Information", "yatra")}
484 </h3>
485 <div className="space-y-3">
486 <div className="flex justify-between">
487 <span className="text-gray-600 dark:text-gray-400">
488 {__("Start Date", "yatra")}:
489 </span>
490 <span className="font-medium text-gray-900 dark:text-white">
491 {departure.start_date
492 ? formatDate(departure.start_date)
493 : "--"}
494 </span>
495 </div>
496 <div className="flex justify-between">
497 <span className="text-gray-600 dark:text-gray-400">
498 {__("End Date", "yatra")}:
499 </span>
500 <span className="font-medium text-gray-900 dark:text-white">
501 {departure.end_date
502 ? formatDate(departure.end_date)
503 : "--"}
504 </span>
505 </div>
506 <div className="flex justify-between">
507 <span className="text-gray-600 dark:text-gray-400">
508 {__("Departure Time", "yatra")}:
509 </span>
510 <span className="font-medium text-gray-900 dark:text-white">
511 {departure.time ? formatTime(departure.time) : "--"}
512 </span>
513 </div>
514 <div className="flex justify-between">
515 <span className="text-gray-600 dark:text-gray-400">
516 {__("Duration", "yatra")}:
517 </span>
518 <span className="font-medium text-gray-900 dark:text-white">
519 {trip.duration_days || "--"} days
520 </span>
521 </div>
522 <div className="flex justify-between">
523 <span className="text-gray-600 dark:text-gray-400">
524 {__("Source", "yatra")}:
525 </span>
526 <span className="font-medium text-gray-900 dark:text-white">
527 {departure.source || "Manual"}
528 </span>
529 </div>
530 </div>
531 </div>
532
533 {/* Capacity Management */}
534 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
535 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
536 <SVGIcons.Users />
537 {__("Capacity Management", "yatra")}
538 </h3>
539 <div className="space-y-4">
540 <div>
541 <div className="flex justify-between mb-2">
542 <span className="text-gray-600 dark:text-gray-400">
543 {__("Occupancy", "yatra")}
544 </span>
545 <span className="font-medium text-gray-900 dark:text-white">
546 {occupancyRate.toFixed(1)}%
547 </span>
548 </div>
549 <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
550 <div
551 className="bg-blue-600 h-3 rounded-full transition-all duration-300"
552 style={{ width: `${Math.min(occupancyRate, 100)}%` }}
553 ></div>
554 </div>
555 </div>
556 <div className="grid grid-cols-3 gap-4 text-center">
557 <div>
558 <div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
559 {bookedCount}
560 </div>
561 <div className="text-xs text-gray-600 dark:text-gray-400">
562 {__("Booked", "yatra")}
563 </div>
564 </div>
565 <div>
566 <div className="text-2xl font-bold text-green-600 dark:text-green-400">
567 {availableSpots}
568 </div>
569 <div className="text-xs text-gray-600 dark:text-gray-400">
570 {__("Available", "yatra")}
571 </div>
572 </div>
573 <div>
574 <div className="text-2xl font-bold text-gray-600 dark:text-gray-400">
575 {maxCapacity}
576 </div>
577 <div className="text-xs text-gray-600 dark:text-gray-400">
578 {__("Total", "yatra")}
579 </div>
580 </div>
581 </div>
582 </div>
583 </div>
584 </div>
585
586 {/* Trip Details */}
587 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
588 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
589 <SVGIcons.MapPin />
590 {__("Trip Details", "yatra")}
591 </h3>
592 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
593 <div>
594 <span className="text-gray-600 dark:text-gray-400 text-sm">
595 {__("Starting Location", "yatra")}:
596 </span>
597 <p className="font-medium text-gray-900 dark:text-white">
598 {trip.starting_location || "--"}
599 </p>
600 </div>
601 <div>
602 <span className="text-gray-600 dark:text-gray-400 text-sm">
603 {__("Ending Location", "yatra")}:
604 </span>
605 <p className="font-medium text-gray-900 dark:text-white">
606 {trip.ending_location || "--"}
607 </p>
608 </div>
609 <div>
610 <span className="text-gray-600 dark:text-gray-400 text-sm">
611 {__("Difficulty Level", "yatra")}:
612 </span>
613 <p className="font-medium text-gray-900 dark:text-white">
614 {trip.difficulty_level || "--"}
615 </p>
616 </div>
617 <div>
618 <span className="text-gray-600 dark:text-gray-400 text-sm">
619 {__("Group Type", "yatra")}:
620 </span>
621 <p className="font-medium text-gray-900 dark:text-white">
622 {trip.group_type || "--"}
623 </p>
624 </div>
625 <div>
626 <span className="text-gray-600 dark:text-gray-400 text-sm">
627 {__("Min Travelers", "yatra")}:
628 </span>
629 <p className="font-medium text-gray-900 dark:text-white">
630 {trip.min_travelers || "--"}
631 </p>
632 </div>
633 <div>
634 <span className="text-gray-600 dark:text-gray-400 text-sm">
635 {__("Max Travelers", "yatra")}:
636 </span>
637 <p className="font-medium text-gray-900 dark:text-white">
638 {trip.max_travelers || "--"}
639 </p>
640 </div>
641 {trip.duration && (
642 <div>
643 <span className="text-gray-600 dark:text-gray-400 text-sm">
644 {__("Duration", "yatra")}:
645 </span>
646 <p className="font-medium text-gray-900 dark:text-white">
647 {trip.duration} {__("days", "yatra")}
648 </p>
649 </div>
650 )}
651 {trip.created_at && (
652 <div>
653 <span className="text-gray-600 dark:text-gray-400 text-sm">
654 {__("Trip Created", "yatra")}:
655 </span>
656 <p className="font-medium text-gray-900 dark:text-white">
657 {formatDate(trip.created_at)}
658 </p>
659 </div>
660 )}
661 {trip.price && (
662 <div>
663 <span className="text-gray-600 dark:text-gray-400 text-sm">
664 {__("Base Price", "yatra")}:
665 </span>
666 <p className="font-medium text-gray-900 dark:text-white">
667 {formatCurrency(trip.price)}
668 </p>
669 </div>
670 )}
671 </div>
672 </div>
673
674 {/* Notes */}
675 {departure.notes && (
676 <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
677 <h3 className="text-lg font-semibold text-yellow-800 dark:text-yellow-300 mb-2 flex items-center gap-2">
678 <SVGIcons.FileText />
679 {__("Notes", "yatra")}
680 </h3>
681 <p className="text-yellow-700 dark:text-yellow-400 whitespace-pre-wrap">
682 {departure.notes}
683 </p>
684 </div>
685 )}
686 </div>
687 )}
688
689 {/* Bookings Tab */}
690 {activeTab === "bookings" && (
691 <div className="space-y-4">
692 <div className="flex justify-between items-center">
693 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
694 <SVGIcons.Calendar />
695 {__("Linked Bookings", "yatra")}
696 </h3>
697 <span className="text-sm text-gray-600 dark:text-gray-400">
698 {departure.booking_ids?.length || 0} {__("bookings", "yatra")}
699 </span>
700 </div>
701
702 {departure.booking_ids && departure.booking_ids.length > 0 ? (
703 <div className="grid gap-4">
704 {departure.booking_ids.map((bookingId: number) => (
705 <div
706 key={bookingId}
707 className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4"
708 >
709 <div className="flex justify-between items-start">
710 <div>
711 <h4 className="font-medium text-gray-900 dark:text-white">
712 {__("Booking", "yatra")} #{bookingId}
713 </h4>
714 <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
715 {__("Click to view booking details", "yatra")}
716 </p>
717 </div>
718 <div className="flex items-center gap-2">
719 <span className="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400 text-xs rounded">
720 {__("Active", "yatra")}
721 </span>
722 <a
723 href={`?page=yatra&subpage=bookings&action=view&id=${bookingId}`}
724 className="text-blue-600 hover:text-blue-800 text-sm font-medium"
725 >
726 View →
727 </a>
728 </div>
729 </div>
730 </div>
731 ))}
732 </div>
733 ) : (
734 <div className="text-center py-12 bg-gray-50 dark:bg-gray-800 rounded-lg">
735 <div className="w-16 h-16 mx-auto mb-4 text-gray-400 dark:text-gray-500">
736 <SVGIcons.Calendar />
737 </div>
738 <h3 className="text-lg font-medium text-gray-900 dark:text-white mt-4">
739 {__("No Bookings Yet", "yatra")}
740 </h3>
741 <p className="text-gray-600 dark:text-gray-400 mt-2">
742 {__(
743 "This departure doesn't have any linked bookings.",
744 "yatra",
745 )}
746 </p>
747 </div>
748 )}
749 </div>
750 )}
751
752 {/* Travelers Tab */}
753 {activeTab === "travelers" && (
754 <div className="space-y-4">
755 <div className="flex justify-between items-center">
756 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
757 <SVGIcons.Users />
758 {__("Travelers", "yatra")}
759 </h3>
760 <span className="text-sm text-gray-600 dark:text-gray-400">
761 {departure.travelers?.length || 0} {__("travelers", "yatra")}
762 </span>
763 </div>
764
765 {departure.travelers && departure.travelers.length > 0 ? (
766 <div className="grid gap-4">
767 {departure.travelers.map((traveler: any, index: number) => (
768 <div
769 key={traveler.id || index}
770 className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4"
771 >
772 <div className="flex justify-between items-start">
773 <div>
774 <h4 className="font-medium text-gray-900 dark:text-white">
775 {traveler.first_name} {traveler.last_name}
776 {traveler.is_lead && (
777 <span className="ml-2 px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400 text-xs rounded">
778 {__("Lead", "yatra")}
779 </span>
780 )}
781 </h4>
782 <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
783 {traveler.email && (
784 <span className="block">{traveler.email}</span>
785 )}
786 {traveler.phone && (
787 <span className="block">{traveler.phone}</span>
788 )}
789 </p>
790 </div>
791 <div className="text-right">
792 {traveler.booking_reference && (
793 <a
794 href={`?page=yatra&subpage=bookings&action=view&id=${traveler.booking_id}`}
795 className="text-blue-600 hover:text-blue-800 text-sm font-medium"
796 >
797 Booking #{traveler.booking_reference}
798 </a>
799 )}
800 </div>
801 </div>
802 </div>
803 ))}
804 </div>
805 ) : (
806 <div className="text-center py-12 bg-gray-50 dark:bg-gray-800 rounded-lg">
807 <div className="w-16 h-16 mx-auto mb-4 text-gray-400 dark:text-gray-500">
808 <SVGIcons.Users />
809 </div>
810 <h3 className="text-lg font-medium text-gray-900 dark:text-white mt-4">
811 {__("No Travelers Yet", "yatra")}
812 </h3>
813 <p className="text-gray-600 dark:text-gray-400 mt-2">
814 {__(
815 "No travelers have been assigned to this departure.",
816 "yatra",
817 )}
818 </p>
819 </div>
820 )}
821 </div>
822 )}
823
824 {/* Financials Tab */}
825 {activeTab === "financials" && (
826 <div className="space-y-6">
827 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
828 <SVGIcons.DollarSign />
829 {__("Financial Overview", "yatra")}
830 </h3>
831
832 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
833 <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6 text-center">
834 <div className="text-3xl font-bold text-green-600 dark:text-green-400">
835 {formatCurrency(departure.total_revenue || 0)}
836 </div>
837 <div className="text-sm text-green-800 dark:text-green-300 mt-1">
838 {__("Total Revenue", "yatra")}
839 </div>
840 </div>
841
842 <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6 text-center">
843 <div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
844 {formatCurrency(departure.collected_amount || 0)}
845 </div>
846 <div className="text-sm text-blue-800 dark:text-blue-300 mt-1">
847 {__("Collected", "yatra")}
848 </div>
849 </div>
850
851 <div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-6 text-center">
852 <div className="text-3xl font-bold text-orange-600 dark:text-orange-400">
853 {formatCurrency(
854 (departure.total_revenue || 0) -
855 (departure.collected_amount || 0),
856 )}
857 </div>
858 <div className="text-sm text-orange-800 dark:text-orange-300 mt-1">
859 {__("Pending", "yatra")}
860 </div>
861 </div>
862 </div>
863
864 {departure.price_override && (
865 <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
866 <h4 className="font-medium text-yellow-800 dark:text-yellow-300 flex items-center gap-2">
867 <SVGIcons.Info />
868 {__("Price Override Active", "yatra")}
869 </h4>
870 <p className="text-yellow-700 dark:text-yellow-400 text-sm mt-1">
871 {__("This departure has a custom price of", "yatra")}{" "}
872 {formatCurrency(departure.price_override)}
873 {__("instead of the standard trip price.", "yatra")}
874 </p>
875 </div>
876 )}
877 </div>
878 )}
879
880 {/* Operations Tab */}
881 {activeTab === "operations" && (
882 <div className="space-y-6">
883 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
884 <SVGIcons.Activity />
885 {__("Operational Details", "yatra")}
886 </h3>
887
888 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
889 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
890 <h4 className="font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
891 <SVGIcons.BarChart />
892 {__("Status Information", "yatra")}
893 </h4>
894 <div className="space-y-3">
895 <div className="flex justify-between">
896 <span className="text-gray-600 dark:text-gray-400">
897 {__("Current Status", "yatra")}:
898 </span>
899 <span
900 className={`px-2 py-1 rounded text-sm font-medium ${getStatusStyle(departure.status || "upcoming")}`}
901 >
902 {departure.status || "upcoming"}
903 </span>
904 </div>
905 <div className="flex justify-between">
906 <span className="text-gray-600 dark:text-gray-400">
907 {__("Created", "yatra")}:
908 </span>
909 <span className="font-medium text-gray-900 dark:text-white">
910 {departure.created_at
911 ? formatDate(departure.created_at)
912 : "--"}
913 </span>
914 </div>
915 <div className="flex justify-between">
916 <span className="text-gray-600 dark:text-gray-400">
917 {__("Last Updated", "yatra")}:
918 </span>
919 <span className="font-medium text-gray-900 dark:text-white">
920 {departure.updated_at
921 ? formatDate(departure.updated_at)
922 : "--"}
923 </span>
924 </div>
925 <div className="flex justify-between">
926 <span className="text-gray-600 dark:text-gray-400">
927 {__("Source", "yatra")}:
928 </span>
929 <span className="font-medium text-gray-900 dark:text-white">
930 {departure.source || "Manual"}
931 </span>
932 </div>
933 </div>
934 </div>
935
936 <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
937 <h4 className="font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
938 <SVGIcons.Activity />
939 {__("Performance Metrics", "yatra")}
940 </h4>
941 <div className="space-y-3">
942 <div className="flex justify-between">
943 <span className="text-gray-600 dark:text-gray-400">
944 {__("Booking Efficiency", "yatra")}:
945 </span>
946 <span className="font-medium text-gray-900 dark:text-white">
947 {occupancyRate.toFixed(1)}%
948 </span>
949 </div>
950 <div className="flex justify-between">
951 <span className="text-gray-600 dark:text-gray-400">
952 {__("Revenue per Seat", "yatra")}:
953 </span>
954 <span className="font-medium text-gray-900 dark:text-white">
955 {formatCurrency(
956 (departure.total_revenue || 0) /
957 Math.max(maxCapacity, 1),
958 )}
959 </span>
960 </div>
961 <div className="flex justify-between">
962 <span className="text-gray-600 dark:text-gray-400">
963 {__("Utilization Rate", "yatra")}:
964 </span>
965 <span className="font-medium text-gray-900 dark:text-white">
966 {occupancyRate.toFixed(1)}%
967 </span>
968 </div>
969 </div>
970 </div>
971 </div>
972 </div>
973 )}
974 </div>
975 </div>
976 </div>
977 );
978 };
979
980 export default ViewDeparture;
981