PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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.5, at resources/js/pages/BookingForm.tsx

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