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

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

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