PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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 / BookingForm.tsx

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

1,874 lines 78.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Booking Form Page
3 * Add/Edit Booking form with clean, minimal SaaS-style design
4 */
5
6 import React, { useState, useEffect, useMemo, useRef } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Save,
11 Loader2,
12 Plus,
13 Trash2,
14 Users,
15 ChevronDown,
16 ChevronUp,
17 AlertCircle,
18 Search,
19 X,
20 } from "lucide-react";
21 import { __ } from "../lib/i18n";
22 import {
23 formatDate as formatDateUtil,
24 formatDateForInput,
25 } from "../lib/dateFormat";
26 import { apiService } from "../lib/api-client";
27 import { usePermissions } from "../hooks/usePermissions";
28 import { getCurrencySymbol } from "../data/currencies";
29 import { Button } from "../components/ui/button";
30 import { Input } from "../components/ui/input";
31 import { Select } from "../components/ui/select";
32 import { PageHeader } from "../components/common/PageHeader";
33 import {
34 Card,
35 CardContent,
36 CardHeader,
37 CardTitle,
38 } from "../components/ui/card";
39 import { ConditionalRender } from "../components/ui/conditional-render";
40 import { Skeleton } from "../components/ui/skeleton";
41 import { DatePicker } from "../components/ui/date-picker";
42 import { taxService } from "../services/TaxService";
43
44 // Dynamic traveler data - keys are field IDs from form config
45 interface TravelerData {
46 [key: string]: string;
47 }
48
49 interface FormFieldConfig {
50 id: string;
51 type: string;
52 label: string;
53 placeholder?: string;
54 required: boolean;
55 enabled: boolean;
56 order: number;
57 width?: string;
58 options?: { value: string; label: string }[];
59 section?: string;
60 }
61
62 interface FormSectionConfig {
63 title: string;
64 description?: string;
65 enabled: boolean;
66 fields: FormFieldConfig[];
67 }
68
69 interface BookingFormConfig {
70 contact_form: FormSectionConfig;
71 emergency_contact_form: FormSectionConfig;
72 traveler_form: FormSectionConfig;
73 }
74
75 interface BookingFormData {
76 customer_name: string;
77 customer_email: string;
78 customer_phone: string;
79 customer_country: string;
80 trip_id: string;
81 booking_date: string;
82 travel_date: string;
83 travelers: string;
84 subtotal: string;
85 tax_amount: string;
86 total_amount: string;
87 currency: string;
88 payment_status: string;
89 booking_status: string;
90 payment_method: string;
91 notes: string;
92 }
93
94 // Country list for country fields
95 const countryList = [
96 { code: "US", name: "United States" },
97 { code: "GB", name: "United Kingdom" },
98 { code: "CA", name: "Canada" },
99 { code: "AU", name: "Australia" },
100 { code: "DE", name: "Germany" },
101 { code: "FR", name: "France" },
102 { code: "IN", name: "India" },
103 { code: "NP", name: "Nepal" },
104 { code: "CN", name: "China" },
105 { code: "JP", name: "Japan" },
106 { code: "BR", name: "Brazil" },
107 { code: "MX", name: "Mexico" },
108 { code: "ES", name: "Spain" },
109 { code: "IT", name: "Italy" },
110 { code: "NL", name: "Netherlands" },
111 { code: "CH", name: "Switzerland" },
112 { code: "SE", name: "Sweden" },
113 { code: "NO", name: "Norway" },
114 { code: "DK", name: "Denmark" },
115 { code: "FI", name: "Finland" },
116 { code: "NZ", name: "New Zealand" },
117 { code: "SG", name: "Singapore" },
118 { code: "AE", name: "United Arab Emirates" },
119 { code: "TH", name: "Thailand" },
120 { code: "MY", name: "Malaysia" },
121 { code: "ID", name: "Indonesia" },
122 { code: "PH", name: "Philippines" },
123 { code: "VN", name: "Vietnam" },
124 { code: "KR", name: "South Korea" },
125 { code: "ZA", name: "South Africa" },
126 ];
127
128 // Normalize ISO/date-like string to formatted date using shared date library
129 const normalizeDateInput = (value?: string | null) => {
130 if (!value) return "";
131 // DatePicker strictly requires YYYY-MM-DD; use formatDateForInput to ensure correct format
132 return formatDateForInput(value) || value;
133 };
134
135 const BookingForm: React.FC = () => {
136 const queryClient = useQueryClient();
137 const { can } = usePermissions();
138
139 // Get global currency settings
140 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
141 const currencySymbol = getCurrencySymbol(globalCurrency);
142
143 const [formData, setFormData] = useState<BookingFormData>({
144 customer_name: "",
145 customer_email: "",
146 customer_phone: "",
147 customer_country: "",
148 trip_id: "",
149 booking_date: normalizeDateInput(new Date().toISOString()),
150 travel_date: "",
151 travelers: "1",
152 subtotal: "",
153 tax_amount: "",
154 total_amount: "",
155 currency: globalCurrency,
156 payment_status: "pending",
157 booking_status: "pending",
158 payment_method: "",
159 notes: "",
160 });
161 const [travelersData, setTravelersData] = useState<TravelerData[]>([{}]);
162 const [expandedTravelers, setExpandedTravelers] = useState<number[]>([0]);
163 const [emergencyContactData, setEmergencyContactData] = useState<
164 Record<string, string>
165 >({});
166 const [errors, setErrors] = useState<Record<string, string>>({});
167 const [isSubmitting, setIsSubmitting] = useState(false);
168
169 // Trip search state
170 const [tripSearchQuery, setTripSearchQuery] = useState("");
171 const [isTripDropdownOpen, setIsTripDropdownOpen] = useState(false);
172 const tripDropdownRef = useRef<HTMLDivElement>(null);
173
174 // Close dropdown when clicking outside
175 useEffect(() => {
176 const handleClickOutside = (event: MouseEvent) => {
177 if (
178 tripDropdownRef.current &&
179 !tripDropdownRef.current.contains(event.target as Node)
180 ) {
181 setIsTripDropdownOpen(false);
182 }
183 };
184 document.addEventListener("mousedown", handleClickOutside);
185 return () => document.removeEventListener("mousedown", handleClickOutside);
186 }, []);
187
188 // Fetch booking form configuration
189 const { data: formConfig } = useQuery<BookingFormConfig>({
190 queryKey: ["booking-form-config"],
191 queryFn: async () => {
192 const response = await apiService.getSettings();
193 return (
194 response?.data?.booking_form_config ||
195 response?.booking_form_config ||
196 null
197 );
198 },
199 });
200
201 // Get enabled traveler fields from config
202 const travelerFields = useMemo(() => {
203 if (!formConfig?.traveler_form?.fields) return [];
204 return formConfig.traveler_form.fields
205 .filter((field) => field.enabled)
206 .sort((a, b) => a.order - b.order);
207 }, [formConfig]);
208
209 // Get enabled emergency contact fields from config
210 const emergencyContactFields = useMemo(() => {
211 if (!formConfig?.emergency_contact_form?.fields) return [];
212 return formConfig.emergency_contact_form.fields
213 .filter((field) => field.enabled)
214 .sort((a, b) => a.order - b.order);
215 }, [formConfig]);
216
217 // Create empty traveler based on enabled fields
218 const createEmptyTraveler = (): TravelerData => {
219 const empty: TravelerData = {};
220 travelerFields.forEach((field) => {
221 empty[field.id] = "";
222 });
223 return empty;
224 };
225
226 // Get action and id from URL
227 const action = useMemo(() => {
228 const params = new URLSearchParams(window.location.search);
229 return params.get("action") || "create";
230 }, []);
231
232 const bookingId = useMemo(() => {
233 const params = new URLSearchParams(window.location.search);
234 return params.get("id") ? parseInt(params.get("id") || "0") : null;
235 }, []);
236
237 const isEditMode = action === "edit" && bookingId !== null;
238
239 // Fetch payment gateways for dropdown
240 const { data: gatewaysData } = useQuery({
241 queryKey: ["payment-gateways"],
242 queryFn: async () => {
243 const result = await apiService.getPaymentGateways();
244 if (result?.success) {
245 return {
246 data: result.data
247 .filter((gw: any) => gw.enabled)
248 .map((gw: any) => ({
249 id: gw.id,
250 title: gw.title || gw.name,
251 })),
252 };
253 }
254 return { data: [] };
255 },
256 });
257
258 // Fetch trips for dropdown - get all trips regardless of status
259 const { data: tripsData, isLoading: isLoadingTrips } = useQuery({
260 queryKey: ["trips-list-all"],
261 queryFn: async () => {
262 // Fetch all trips - try without status filter first, then with status=all
263 return await apiService.getTrips({ per_page: 500 });
264 },
265 // Always fetch trips when user can manage bookings
266 enabled: can("yatra_view_bookings") || can("yatra_view_trips"),
267 retry: 1,
268 });
269
270 // Fetch booking data if editing
271 const { data: bookingData, isLoading: isLoadingBooking } = useQuery({
272 queryKey: ["booking", bookingId],
273 queryFn: async () => {
274 if (!bookingId) return null;
275 const result = await apiService.getBooking(bookingId);
276
277 if (!result) {
278 throw new Error("Failed to fetch booking");
279 }
280
281 // Handle both wrapped { success, data } and direct data response formats
282 const booking = (result as any)?.data ?? result;
283
284 if (booking && booking.id) {
285 // Use customer_name if available, otherwise construct from first/last name
286 const contact = booking.contact || {};
287 const firstName =
288 booking.contact_first_name || contact.first_name || "";
289 const lastName = booking.contact_last_name || contact.last_name || "";
290 const customerName =
291 booking.customer_name || `${firstName} ${lastName}`.trim();
292
293 // Parse travelers data - dynamic fields based on stored data
294 let travelers: TravelerData[] = [];
295 if (booking.travelers && Array.isArray(booking.travelers)) {
296 // Map all stored fields dynamically (keys are field IDs from form builder)
297 // The API returns travelers with a nested 'fields' object containing the form data
298 travelers = booking.travelers.map((t: any) => {
299 const traveler: TravelerData = {};
300
301 // Check if fields are nested in a 'fields' property (from TravellerRepository)
302 const fieldsData = t.fields || t;
303
304 if (fieldsData && typeof fieldsData === "object") {
305 Object.entries(fieldsData).forEach(([key, value]) => {
306 // Skip internal fields like id, booking_id, traveller_index, etc.
307 if (
308 ![
309 "id",
310 "booking_id",
311 "traveller_index",
312 "is_lead",
313 "created_at",
314 "updated_at",
315 ].includes(key)
316 ) {
317 traveler[key] = String(value || "");
318 }
319 });
320 }
321
322 // Also copy is_lead if present
323 if (t.is_lead !== undefined) {
324 traveler["is_lead"] = String(t.is_lead);
325 }
326
327 return traveler;
328 });
329 }
330
331 // Ensure at least one traveler exists
332 if (travelers.length === 0) {
333 travelers = [{}]; // Empty object - fields will be populated from form config
334 }
335
336 // Parse emergency contact data
337 let emergencyContact: Record<string, string> = {};
338 if (
339 booking.emergency_contact &&
340 typeof booking.emergency_contact === "object"
341 ) {
342 Object.entries(booking.emergency_contact).forEach(([key, value]) => {
343 emergencyContact[key] = String(value || "");
344 });
345 } else if (
346 contact.emergency_name ||
347 contact.emergency_phone ||
348 contact.emergency_relationship
349 ) {
350 emergencyContact = {
351 name: String(contact.emergency_name || ""),
352 phone: String(contact.emergency_phone || ""),
353 relationship: String(contact.emergency_relationship || ""),
354 };
355 }
356
357 const mappedData = {
358 id: booking.id,
359 customer_name: customerName,
360 customer_email:
361 booking.customer_email ||
362 contact.email ||
363 booking.contact_email ||
364 "",
365 customer_phone:
366 booking.customer_phone ||
367 contact.phone ||
368 booking.contact_phone ||
369 "",
370 customer_country: (booking as any).contact_country || "",
371 trip_id: String(booking.trip_id || ""),
372 trip_title: booking.trip_title || "",
373 booking_date: normalizeDateInput(
374 booking.booking_date ??
375 booking.created_at ??
376 new Date().toISOString(),
377 ),
378 travel_date: normalizeDateInput(booking.travel_date ?? ""),
379 travelers: String(booking.travelers || "1"),
380 travelers_data: travelers, // Add the correctly parsed travelers data
381 subtotal: String(
382 (booking as any).subtotal || booking.total_amount || "",
383 ),
384 tax_amount: String((booking as any).tax_amount || "0"),
385 total_amount: String(booking.total_amount || ""),
386 currency: booking.currency || "USD",
387 payment_status: booking.payment_status || "pending",
388 booking_status: booking.booking_status || "pending",
389 payment_method: booking.payment_method || "",
390 notes: booking.notes || "",
391 emergency_contact: emergencyContact,
392 };
393
394 return mappedData;
395 }
396 return null;
397 },
398 enabled: isEditMode && can("yatra_view_bookings"),
399 });
400
401 // Track if we've loaded booking data to prevent auto-calculation from overwriting
402 const [isDataLoaded, setIsDataLoaded] = useState(false);
403
404 // Load booking data into form when editing
405 useEffect(() => {
406 if (bookingData && isEditMode) {
407 setFormData({
408 customer_name: bookingData.customer_name || "",
409 customer_email: bookingData.customer_email || "",
410 customer_phone: bookingData.customer_phone || "",
411 customer_country:
412 (bookingData as any).contact_country ||
413 (bookingData as any).customer_country ||
414 "",
415 trip_id: String(bookingData.trip_id || ""),
416 booking_date:
417 bookingData.booking_date || new Date().toISOString().split("T")[0],
418 travel_date: bookingData.travel_date || "",
419 travelers: String(
420 (bookingData as any).travelers_count ||
421 bookingData.travelers_data?.length ||
422 "1",
423 ),
424 subtotal: String(
425 (bookingData as any).subtotal || bookingData.total_amount || "",
426 ),
427 tax_amount: String((bookingData as any).tax_amount || "0"),
428 total_amount: String(bookingData.total_amount || ""),
429 currency: bookingData.currency || "USD",
430 payment_status: bookingData.payment_status || "pending",
431 booking_status: bookingData.booking_status || "pending",
432 payment_method: bookingData.payment_method || "",
433 notes: (bookingData as any).special_requests || bookingData.notes || "",
434 });
435
436 // Set travelers data - use the correctly parsed travelers_data
437 if (
438 bookingData.travelers_data &&
439 Array.isArray(bookingData.travelers_data) &&
440 bookingData.travelers_data.length > 0
441 ) {
442 setTravelersData(bookingData.travelers_data);
443 // Expand first traveler by default
444 setExpandedTravelers([0]);
445 } else {
446 // Ensure at least one empty traveler
447 setTravelersData([{}]);
448 setExpandedTravelers([0]);
449 }
450
451 // Set emergency contact data
452 if (bookingData.emergency_contact) {
453 setEmergencyContactData(bookingData.emergency_contact);
454 }
455
456 setIsDataLoaded(true);
457 }
458 }, [bookingData, isEditMode]);
459
460 // Auto-calculate pricing when trip or travelers change
461 useEffect(() => {
462 // Don't auto-calculate if we're in edit mode - user can manually update if needed
463 if (isEditMode) return;
464
465 if (formData.trip_id && formData.travelers) {
466 const selectedTrip = tripsData?.data?.find(
467 (trip: any) => String(trip.id) === formData.trip_id,
468 );
469 if (selectedTrip) {
470 const subtotal = selectedTrip.price * parseInt(formData.travelers);
471
472 // Calculate tax
473 const taxCalculation = taxService.calculateBookingTax(
474 subtotal,
475 formData.customer_country,
476 );
477
478 setFormData((prev) => ({
479 ...prev,
480 subtotal: taxCalculation.subtotal.toString(),
481 tax_amount: taxCalculation.tax_amount.toString(),
482 total_amount: taxCalculation.total_amount.toString(),
483 }));
484 }
485 }
486 }, [
487 formData.trip_id,
488 formData.travelers,
489 formData.customer_country,
490 tripsData?.data,
491 isEditMode,
492 ]);
493
494 // Load tax settings on mount
495 useEffect(() => {
496 taxService.loadTaxSettings();
497 }, []);
498
499 const handleFieldChange = (field: keyof BookingFormData, value: string) => {
500 setFormData((prev) => ({ ...prev, [field]: value }));
501 if (errors[field]) {
502 setErrors((prev) => ({ ...prev, [field]: "" }));
503 }
504 };
505
506 // Emergency contact change handler
507 const handleEmergencyContactChange = (field: string, value: string) => {
508 setEmergencyContactData((prev) => ({ ...prev, [field]: value }));
509 };
510
511 // Traveler management functions
512 const handleTravelerChange = (
513 index: number,
514 field: keyof TravelerData,
515 value: string,
516 ) => {
517 setTravelersData((prev) => {
518 const updated = [...prev];
519 updated[index] = { ...updated[index], [field]: value };
520 return updated;
521 });
522 };
523
524 const addTraveler = () => {
525 const newTraveler = createEmptyTraveler();
526 setTravelersData((prev) => [...prev, newTraveler]);
527 setFormData((prev) => ({
528 ...prev,
529 travelers: String(travelersData.length + 1),
530 }));
531 setExpandedTravelers((prev) => [...prev, travelersData.length]);
532 };
533
534 const removeTraveler = (index: number) => {
535 if (travelersData.length <= 1) return;
536 setTravelersData((prev) => prev.filter((_, i) => i !== index));
537 setFormData((prev) => ({
538 ...prev,
539 travelers: String(travelersData.length - 1),
540 }));
541 setExpandedTravelers((prev) =>
542 prev.filter((i) => i !== index).map((i) => (i > index ? i - 1 : i)),
543 );
544 };
545
546 const toggleTravelerExpanded = (index: number) => {
547 setExpandedTravelers((prev) =>
548 prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index],
549 );
550 };
551
552 const validateForm = (): boolean => {
553 const newErrors: Record<string, string> = {};
554
555 if (!formData.customer_name.trim()) {
556 newErrors.customer_name = __("Customer name is required", "yatra");
557 }
558
559 if (!formData.customer_email.trim()) {
560 newErrors.customer_email = __("Customer email is required", "yatra");
561 } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.customer_email)) {
562 newErrors.customer_email = __("Invalid email address", "yatra");
563 }
564
565 if (!formData.trip_id) {
566 newErrors.trip_id = __("Trip is required", "yatra");
567 }
568
569 if (!formData.booking_date) {
570 newErrors.booking_date = __("Booking date is required", "yatra");
571 }
572
573 if (!formData.travel_date) {
574 newErrors.travel_date = __("Travel date is required", "yatra");
575 }
576
577 if (!formData.travelers || parseInt(formData.travelers) < 1) {
578 newErrors.travelers = __(
579 "Number of travelers must be at least 1",
580 "yatra",
581 );
582 }
583
584 if (!formData.total_amount || parseFloat(formData.total_amount) <= 0) {
585 newErrors.total_amount = __(
586 "Total amount must be greater than 0",
587 "yatra",
588 );
589 }
590
591 setErrors(newErrors);
592 return Object.keys(newErrors).length === 0;
593 };
594
595 // Create/Update mutation
596 const saveMutation = useMutation({
597 mutationFn: async (data: BookingFormData) => {
598 // Split customer name into first and last
599 const nameParts = data.customer_name.trim().split(" ");
600 const firstName = nameParts[0] || "";
601 const lastName = nameParts.slice(1).join(" ") || "";
602
603 const payload: any = {
604 contact_first_name: firstName,
605 contact_last_name: lastName,
606 contact_email: data.customer_email.trim(),
607 contact_phone: data.customer_phone.trim(),
608 trip_id: parseInt(data.trip_id),
609 travel_date: data.travel_date,
610 travelers_count: travelersData.length,
611 total_amount: parseFloat(data.total_amount),
612 payment_status: data.payment_status,
613 status: data.booking_status,
614 payment_gateway: data.payment_method.trim(),
615 special_requests: data.notes.trim(),
616 // Include travelers data - use 'travelers' key as expected by BookingService.updateBooking
617 travelers: travelersData,
618 // Include emergency contact data
619 emergency_contact: emergencyContactData,
620 };
621
622 return isEditMode
623 ? await apiService.updateBooking(bookingId!, payload)
624 : await apiService.createBooking(payload);
625 },
626 onSuccess: () => {
627 queryClient.invalidateQueries({ queryKey: ["bookings"] });
628 // Redirect to bookings list
629 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings`;
630 },
631 onError: (error: any) => {
632 const errorMessage =
633 error?.message ||
634 __("An error occurred while saving the booking", "yatra");
635 setErrors({ submit: errorMessage });
636 setIsSubmitting(false);
637 },
638 });
639
640 const handleSubmit = async (e: React.FormEvent) => {
641 e.preventDefault();
642
643 if (!validateForm()) {
644 return;
645 }
646
647 setIsSubmitting(true);
648 setErrors({});
649 saveMutation.mutate(formData);
650 };
651
652 const handleCancel = () => {
653 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings`;
654 };
655
656 if (isEditMode && isLoadingBooking) {
657 return (
658 <div className="space-y-3">
659 {/* Header Skeleton */}
660 <div className="flex items-center justify-between">
661 <div className="space-y-2">
662 <Skeleton className="h-8 w-48" />
663 <Skeleton className="h-4 w-64" />
664 </div>
665 <Skeleton className="h-10 w-24" />
666 </div>
667
668 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
669 {/* Main Form Skeleton */}
670 <div className="lg:col-span-2 space-y-3">
671 {/* Customer Information Card */}
672 <Card>
673 <CardHeader className="pb-2">
674 <Skeleton className="h-5 w-40" />
675 </CardHeader>
676 <CardContent className="space-y-3">
677 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
678 <div className="space-y-1.5">
679 <Skeleton className="h-4 w-24" />
680 <Skeleton className="h-10 w-full" />
681 </div>
682 <div className="space-y-1.5">
683 <Skeleton className="h-4 w-24" />
684 <Skeleton className="h-10 w-full" />
685 </div>
686 </div>
687 <div className="space-y-1.5">
688 <Skeleton className="h-4 w-28" />
689 <Skeleton className="h-10 w-full" />
690 </div>
691 </CardContent>
692 </Card>
693
694 {/* Booking Details Card */}
695 <Card>
696 <CardHeader className="pb-2">
697 <Skeleton className="h-5 w-32" />
698 </CardHeader>
699 <CardContent className="space-y-3">
700 <div className="space-y-1.5">
701 <Skeleton className="h-4 w-16" />
702 <Skeleton className="h-10 w-full" />
703 </div>
704 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
705 <div className="space-y-1.5">
706 <Skeleton className="h-4 w-24" />
707 <Skeleton className="h-10 w-full" />
708 </div>
709 <div className="space-y-1.5">
710 <Skeleton className="h-4 w-20" />
711 <Skeleton className="h-10 w-full" />
712 </div>
713 </div>
714 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
715 <div className="space-y-1.5">
716 <Skeleton className="h-4 w-32" />
717 <Skeleton className="h-10 w-full" />
718 </div>
719 <div className="space-y-1.5">
720 <Skeleton className="h-4 w-24" />
721 <Skeleton className="h-10 w-full" />
722 </div>
723 </div>
724 <div className="space-y-1.5">
725 <Skeleton className="h-4 w-16" />
726 <Skeleton className="h-20 w-full" />
727 </div>
728 </CardContent>
729 </Card>
730 </div>
731
732 {/* Sidebar Skeleton */}
733 <div className="space-y-3">
734 <Card>
735 <CardHeader className="pb-2">
736 <Skeleton className="h-5 w-32" />
737 </CardHeader>
738 <CardContent className="space-y-3">
739 <div className="space-y-1.5">
740 <Skeleton className="h-4 w-24" />
741 <Skeleton className="h-10 w-full" />
742 </div>
743 <div className="space-y-1.5">
744 <Skeleton className="h-4 w-28" />
745 <Skeleton className="h-10 w-full" />
746 </div>
747 <div className="space-y-1.5">
748 <Skeleton className="h-4 w-28" />
749 <Skeleton className="h-10 w-full" />
750 </div>
751 </CardContent>
752 </Card>
753
754 <Card>
755 <CardContent className="p-3">
756 <div className="flex gap-2">
757 <Skeleton className="h-10 flex-1" />
758 <Skeleton className="h-10 w-20" />
759 </div>
760 </CardContent>
761 </Card>
762 </div>
763 </div>
764 </div>
765 );
766 }
767
768 return (
769 <div className="space-y-3">
770 <PageHeader
771 title={
772 isEditMode
773 ? __("Edit Booking", "yatra")
774 : __("Create Booking", "yatra")
775 }
776 description={
777 isEditMode
778 ? __("Update booking information", "yatra")
779 : __("Create a new customer booking", "yatra")
780 }
781 actions={
782 <Button
783 variant="outline"
784 onClick={handleCancel}
785 className="flex items-center gap-2"
786 >
787 <ArrowLeft className="w-4 h-4" />
788 {__("Back", "yatra")}
789 </Button>
790 }
791 />
792
793 <ConditionalRender capability="yatra_edit_bookings">
794 <form onSubmit={handleSubmit}>
795 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
796 {/* Main Form Fields */}
797 <div className="lg:col-span-2 space-y-3">
798 {/* Customer Information */}
799 <Card>
800 <CardHeader className="pb-2">
801 <CardTitle className="text-base">
802 {__("Customer Information", "yatra")}
803 </CardTitle>
804 </CardHeader>
805 <CardContent className="space-y-3">
806 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
807 {/* Customer Name */}
808 <div>
809 <label
810 htmlFor="customer_name"
811 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
812 >
813 {__("Customer Name", "yatra")}{" "}
814 <span className="text-red-500">*</span>
815 </label>
816 <Input
817 id="customer_name"
818 type="text"
819 value={formData.customer_name}
820 onChange={(e) =>
821 handleFieldChange("customer_name", e.target.value)
822 }
823 placeholder={__("Enter customer name", "yatra")}
824 className={errors.customer_name ? "border-red-500" : ""}
825 required
826 />
827 {errors.customer_name && (
828 <p className="mt-1 text-sm text-red-500">
829 {errors.customer_name}
830 </p>
831 )}
832 </div>
833
834 {/* Customer Email */}
835 <div>
836 <label
837 htmlFor="customer_email"
838 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
839 >
840 {__("Customer Email", "yatra")}{" "}
841 <span className="text-red-500">*</span>
842 </label>
843 <Input
844 id="customer_email"
845 type="email"
846 value={formData.customer_email}
847 onChange={(e) =>
848 handleFieldChange("customer_email", e.target.value)
849 }
850 placeholder={__("[email protected]", "yatra")}
851 className={
852 errors.customer_email ? "border-red-500" : ""
853 }
854 required
855 />
856 {errors.customer_email && (
857 <p className="mt-1 text-sm text-red-500">
858 {errors.customer_email}
859 </p>
860 )}
861 </div>
862 </div>
863
864 {/* Customer Phone */}
865 <div>
866 <label
867 htmlFor="customer_phone"
868 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
869 >
870 {__("Customer Phone", "yatra")}
871 </label>
872 <Input
873 id="customer_phone"
874 type="tel"
875 value={formData.customer_phone}
876 onChange={(e) =>
877 handleFieldChange("customer_phone", e.target.value)
878 }
879 placeholder={__("+1234567890", "yatra")}
880 />
881 </div>
882
883 {/* Customer Country */}
884 <div>
885 <label
886 htmlFor="customer_country"
887 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
888 >
889 {__("Country", "yatra")}
890 </label>
891 <select
892 id="customer_country"
893 value={formData.customer_country}
894 onChange={(e) =>
895 handleFieldChange("customer_country", e.target.value)
896 }
897 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
898 >
899 <option value="">{__("Select country", "yatra")}</option>
900 {countryList.map((country) => (
901 <option key={country.code} value={country.code}>
902 {country.name}
903 </option>
904 ))}
905 </select>
906 </div>
907 </CardContent>
908 </Card>
909
910 {/* Booking Details */}
911 <Card>
912 <CardHeader className="pb-2">
913 <CardTitle className="text-base">
914 {__("Booking Details", "yatra")}
915 </CardTitle>
916 </CardHeader>
917 <CardContent className="space-y-3">
918 {/* Trip Selection - Searchable Dropdown */}
919 <div ref={tripDropdownRef} className="relative">
920 <label
921 htmlFor="trip_search"
922 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
923 >
924 {__("Trip", "yatra")}{" "}
925 <span className="text-red-500">*</span>
926 </label>
927 <div className="relative">
928 <div
929 className={`flex items-center border rounded-md bg-white dark:bg-gray-800 cursor-pointer ${errors.trip_id ? "border-red-500" : "border-gray-300 dark:border-gray-600"} ${isTripDropdownOpen ? "ring-2 ring-blue-500" : ""}`}
930 onClick={() =>
931 setIsTripDropdownOpen(!isTripDropdownOpen)
932 }
933 >
934 <div className="flex-1 px-3 py-2 text-sm">
935 {formData.trip_id ? (
936 <span className="text-gray-900 dark:text-white">
937 {tripsData?.data?.find(
938 (t: any) => String(t.id) === formData.trip_id,
939 )?.title ||
940 bookingData?.trip_title ||
941 `Trip ID: ${formData.trip_id}`}
942 </span>
943 ) : (
944 <span className="text-gray-500">
945 {isLoadingTrips
946 ? __("Loading trips...", "yatra")
947 : __("Select a trip", "yatra")}
948 </span>
949 )}
950 </div>
951 {formData.trip_id && (
952 <button
953 type="button"
954 className="p-2 text-gray-400 hover:text-gray-600"
955 onClick={(e) => {
956 e.stopPropagation();
957 handleFieldChange("trip_id", "");
958 setTripSearchQuery("");
959 }}
960 >
961 <X className="w-4 h-4" />
962 </button>
963 )}
964 <div className="p-2 text-gray-400">
965 <ChevronDown
966 className={`w-4 h-4 transition-transform ${isTripDropdownOpen ? "rotate-180" : ""}`}
967 />
968 </div>
969 </div>
970
971 {/* Dropdown */}
972 {isTripDropdownOpen && (
973 <div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-60 overflow-hidden">
974 {/* Search Input */}
975 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
976 <div className="relative">
977 <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
978 <input
979 type="text"
980 className="w-full pl-8 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
981 placeholder={__("Search trips...", "yatra")}
982 value={tripSearchQuery}
983 onChange={(e) =>
984 setTripSearchQuery(e.target.value)
985 }
986 onClick={(e) => e.stopPropagation()}
987 autoFocus
988 />
989 </div>
990 </div>
991
992 {/* Trip List */}
993 <div className="max-h-48 overflow-y-auto">
994 {(() => {
995 // Combine booked trip with trips list
996 let allTrips = [...(tripsData?.data || [])];
997
998 // Add booked trip if not in list
999 if (
1000 isEditMode &&
1001 bookingData?.trip_id &&
1002 bookingData?.trip_title
1003 ) {
1004 const bookedTripExists = allTrips.some(
1005 (t: any) =>
1006 String(t.id) ===
1007 String(bookingData.trip_id),
1008 );
1009 if (!bookedTripExists) {
1010 allTrips.unshift({
1011 id: bookingData.trip_id,
1012 title: bookingData.trip_title,
1013 price: bookingData.total_amount || 0,
1014 currency: globalCurrency,
1015 status: "publish",
1016 });
1017 }
1018 }
1019
1020 // Filter by search query
1021 const filteredTrips = allTrips.filter(
1022 (trip: any) =>
1023 trip.title
1024 .toLowerCase()
1025 .includes(tripSearchQuery.toLowerCase()) ||
1026 String(trip.id).includes(tripSearchQuery),
1027 );
1028
1029 if (filteredTrips.length === 0) {
1030 return (
1031 <div className="px-3 py-4 text-sm text-gray-500 text-center">
1032 {__("No trips found", "yatra")}
1033 </div>
1034 );
1035 }
1036
1037 return filteredTrips.map((trip: any) => (
1038 <div
1039 key={trip.id}
1040 className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 ${
1041 formData.trip_id === String(trip.id)
1042 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400"
1043 : "text-gray-900 dark:text-white"
1044 }`}
1045 onClick={() => {
1046 handleFieldChange(
1047 "trip_id",
1048 String(trip.id),
1049 );
1050 setIsTripDropdownOpen(false);
1051 setTripSearchQuery("");
1052 }}
1053 >
1054 <div className="font-medium">
1055 {trip.title}
1056 </div>
1057 <div className="text-xs text-gray-500 dark:text-gray-400">
1058 ID: {trip.id}
1059 </div>
1060 </div>
1061 ));
1062 })()}
1063 </div>
1064 </div>
1065 )}
1066 </div>
1067 {errors.trip_id && (
1068 <p className="mt-1 text-sm text-red-500">
1069 {errors.trip_id}
1070 </p>
1071 )}
1072 </div>
1073
1074 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1075 {/* Booking Date */}
1076 <div>
1077 <label
1078 htmlFor="booking_date"
1079 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1080 >
1081 {__("Booking Date", "yatra")}{" "}
1082 <span className="text-red-500">*</span>
1083 </label>
1084 <DatePicker
1085 value={formData.booking_date}
1086 onChange={(value: string) =>
1087 handleFieldChange("booking_date", value)
1088 }
1089 placeholder={__("Select booking date", "yatra")}
1090 error={!!errors.booking_date}
1091 />
1092 {errors.booking_date && (
1093 <p className="mt-1 text-sm text-red-500">
1094 {errors.booking_date}
1095 </p>
1096 )}
1097 </div>
1098
1099 {/* Travel Date */}
1100 <div>
1101 <label
1102 htmlFor="travel_date"
1103 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1104 >
1105 {__("Travel Date", "yatra")}{" "}
1106 <span className="text-red-500">*</span>
1107 </label>
1108 <DatePicker
1109 value={formData.travel_date}
1110 onChange={(value: string) =>
1111 handleFieldChange("travel_date", value)
1112 }
1113 placeholder={__("Select travel date", "yatra")}
1114 error={!!errors.travel_date}
1115 />
1116 {errors.travel_date && (
1117 <p className="mt-1 text-sm text-red-500">
1118 {errors.travel_date}
1119 </p>
1120 )}
1121 </div>
1122 </div>
1123
1124 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1125 {/* Number of Travelers */}
1126 <div>
1127 <label
1128 htmlFor="travelers"
1129 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1130 >
1131 {__("Number of Travelers", "yatra")}{" "}
1132 <span className="text-red-500">*</span>
1133 </label>
1134 <Input
1135 id="travelers"
1136 type="number"
1137 min="1"
1138 value={formData.travelers}
1139 onChange={(e) =>
1140 handleFieldChange("travelers", e.target.value)
1141 }
1142 className={errors.travelers ? "border-red-500" : ""}
1143 required
1144 />
1145 {errors.travelers && (
1146 <p className="mt-1 text-sm text-red-500">
1147 {errors.travelers}
1148 </p>
1149 )}
1150 </div>
1151
1152 {/* Tax Breakdown */}
1153 <div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 border border-gray-200 dark:border-gray-600">
1154 <h4 className="font-medium text-gray-900 dark:text-white mb-2 text-sm">
1155 {__("Pricing Breakdown", "yatra")}
1156 </h4>
1157 <div className="space-y-2 text-sm">
1158 {formData.subtotal ? (
1159 <>
1160 <div className="flex justify-between">
1161 <span className="text-gray-600 dark:text-gray-400">
1162 {__("Subtotal", "yatra")}
1163 </span>
1164 <span className="font-medium text-gray-900 dark:text-white">
1165 {currencySymbol}
1166 {parseFloat(formData.subtotal).toFixed(2)}
1167 </span>
1168 </div>
1169 {/* Itinerary Costs */}
1170 {(bookingData as any)?.itinerary_costs_total >
1171 0 && (
1172 <div className="flex justify-between">
1173 <span className="text-gray-600 dark:text-gray-400">
1174 {__("Itinerary Costs", "yatra")}
1175 </span>
1176 <span className="font-medium text-gray-900 dark:text-white">
1177 {getCurrencySymbol(
1178 formData.currency || globalCurrency,
1179 )}
1180 {(
1181 bookingData as any
1182 ).itinerary_costs_total.toFixed(2)}
1183 </span>
1184 </div>
1185 )}
1186 {parseFloat(formData.tax_amount) > 0 && (
1187 <div className="flex justify-between">
1188 <span className="text-gray-600 dark:text-gray-400">
1189 {__("Tax", "yatra")}
1190 </span>
1191 <span className="font-medium text-gray-900 dark:text-white">
1192 {currencySymbol}
1193 {parseFloat(formData.tax_amount).toFixed(2)}
1194 </span>
1195 </div>
1196 )}
1197 <div className="border-t pt-2 flex justify-between font-semibold">
1198 <span className="text-gray-900 dark:text-white">
1199 {__("Total", "yatra")}
1200 </span>
1201 <span className="text-gray-900 dark:text-white">
1202 {getCurrencySymbol(
1203 formData.currency || globalCurrency,
1204 )}
1205 {formData.total_amount}
1206 </span>
1207 </div>
1208 </>
1209 ) : (
1210 <div className="text-center text-gray-500 dark:text-gray-400 py-2">
1211 {__(
1212 "Select a trip and number of travelers to see pricing",
1213 "yatra",
1214 )}
1215 </div>
1216 )}
1217 </div>
1218 </div>
1219
1220 {/* Total Amount */}
1221 <div>
1222 <label
1223 htmlFor="total_amount"
1224 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1225 >
1226 {__("Total Amount", "yatra")} (
1227 {formData.currency || globalCurrency}){" "}
1228 <span className="text-red-500">*</span>
1229 </label>
1230 <div className="relative">
1231 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
1232 {getCurrencySymbol(
1233 formData.currency || globalCurrency,
1234 )}
1235 </span>
1236 <Input
1237 id="total_amount"
1238 type="number"
1239 step="0.01"
1240 min="0"
1241 value={formData.total_amount}
1242 onChange={(e) =>
1243 handleFieldChange("total_amount", e.target.value)
1244 }
1245 placeholder="0.00"
1246 className={`pl-7 ${errors.total_amount ? "border-red-500" : ""}`}
1247 required
1248 />
1249 </div>
1250 {errors.total_amount && (
1251 <p className="mt-1 text-sm text-red-500">
1252 {errors.total_amount}
1253 </p>
1254 )}
1255 </div>
1256 </div>
1257
1258 {/* Notes */}
1259 <div>
1260 <label
1261 htmlFor="notes"
1262 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1263 >
1264 {__("Notes", "yatra")}
1265 </label>
1266 <textarea
1267 id="notes"
1268 value={formData.notes}
1269 onChange={(e) =>
1270 handleFieldChange("notes", e.target.value)
1271 }
1272 placeholder={__(
1273 "Additional notes or special requests",
1274 "yatra",
1275 )}
1276 rows={3}
1277 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
1278 />
1279 </div>
1280 </CardContent>
1281 </Card>
1282
1283 {/* Emergency Contact - Dynamic Fields */}
1284 {formConfig?.emergency_contact_form?.enabled !== false &&
1285 emergencyContactFields.length > 0 && (
1286 <Card>
1287 <CardHeader className="pb-2">
1288 <CardTitle className="text-base flex items-center gap-2">
1289 <AlertCircle className="w-4 h-4" />
1290 {formConfig?.emergency_contact_form?.title ||
1291 __("Emergency Contact", "yatra")}
1292 </CardTitle>
1293 {formConfig?.emergency_contact_form?.description && (
1294 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1295 {formConfig.emergency_contact_form.description}
1296 </p>
1297 )}
1298 </CardHeader>
1299 <CardContent>
1300 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1301 {emergencyContactFields.map((field) => (
1302 <div
1303 key={field.id}
1304 className={
1305 field.width === "full" ? "md:col-span-2" : ""
1306 }
1307 >
1308 <label
1309 htmlFor={`emergency-${field.id}`}
1310 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
1311 >
1312 {field.label}
1313 {field.required && (
1314 <span className="text-red-500 ml-1">*</span>
1315 )}
1316 </label>
1317
1318 {/* Render field based on type */}
1319 {field.type === "select" ? (
1320 <Select
1321 id={`emergency-${field.id}`}
1322 value={emergencyContactData[field.id] || ""}
1323 onChange={(e) =>
1324 handleEmergencyContactChange(
1325 field.id,
1326 e.target.value,
1327 )
1328 }
1329 >
1330 <option value="">
1331 {field.placeholder || `Select ${field.label}`}
1332 </option>
1333 {field.options?.map((opt) => (
1334 <option key={opt.value} value={opt.value}>
1335 {opt.label}
1336 </option>
1337 ))}
1338 </Select>
1339 ) : field.type === "country" ? (
1340 <Select
1341 id={`emergency-${field.id}`}
1342 value={emergencyContactData[field.id] || ""}
1343 onChange={(e) =>
1344 handleEmergencyContactChange(
1345 field.id,
1346 e.target.value,
1347 )
1348 }
1349 >
1350 <option value="">
1351 {field.placeholder || "Select Country"}
1352 </option>
1353 {countryList.map((country) => (
1354 <option
1355 key={country.code}
1356 value={country.code}
1357 >
1358 {country.name}
1359 </option>
1360 ))}
1361 </Select>
1362 ) : field.type === "textarea" ? (
1363 <textarea
1364 id={`emergency-${field.id}`}
1365 value={emergencyContactData[field.id] || ""}
1366 onChange={(e) =>
1367 handleEmergencyContactChange(
1368 field.id,
1369 e.target.value,
1370 )
1371 }
1372 placeholder={field.placeholder}
1373 rows={2}
1374 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:placeholder:text-gray-400 resize-none"
1375 />
1376 ) : field.type === "checkbox" ? (
1377 <label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
1378 <input
1379 type="checkbox"
1380 className="rounded border-gray-300"
1381 checked={
1382 emergencyContactData[field.id] === "1" ||
1383 emergencyContactData[field.id] === "true"
1384 }
1385 onChange={(e) =>
1386 handleEmergencyContactChange(
1387 field.id,
1388 e.target.checked ? "1" : "",
1389 )
1390 }
1391 />
1392 <span>{field.placeholder || field.label}</span>
1393 </label>
1394 ) : (
1395 <Input
1396 id={`emergency-${field.id}`}
1397 type={
1398 field.type === "email"
1399 ? "email"
1400 : field.type === "tel"
1401 ? "tel"
1402 : field.type === "date"
1403 ? "date"
1404 : field.type === "number"
1405 ? "number"
1406 : "text"
1407 }
1408 value={emergencyContactData[field.id] || ""}
1409 onChange={(e) =>
1410 handleEmergencyContactChange(
1411 field.id,
1412 e.target.value,
1413 )
1414 }
1415 placeholder={field.placeholder}
1416 />
1417 )}
1418 </div>
1419 ))}
1420 </div>
1421 </CardContent>
1422 </Card>
1423 )}
1424
1425 {/* Travelers Information - Dynamic Fields */}
1426 {formConfig?.traveler_form?.enabled !== false &&
1427 travelerFields.length > 0 && (
1428 <Card>
1429 <CardHeader className="pb-2">
1430 <div className="flex items-center justify-between">
1431 <CardTitle className="text-base flex items-center gap-2">
1432 <Users className="w-4 h-4" />
1433 {formConfig?.traveler_form?.title ||
1434 __("Travelers Information", "yatra")}
1435 <span className="text-xs font-normal text-gray-500 dark:text-gray-400">
1436 ({travelersData.length}{" "}
1437 {travelersData.length === 1
1438 ? __("traveler", "yatra")
1439 : __("travelers", "yatra")}
1440 )
1441 </span>
1442 </CardTitle>
1443 <div className="flex items-center gap-2">
1444 {travelersData.length > 1 && (
1445 <Button
1446 type="button"
1447 variant="ghost"
1448 size="sm"
1449 onClick={() =>
1450 setExpandedTravelers(
1451 expandedTravelers.length ===
1452 travelersData.length
1453 ? []
1454 : travelersData.map((_, i) => i),
1455 )
1456 }
1457 className="text-gray-500 text-xs"
1458 >
1459 {expandedTravelers.length === travelersData.length
1460 ? __("Collapse All", "yatra")
1461 : __("Expand All", "yatra")}
1462 </Button>
1463 )}
1464 <Button
1465 type="button"
1466 variant="outline"
1467 size="sm"
1468 onClick={addTraveler}
1469 className="flex items-center gap-1"
1470 >
1471 <Plus className="w-3 h-3" />
1472 {__("Add Traveler", "yatra")}
1473 </Button>
1474 </div>
1475 </div>
1476 {formConfig?.traveler_form?.description && (
1477 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1478 {formConfig.traveler_form.description}
1479 </p>
1480 )}
1481 </CardHeader>
1482 <CardContent className="space-y-3">
1483 {travelersData.map((traveler, travelerIndex) => (
1484 <div
1485 key={travelerIndex}
1486 className={`border rounded-lg ${travelerIndex === 0 ? "border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10" : "border-gray-200 dark:border-gray-700"}`}
1487 >
1488 {/* Traveler Header - Collapsible */}
1489 <div
1490 className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-t-lg"
1491 onClick={() =>
1492 toggleTravelerExpanded(travelerIndex)
1493 }
1494 >
1495 <div className="flex items-center gap-2 flex-1 min-w-0">
1496 <span className="font-medium text-sm text-gray-900 dark:text-white whitespace-nowrap">
1497 {travelerIndex === 0
1498 ? __("Lead Traveler", "yatra")
1499 : `${__("Traveler", "yatra")} ${travelerIndex + 1}`}
1500 </span>
1501 {traveler.first_name || traveler.last_name ? (
1502 <span className="text-sm text-gray-500 dark:text-gray-400 truncate">
1503 -{" "}
1504 {[traveler.first_name, traveler.last_name]
1505 .filter(Boolean)
1506 .join(" ")}
1507 </span>
1508 ) : null}
1509 {travelerIndex === 0 && (
1510 <span className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 px-2 py-0.5 rounded whitespace-nowrap">
1511 {__("Primary", "yatra")}
1512 </span>
1513 )}
1514 </div>
1515 <div className="flex items-center gap-1 ml-2">
1516 {/* Remove button - show for all travelers if more than 1 exists */}
1517 {travelersData.length > 1 && (
1518 <Button
1519 type="button"
1520 variant="ghost"
1521 size="sm"
1522 onClick={(e) => {
1523 e.stopPropagation();
1524 removeTraveler(travelerIndex);
1525 }}
1526 className="text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 h-8 px-2 flex items-center gap-1"
1527 title={__("Remove Traveler", "yatra")}
1528 >
1529 <Trash2 className="w-4 h-4" />
1530 <span className="text-xs hidden sm:inline">
1531 {__("Remove", "yatra")}
1532 </span>
1533 </Button>
1534 )}
1535 {/* Expand/Collapse toggle */}
1536 <div className="w-6 h-6 flex items-center justify-center">
1537 {expandedTravelers.includes(travelerIndex) ? (
1538 <ChevronUp className="w-4 h-4 text-gray-500" />
1539 ) : (
1540 <ChevronDown className="w-4 h-4 text-gray-500" />
1541 )}
1542 </div>
1543 </div>
1544 </div>
1545
1546 {/* Traveler Fields - Collapsible Content */}
1547 {expandedTravelers.includes(travelerIndex) && (
1548 <div className="p-3 pt-0 border-t border-gray-100 dark:border-gray-700/50">
1549 <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
1550 {travelerFields.map((field) => (
1551 <div
1552 key={field.id}
1553 className={
1554 field.width === "full"
1555 ? "md:col-span-2"
1556 : ""
1557 }
1558 >
1559 <label
1560 htmlFor={`traveler-${travelerIndex}-${field.id}`}
1561 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
1562 >
1563 {field.label}
1564 {field.required && (
1565 <span className="text-red-500 ml-1">
1566 *
1567 </span>
1568 )}
1569 </label>
1570
1571 {/* Render field based on type */}
1572 {field.type === "select" ? (
1573 <Select
1574 id={`traveler-${travelerIndex}-${field.id}`}
1575 value={traveler[field.id] || ""}
1576 onChange={(e) =>
1577 handleTravelerChange(
1578 travelerIndex,
1579 field.id,
1580 e.target.value,
1581 )
1582 }
1583 >
1584 <option value="">
1585 {field.placeholder ||
1586 `Select ${field.label}`}
1587 </option>
1588 {field.options?.map((opt) => (
1589 <option
1590 key={opt.value}
1591 value={opt.value}
1592 >
1593 {opt.label}
1594 </option>
1595 ))}
1596 </Select>
1597 ) : field.type === "country" ? (
1598 <Select
1599 id={`traveler-${travelerIndex}-${field.id}`}
1600 value={traveler[field.id] || ""}
1601 onChange={(e) =>
1602 handleTravelerChange(
1603 travelerIndex,
1604 field.id,
1605 e.target.value,
1606 )
1607 }
1608 >
1609 <option value="">
1610 {field.placeholder ||
1611 "Select Country"}
1612 </option>
1613 {countryList.map((country) => (
1614 <option
1615 key={country.code}
1616 value={country.code}
1617 >
1618 {country.name}
1619 </option>
1620 ))}
1621 </Select>
1622 ) : field.type === "textarea" ? (
1623 <textarea
1624 id={`traveler-${travelerIndex}-${field.id}`}
1625 value={traveler[field.id] || ""}
1626 onChange={(e) =>
1627 handleTravelerChange(
1628 travelerIndex,
1629 field.id,
1630 e.target.value,
1631 )
1632 }
1633 placeholder={field.placeholder}
1634 rows={2}
1635 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:placeholder:text-gray-400 resize-none"
1636 />
1637 ) : field.type === "checkbox" ? (
1638 <label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
1639 <input
1640 type="checkbox"
1641 className="rounded border-gray-300"
1642 checked={
1643 traveler[field.id] === "1" ||
1644 traveler[field.id] === "true"
1645 }
1646 onChange={(e) =>
1647 handleTravelerChange(
1648 travelerIndex,
1649 field.id,
1650 e.target.checked ? "1" : "",
1651 )
1652 }
1653 />
1654 <span>
1655 {field.placeholder || field.label}
1656 </span>
1657 </label>
1658 ) : field.type === "date" ||
1659 field.id.toLowerCase().includes("date") ||
1660 field.id
1661 .toLowerCase()
1662 .includes("expiry") ? (
1663 <DatePicker
1664 value={traveler[field.id] || ""}
1665 onChange={(value: string) =>
1666 handleTravelerChange(
1667 travelerIndex,
1668 field.id,
1669 value,
1670 )
1671 }
1672 placeholder={
1673 field.placeholder ||
1674 __("Select date", "yatra")
1675 }
1676 />
1677 ) : (
1678 <Input
1679 id={`traveler-${travelerIndex}-${field.id}`}
1680 type={
1681 field.type === "email"
1682 ? "email"
1683 : field.type === "tel"
1684 ? "tel"
1685 : field.type === "number"
1686 ? "number"
1687 : "text"
1688 }
1689 value={traveler[field.id] || ""}
1690 onChange={(e) =>
1691 handleTravelerChange(
1692 travelerIndex,
1693 field.id,
1694 e.target.value,
1695 )
1696 }
1697 placeholder={field.placeholder}
1698 />
1699 )}
1700 </div>
1701 ))}
1702 </div>
1703 </div>
1704 )}
1705 </div>
1706 ))}
1707 </CardContent>
1708 </Card>
1709 )}
1710 </div>
1711
1712 {/* Sidebar */}
1713 <div className="space-y-3">
1714 {/* Status & Payment */}
1715 <Card>
1716 <CardHeader className="pb-2">
1717 <CardTitle className="text-base">
1718 {__("Status & Payment", "yatra")}
1719 </CardTitle>
1720 </CardHeader>
1721 <CardContent className="space-y-3">
1722 {/* Booking Status */}
1723 <div>
1724 <label
1725 htmlFor="booking_status"
1726 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1727 >
1728 {__("Booking Status", "yatra")}
1729 </label>
1730 <Select
1731 id="booking_status"
1732 value={formData.booking_status}
1733 onChange={(e) =>
1734 handleFieldChange("booking_status", e.target.value)
1735 }
1736 >
1737 <option value="pending">{__("Pending", "yatra")}</option>
1738 <option value="confirmed">
1739 {__("Confirmed", "yatra")}
1740 </option>
1741 <option value="cancelled">
1742 {__("Cancelled", "yatra")}
1743 </option>
1744 <option value="completed">
1745 {__("Completed", "yatra")}
1746 </option>
1747 <option value="refunded">
1748 {__("Refunded", "yatra")}
1749 </option>
1750 <option value="failed">{__("Failed", "yatra")}</option>
1751 </Select>
1752 </div>
1753
1754 {/* Payment Status */}
1755 <div>
1756 <label
1757 htmlFor="payment_status"
1758 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1759 >
1760 {__("Payment Status", "yatra")}
1761 </label>
1762 <Select
1763 id="payment_status"
1764 value={formData.payment_status}
1765 onChange={(e) =>
1766 handleFieldChange("payment_status", e.target.value)
1767 }
1768 >
1769 <option value="pending">{__("Pending", "yatra")}</option>
1770 <option value="partial">{__("Partial", "yatra")}</option>
1771 <option value="paid">{__("Paid", "yatra")}</option>
1772 <option value="refunded">
1773 {__("Refunded", "yatra")}
1774 </option>
1775 </Select>
1776 </div>
1777
1778 {/* Payment Method */}
1779 <div>
1780 <label
1781 htmlFor="payment_method"
1782 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1783 >
1784 {__("Payment Method", "yatra")}
1785 </label>
1786 <Select
1787 id="payment_method"
1788 value={formData.payment_method}
1789 onChange={(e) =>
1790 handleFieldChange("payment_method", e.target.value)
1791 }
1792 >
1793 <option value="">
1794 {__("Select payment method", "yatra")}
1795 </option>
1796 {gatewaysData?.data?.map((gw: any) => (
1797 <option key={gw.id} value={gw.id}>
1798 {gw.title}
1799 </option>
1800 ))}
1801 {/* Fallback options if no gateways loaded */}
1802 {(!gatewaysData?.data ||
1803 gatewaysData.data.length === 0) && (
1804 <>
1805 <option value="pay_later">
1806 {__("Pay Later", "yatra")}
1807 </option>
1808 <option value="bank_transfer">
1809 {__("Bank Transfer", "yatra")}
1810 </option>
1811 <option value="stripe">
1812 {__("Stripe", "yatra")}
1813 </option>
1814 <option value="paypal">
1815 {__("PayPal", "yatra")}
1816 </option>
1817 </>
1818 )}
1819 </Select>
1820 </div>
1821 </CardContent>
1822 </Card>
1823
1824 {/* Submit Actions */}
1825 <Card>
1826 <CardContent className="p-3">
1827 <div className="space-y-2">
1828 {errors.submit && (
1829 <div className="p-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-md">
1830 {errors.submit}
1831 </div>
1832 )}
1833 <div className="flex gap-2">
1834 <Button
1835 type="submit"
1836 disabled={isSubmitting}
1837 className="flex-1 flex items-center justify-center gap-2"
1838 >
1839 {isSubmitting ? (
1840 <>
1841 <Loader2 className="w-4 h-4 animate-spin" />
1842 {__("Saving...", "yatra")}
1843 </>
1844 ) : (
1845 <>
1846 <Save className="w-4 h-4" />
1847 {isEditMode
1848 ? __("Update Booking", "yatra")
1849 : __("Create Booking", "yatra")}
1850 </>
1851 )}
1852 </Button>
1853 <Button
1854 type="button"
1855 variant="outline"
1856 onClick={handleCancel}
1857 disabled={isSubmitting}
1858 >
1859 {__("Cancel", "yatra")}
1860 </Button>
1861 </div>
1862 </div>
1863 </CardContent>
1864 </Card>
1865 </div>
1866 </div>
1867 </form>
1868 </ConditionalRender>
1869 </div>
1870 );
1871 };
1872
1873 export default BookingForm;
1874