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 / Travelers.tsx

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

840 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Travelers Listing Page
3 * Display all travelers from bookings with dynamic columns from Form Builder
4 */
5
6 import React, { useState, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 Users,
10 Search,
11 Eye,
12 Mail,
13 Phone,
14 Calendar,
15 MapPin,
16 Trash2,
17 } from "lucide-react";
18 import { __ } from "../lib/i18n";
19 import { apiService } from "../lib/api-client";
20 import { usePermissions } from "../hooks/usePermissions";
21 import { Input } from "../components/ui/input";
22 import { Select } from "../components/ui/select";
23 import { PageHeader } from "../components/common/PageHeader";
24 import { Card, CardContent } from "../components/ui/card";
25 import {
26 Pagination,
27 Table as SharedTable,
28 BulkActionToolbar,
29 } from "../components/shared";
30 import { Skeleton } from "../components/ui/skeleton";
31 import { Badge } from "../components/ui/badge";
32 import { useToast } from "../components/ui/toast";
33 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
34 import { getErrorContext } from "../lib/errors";
35
36 interface FormField {
37 id: string;
38 type: string;
39 label: string;
40 placeholder?: string;
41 required: boolean;
42 enabled: boolean;
43 order: number;
44 width?: string;
45 locked?: boolean;
46 }
47
48 interface FormConfig {
49 traveler_form?: {
50 title: string;
51 description: string;
52 enabled: boolean;
53 fields: FormField[];
54 };
55 }
56
57 interface Traveler {
58 id: string | number;
59 booking_id: number;
60 booking_reference: string;
61 trip_id: number;
62 trip_title: string;
63 travel_date: string;
64 traveler_index: number;
65 is_lead: boolean;
66 // Dynamic fields
67 [key: string]: any;
68 }
69
70 // Fields to exclude from dynamic columns (already shown in fixed columns)
71 const EXCLUDED_DYNAMIC_FIELDS = [
72 "first_name",
73 "last_name",
74 "gender",
75 "nationality",
76 "phone",
77 "_traveller_id",
78 "_is_lead",
79 "_traveller_index",
80 "booking_id",
81 "booking_reference",
82 "trip_id",
83 "trip_title",
84 "travel_date",
85 "traveler_index",
86 "is_lead",
87 "id",
88 ];
89
90 // Country code to full name mapping
91 const COUNTRY_NAMES: Record<string, string> = {
92 AF: "Afghanistan",
93 AL: "Albania",
94 DZ: "Algeria",
95 AD: "Andorra",
96 AO: "Angola",
97 AG: "Antigua and Barbuda",
98 AR: "Argentina",
99 AM: "Armenia",
100 AU: "Australia",
101 AT: "Austria",
102 AZ: "Azerbaijan",
103 BS: "Bahamas",
104 BH: "Bahrain",
105 BD: "Bangladesh",
106 BB: "Barbados",
107 BY: "Belarus",
108 BE: "Belgium",
109 BZ: "Belize",
110 BJ: "Benin",
111 BT: "Bhutan",
112 BO: "Bolivia",
113 BA: "Bosnia and Herzegovina",
114 BW: "Botswana",
115 BR: "Brazil",
116 BN: "Brunei",
117 BG: "Bulgaria",
118 BF: "Burkina Faso",
119 BI: "Burundi",
120 KH: "Cambodia",
121 CM: "Cameroon",
122 CA: "Canada",
123 CV: "Cape Verde",
124 CF: "Central African Republic",
125 TD: "Chad",
126 CL: "Chile",
127 CN: "China",
128 CO: "Colombia",
129 KM: "Comoros",
130 CG: "Congo",
131 CD: "DR Congo",
132 CR: "Costa Rica",
133 CI: "Ivory Coast",
134 HR: "Croatia",
135 CU: "Cuba",
136 CY: "Cyprus",
137 CZ: "Czech Republic",
138 DK: "Denmark",
139 DJ: "Djibouti",
140 DM: "Dominica",
141 DO: "Dominican Republic",
142 EC: "Ecuador",
143 EG: "Egypt",
144 SV: "El Salvador",
145 GQ: "Equatorial Guinea",
146 ER: "Eritrea",
147 EE: "Estonia",
148 SZ: "Eswatini",
149 ET: "Ethiopia",
150 FJ: "Fiji",
151 FI: "Finland",
152 FR: "France",
153 GA: "Gabon",
154 GM: "Gambia",
155 GE: "Georgia",
156 DE: "Germany",
157 GH: "Ghana",
158 GR: "Greece",
159 GD: "Grenada",
160 GT: "Guatemala",
161 GN: "Guinea",
162 GW: "Guinea-Bissau",
163 GY: "Guyana",
164 HT: "Haiti",
165 HN: "Honduras",
166 HU: "Hungary",
167 IS: "Iceland",
168 IN: "India",
169 ID: "Indonesia",
170 IR: "Iran",
171 IQ: "Iraq",
172 IE: "Ireland",
173 IL: "Israel",
174 IT: "Italy",
175 JM: "Jamaica",
176 JP: "Japan",
177 JO: "Jordan",
178 KZ: "Kazakhstan",
179 KE: "Kenya",
180 KI: "Kiribati",
181 KP: "North Korea",
182 KR: "South Korea",
183 KW: "Kuwait",
184 KG: "Kyrgyzstan",
185 LA: "Laos",
186 LV: "Latvia",
187 LB: "Lebanon",
188 LS: "Lesotho",
189 LR: "Liberia",
190 LY: "Libya",
191 LI: "Liechtenstein",
192 LT: "Lithuania",
193 LU: "Luxembourg",
194 MG: "Madagascar",
195 MW: "Malawi",
196 MY: "Malaysia",
197 MV: "Maldives",
198 ML: "Mali",
199 MT: "Malta",
200 MH: "Marshall Islands",
201 MR: "Mauritania",
202 MU: "Mauritius",
203 MX: "Mexico",
204 FM: "Micronesia",
205 MD: "Moldova",
206 MC: "Monaco",
207 MN: "Mongolia",
208 ME: "Montenegro",
209 MA: "Morocco",
210 MZ: "Mozambique",
211 MM: "Myanmar",
212 NA: "Namibia",
213 NR: "Nauru",
214 NP: "Nepal",
215 NL: "Netherlands",
216 NZ: "New Zealand",
217 NI: "Nicaragua",
218 NE: "Niger",
219 NG: "Nigeria",
220 MK: "North Macedonia",
221 NO: "Norway",
222 OM: "Oman",
223 PK: "Pakistan",
224 PW: "Palau",
225 PS: "Palestine",
226 PA: "Panama",
227 PG: "Papua New Guinea",
228 PY: "Paraguay",
229 PE: "Peru",
230 PH: "Philippines",
231 PL: "Poland",
232 PT: "Portugal",
233 QA: "Qatar",
234 RO: "Romania",
235 RU: "Russia",
236 RW: "Rwanda",
237 KN: "Saint Kitts and Nevis",
238 LC: "Saint Lucia",
239 VC: "Saint Vincent and the Grenadines",
240 WS: "Samoa",
241 SM: "San Marino",
242 ST: "Sao Tome and Principe",
243 SA: "Saudi Arabia",
244 SN: "Senegal",
245 RS: "Serbia",
246 SC: "Seychelles",
247 SL: "Sierra Leone",
248 SG: "Singapore",
249 SK: "Slovakia",
250 SI: "Slovenia",
251 SB: "Solomon Islands",
252 SO: "Somalia",
253 ZA: "South Africa",
254 SS: "South Sudan",
255 ES: "Spain",
256 LK: "Sri Lanka",
257 SD: "Sudan",
258 SR: "Suriname",
259 SE: "Sweden",
260 CH: "Switzerland",
261 SY: "Syria",
262 TW: "Taiwan",
263 TJ: "Tajikistan",
264 TZ: "Tanzania",
265 TH: "Thailand",
266 TL: "Timor-Leste",
267 TG: "Togo",
268 TO: "Tonga",
269 TT: "Trinidad and Tobago",
270 TN: "Tunisia",
271 TR: "Turkey",
272 TM: "Turkmenistan",
273 TV: "Tuvalu",
274 UG: "Uganda",
275 UA: "Ukraine",
276 AE: "United Arab Emirates",
277 GB: "United Kingdom",
278 US: "United States",
279 UY: "Uruguay",
280 UZ: "Uzbekistan",
281 VU: "Vanuatu",
282 VA: "Vatican City",
283 VE: "Venezuela",
284 VN: "Vietnam",
285 YE: "Yemen",
286 ZM: "Zambia",
287 ZW: "Zimbabwe",
288 };
289
290 // Helper to get full country name from code
291 const getCountryName = (code: string): string => {
292 if (!code) return "";
293 const upperCode = code.toUpperCase();
294 return COUNTRY_NAMES[upperCode] || code;
295 };
296
297 const Travelers: React.FC = () => {
298 const queryClient = useQueryClient();
299 const { can } = usePermissions();
300 const { showToast } = useToast();
301 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
302 const [travelerToDelete, setTravelerToDelete] = useState<Traveler | null>(
303 null,
304 );
305 const [searchTerm, setSearchTerm] = useState("");
306 const [tripFilter, setTripFilter] = useState("");
307 const [page, setPage] = useState(1);
308 const perPage = 20;
309 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
310 const [bulkAction, setBulkAction] = useState("");
311 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
312 const [visibleColumns, setVisibleColumns] = useState(() => {
313 if (typeof window === "undefined") {
314 return {
315 traveler_info: true,
316 trip: true,
317 travel_date: true,
318 booking_reference: true,
319 };
320 }
321 const saved = window.localStorage.getItem(
322 "yatra-travelers-visible-columns",
323 );
324 return saved
325 ? JSON.parse(saved)
326 : {
327 traveler_info: true,
328 trip: true,
329 travel_date: true,
330 booking_reference: true,
331 };
332 });
333
334 // Fetch form configuration for dynamic columns
335 const { data: formConfigData } = useQuery({
336 queryKey: ["booking-form-config"],
337 queryFn: async () => {
338 const result = await apiService.getSettings("booking_form");
339 return result.success ? result.data?.booking_form_config : null;
340 },
341 });
342
343 // Store available trips from first page load
344 const [availableTrips, setAvailableTrips] = useState<
345 Array<{ id: number; title: string }>
346 >([]);
347
348 // Fetch travelers from API (includes trip info via JOIN, and available_trips in meta on first page)
349 const { data, isLoading, error, refetch } = useQuery({
350 queryKey: ["travelers", searchTerm, tripFilter, page],
351 queryFn: async () => {
352 // Check URL parameter for error simulation (for testing)
353 const urlParams = new URLSearchParams(window.location.search);
354 if (urlParams.get("simulate_error") === "true") {
355 throw new Error(
356 "Simulated API error for testing error UI functionality",
357 );
358 }
359
360 const paramsObj: Record<string, any> = {
361 page: page,
362 per_page: perPage,
363 };
364 if (searchTerm) paramsObj.search = searchTerm;
365 if (tripFilter) paramsObj.trip_id = tripFilter;
366
367 const response = await apiService.getTravelers(paramsObj);
368
369 // Store available trips from meta (returned on first page with no search filter)
370 if (response.meta?.available_trips?.length > 0) {
371 setAvailableTrips(response.meta.available_trips);
372 }
373
374 return response;
375 },
376 enabled: can("yatra_view_bookings"),
377 });
378
379 // Use available trips from state for filter dropdown
380 const tripsData = useMemo(() => ({ data: availableTrips }), [availableTrips]);
381
382 // Get dynamic columns from form config
383 const dynamicColumns = useMemo(() => {
384 const formConfig = formConfigData as FormConfig | null;
385 if (!formConfig?.traveler_form?.fields) {
386 return [];
387 }
388
389 // Get enabled fields from form config, sorted by order
390 const enabledFields = formConfig.traveler_form.fields
391 .filter(
392 (field) => field.enabled && !EXCLUDED_DYNAMIC_FIELDS.includes(field.id),
393 )
394 .sort((a, b) => (a.order || 0) - (b.order || 0));
395
396 return enabledFields.map((field) => ({
397 id: field.id,
398 label: field.label,
399 type: field.type,
400 }));
401 }, [formConfigData]);
402
403 const travelers = data?.data || [];
404 const totalTravelers = data?.meta?.total || 0;
405 const totalPages = Math.ceil(totalTravelers / perPage);
406
407 // Enhanced error handling
408 const errorContext = getErrorContext(error);
409 const apiErrorMessage = (data as any)?.error || (data as any)?.message;
410 const derivedErrorDetails =
411 errorContext.details ||
412 (apiErrorMessage ? String(apiErrorMessage) : undefined) ||
413 (error ? String(error?.message || error) : undefined);
414 const isTravelersError = !!error || !!apiErrorMessage;
415
416 const handleViewBooking = (bookingId: number) => {
417 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=view&id=${bookingId}`;
418 };
419
420 const formatDate = (dateString: string) => {
421 if (!dateString) return "-";
422 try {
423 return new Date(dateString).toLocaleDateString();
424 } catch {
425 return dateString;
426 }
427 };
428
429 // Bulk delete travelers
430 const bulkMutation = useMutation({
431 mutationFn: async ({
432 action,
433 ids,
434 }: {
435 action: string;
436 ids: (string | number)[];
437 }) => {
438 return await apiService.bulkTravelersAction(action, ids);
439 },
440 onSuccess: () => {
441 queryClient.invalidateQueries({ queryKey: ["travelers"] });
442 setSelectedIds([]);
443 setBulkAction("");
444 },
445 });
446
447 const handleBulkApply = () => {
448 if (!bulkAction || selectedIds.length === 0) {
449 return;
450 }
451
452 bulkMutation.mutate({ action: bulkAction, ids: selectedIds });
453 };
454
455 // Handle single traveler delete
456 const handleDelete = (traveler: Traveler) => {
457 setTravelerToDelete(traveler);
458 setDeleteDialogOpen(true);
459 };
460
461 const confirmDelete = async () => {
462 if (!travelerToDelete) return;
463
464 try {
465 await bulkMutation.mutateAsync({
466 action: "delete",
467 ids: [travelerToDelete.id],
468 });
469 showToast(__("Traveler deleted successfully", "yatra"), "success");
470 setDeleteDialogOpen(false);
471 setTravelerToDelete(null);
472 } catch (error: any) {
473 showToast(
474 error?.message || __("Failed to delete traveler", "yatra"),
475 "error",
476 );
477 }
478 };
479
480 // Toggle column visibility
481 const toggleColumn = (columnKey: string) => {
482 const newVisibleColumns = {
483 ...visibleColumns,
484 [columnKey]: !visibleColumns[columnKey],
485 };
486 setVisibleColumns(newVisibleColumns);
487 if (typeof window !== "undefined") {
488 window.localStorage.setItem(
489 "yatra-travelers-visible-columns",
490 JSON.stringify(newVisibleColumns),
491 );
492 }
493 };
494
495 // Column options for the columns dropdown
496 const columnOptions = [
497 {
498 key: "traveler_info",
499 label: __("Traveler", "yatra"),
500 visible: visibleColumns.traveler_info,
501 },
502 { key: "trip", label: __("Trip", "yatra"), visible: visibleColumns.trip },
503 {
504 key: "travel_date",
505 label: __("Travel Date", "yatra"),
506 visible: visibleColumns.travel_date,
507 },
508 {
509 key: "booking_reference",
510 label: __("Booking", "yatra"),
511 visible: visibleColumns.booking_reference,
512 },
513 ];
514
515 // Actions for the 3-dot menu
516 const actions = [
517 {
518 key: "view",
519 label: __("View Booking", "yatra"),
520 icon: <Eye className="w-4 h-4" />,
521 onClick: (traveler: Traveler) => handleViewBooking(traveler.booking_id),
522 },
523 {
524 key: "delete",
525 label: __("Delete", "yatra"),
526 icon: <Trash2 className="w-4 h-4" />,
527 onClick: (traveler: Traveler) => handleDelete(traveler),
528 variant: "destructive" as const,
529 condition: () => can("yatra_delete_bookings"),
530 },
531 ];
532
533 // Format cell value based on field type
534 const formatCellValue = (value: any, fieldType: string): React.ReactNode => {
535 if (value === undefined || value === null || value === "") {
536 return <span className="text-gray-400">-</span>;
537 }
538
539 switch (fieldType) {
540 case "date":
541 return formatDate(value);
542 case "email":
543 return (
544 <div className="flex items-center gap-1 text-sm">
545 <Mail className="w-3 h-3 text-gray-400" />
546 <span className="truncate max-w-[150px]">{value}</span>
547 </div>
548 );
549 case "tel":
550 return (
551 <div className="flex items-center gap-1 text-sm">
552 <Phone className="w-3 h-3 text-gray-400" />
553 <span>{value}</span>
554 </div>
555 );
556 case "country":
557 return (
558 <div className="flex items-center gap-1 text-sm">
559 <MapPin className="w-3 h-3 text-gray-400" />
560 <span>{getCountryName(value)}</span>
561 </div>
562 );
563 default:
564 return <span className="text-sm">{String(value)}</span>;
565 }
566 };
567
568 return (
569 <div className="space-y-3">
570 <PageHeader
571 title={__("Travelers", "yatra")}
572 description={__("View all travelers from bookings", "yatra")}
573 />
574
575 {/* Filters */}
576 <Card>
577 <CardContent className="p-4">
578 <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 items-center">
579 {/* Search Field - Takes most space */}
580 <div className="lg:col-span-8">
581 <div className="relative">
582 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
583 <Input
584 type="text"
585 placeholder={__("Search by name, email, phone...", "yatra")}
586 value={searchTerm}
587 onChange={(e) => {
588 setSearchTerm(e.target.value);
589 setPage(1);
590 }}
591 className="pl-10 w-full"
592 />
593 </div>
594 </div>
595
596 {/* Trip Filter */}
597 <div className="lg:col-span-4">
598 <Select
599 value={tripFilter}
600 onChange={(e) => {
601 setTripFilter(e.target.value);
602 setPage(1);
603 }}
604 className="w-full"
605 >
606 <option value="all">{__("All Trips", "yatra")}</option>
607 {tripsData?.data?.map((trip: any) => (
608 <option key={trip.id} value={trip.id}>
609 {trip.title}
610 </option>
611 ))}
612 </Select>
613 </div>
614 </div>
615 </CardContent>
616 </Card>
617
618 {/* Stats */}
619 <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
620 <Card>
621 <CardContent className="p-4 flex items-center gap-3">
622 <div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
623 <Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
624 </div>
625 <div>
626 <p className="text-sm text-gray-500 dark:text-gray-400">
627 {__("Total Travelers", "yatra")}
628 </p>
629 <p className="text-xl font-semibold text-gray-900 dark:text-white">
630 {isLoading ? <Skeleton className="h-6 w-16" /> : totalTravelers}
631 </p>
632 </div>
633 </CardContent>
634 </Card>
635 </div>
636
637 {/* Bulk actions toolbar */}
638 <BulkActionToolbar
639 selectedIds={selectedIds}
640 bulkAction={bulkAction}
641 setBulkAction={setBulkAction}
642 onApply={handleBulkApply}
643 onClearSelection={() => setSelectedIds([])}
644 statusFilter="all"
645 setStatusFilter={() => {}}
646 statusOptions={[
647 { key: "all", label: __("All", "yatra"), count: totalTravelers },
648 ]}
649 showColumnsDropdown={showColumnsDropdown}
650 setShowColumnsDropdown={setShowColumnsDropdown}
651 columnOptions={columnOptions}
652 onToggleColumn={toggleColumn}
653 bulkMutationPending={bulkMutation.isPending}
654 totalItems={travelers.length}
655 bulkActionOptions={[{ value: "delete", label: __("Delete", "yatra") }]}
656 />
657
658 {/* Travelers Table */}
659 <Card>
660 <CardContent className="p-0 overflow-x-auto">
661 <SharedTable
662 data={travelers}
663 isLoading={isLoading}
664 isError={isTravelersError}
665 errorText={__("Error loading travelers", "yatra")}
666 errorDescription={__(
667 "We couldn't connect to the travelers service. Please refresh or try again shortly.",
668 "yatra",
669 )}
670 errorDetails={derivedErrorDetails}
671 errorRequestInfo={errorContext.requestInfo}
672 onRetry={() => refetch()}
673 skeletonRows={5}
674 emptyText={
675 searchTerm || tripFilter
676 ? __("No travelers found matching your criteria", "yatra")
677 : __("No travelers found", "yatra")
678 }
679 emptyDescription={__(
680 "View and manage travelers collected from your bookings.",
681 "yatra",
682 )}
683 capability="yatra_view_bookings"
684 selectedItemIds={selectedIds}
685 onSelectItem={(id: string | number, checked: boolean) => {
686 if (checked) {
687 setSelectedIds((prev) =>
688 prev.includes(id) ? prev : [...prev, id],
689 );
690 } else {
691 setSelectedIds((prev) =>
692 prev.filter((existingId) => existingId !== id),
693 );
694 }
695 }}
696 onSelectAll={(checked: boolean) => {
697 if (checked) {
698 setSelectedIds(travelers.map((t: Traveler) => t.id));
699 } else {
700 setSelectedIds([]);
701 }
702 }}
703 isAllSelected={
704 travelers.length > 0 && selectedIds.length === travelers.length
705 }
706 getItemId={(traveler: Traveler) => traveler.id}
707 actions={actions}
708 columns={[
709 visibleColumns.traveler_info && {
710 key: "traveler_info",
711 label: __("Traveler", "yatra"),
712 render: (traveler: Traveler) => (
713 <div className="flex items-center gap-3">
714 <div className="w-9 h-9 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-sm font-medium text-gray-600 dark:text-gray-300">
715 {(traveler.first_name?.[0] || "?").toUpperCase()}
716 {(traveler.last_name?.[0] || "").toUpperCase()}
717 </div>
718 <div>
719 <div className="font-medium text-gray-900 dark:text-white flex items-center gap-2">
720 {[traveler.first_name, traveler.last_name]
721 .filter(Boolean)
722 .join(" ") || "N/A"}
723 {traveler.is_lead && (
724 <Badge variant="info" className="text-xs">
725 {__("Lead", "yatra")}
726 </Badge>
727 )}
728 </div>
729 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 space-y-0.5">
730 {traveler.phone && (
731 <div className="flex items-center gap-1">
732 <Phone className="w-3 h-3" />
733 <span>{traveler.phone}</span>
734 </div>
735 )}
736 <div className="flex items-center gap-2">
737 {traveler.gender && (
738 <span className="capitalize">
739 {traveler.gender}
740 </span>
741 )}
742 {traveler.nationality && (
743 <>
744 {traveler.gender && <span>•</span>}
745 <span>
746 {getCountryName(traveler.nationality)}
747 </span>
748 </>
749 )}
750 </div>
751 </div>
752 </div>
753 </div>
754 ),
755 },
756 ...dynamicColumns.map((col) => ({
757 key: `dynamic_${col.id}`,
758 label: col.label,
759 render: (traveler: Traveler) =>
760 formatCellValue(traveler[col.id], col.type),
761 })),
762 visibleColumns.trip && {
763 key: "trip",
764 label: __("Trip", "yatra"),
765 render: (traveler: Traveler) => (
766 <div
767 className="max-w-[200px] truncate"
768 title={traveler.trip_title}
769 >
770 {traveler.trip_title || `Trip #${traveler.trip_id}`}
771 </div>
772 ),
773 },
774 visibleColumns.travel_date && {
775 key: "travel_date",
776 label: __("Travel Date", "yatra"),
777 render: (traveler: Traveler) => (
778 <div className="flex items-center gap-1 text-sm">
779 <Calendar className="w-3 h-3 text-gray-400" />
780 {formatDate(traveler.travel_date)}
781 </div>
782 ),
783 },
784 visibleColumns.booking_reference && {
785 key: "booking_reference",
786 label: __("Booking", "yatra"),
787 render: (traveler: Traveler) => (
788 <a
789 href={`${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=view&id=${traveler.booking_id}`}
790 className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-mono text-sm"
791 >
792 {traveler.booking_reference}
793 </a>
794 ),
795 },
796 ].filter(Boolean)}
797 />
798 </CardContent>
799 </Card>
800
801 {/* Pagination */}
802 {totalTravelers > 0 && (
803 <Pagination
804 currentPage={page}
805 totalPages={totalPages}
806 totalItems={totalTravelers}
807 itemsPerPage={perPage}
808 onPageChange={setPage}
809 itemName={__("travelers", "yatra")}
810 />
811 )}
812
813 {/* Delete Confirmation Dialog */}
814 <ConfirmationDialog
815 isOpen={deleteDialogOpen}
816 onClose={() => {
817 setDeleteDialogOpen(false);
818 setTravelerToDelete(null);
819 }}
820 onConfirm={confirmDelete}
821 title={__("Delete Traveler", "yatra")}
822 message={
823 travelerToDelete
824 ? __(
825 `Are you sure you want to delete ${[travelerToDelete.first_name, travelerToDelete.last_name].filter(Boolean).join(" ")}? This action cannot be undone.`,
826 "yatra",
827 )
828 : __("Are you sure you want to delete this traveler?", "yatra")
829 }
830 confirmText={__("Delete", "yatra")}
831 cancelText={__("Cancel", "yatra")}
832 variant="danger"
833 isLoading={bulkMutation.isPending}
834 />
835 </div>
836 );
837 };
838
839 export default Travelers;
840