PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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 / TripForm.tsx

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

9,366 lines 396.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Trip Form Page - Wizard Style
3 * Multi-step form for creating/editing trips with sidebar navigation
4 */
5
6 import React, { useState, useEffect, useMemo, useRef } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8
9 // Utility function to strip HTML and sanitize text for SEO preview
10 const sanitizeTextForSEO = (text: string, maxLength: number = 160): string => {
11 if (!text) return "";
12
13 // Remove HTML tags
14 const plainText = text.replace(/<[^>]*>/g, "");
15
16 // Decode HTML entities
17 const decodedText = plainText
18 .replace(/&amp;/g, "&")
19 .replace(/&lt;/g, "<")
20 .replace(/&gt;/g, ">")
21 .replace(/&quot;/g, '"')
22 .replace(/&#39;/g, "'")
23 .replace(/&nbsp;/g, " ");
24
25 // Remove extra whitespace and trim
26 const cleanText = decodedText.replace(/\s+/g, " ").trim();
27
28 // Truncate if needed
29 if (cleanText.length > maxLength) {
30 return cleanText.substring(0, maxLength) + "...";
31 }
32
33 return cleanText;
34 };
35 import {
36 Save,
37 Loader2,
38 Sparkles,
39 Calendar,
40 Clock,
41 Image,
42 HelpCircle,
43 Search,
44 Settings,
45 CheckCircle2,
46 CheckSquare,
47 FileText,
48 DollarSign,
49 AlertCircle,
50 Plus,
51 Upload,
52 MapPin,
53 Tag,
54 GripVertical,
55 ChevronUp,
56 ChevronDown,
57 ChevronLeft,
58 ChevronRight,
59 Copy,
60 Check,
61 BookOpen,
62 Download,
63 Eye,
64 X,
65 Trash2,
66 Users,
67 Box,
68 BarChart3,
69 Database,
70 History,
71 Home,
72 Car,
73 Lightbulb,
74 } from "lucide-react";
75 import { RichTextEditor } from "../components/ui/rich-text-editor";
76 import { IconPicker, IconPickerValue } from "../components/ui/icon-picker";
77 import { __, sprintf } from "../lib/i18n";
78 import { MEAL_PLAN_SELECT_OPTIONS } from "../lib/meal-plan-labels";
79 import { usePermissions } from "../hooks/usePermissions";
80 import { fetchSettings } from "../api/settings-api";
81 import { apiClient } from "../lib/api-client";
82 import { wpService } from "../lib/api-client";
83 import { Button } from "../components/ui/button";
84 import { Input } from "@/components/ui/input";
85 import { DatePicker } from "@/components/ui/date-picker";
86 import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open";
87 import { Modal } from "../components/ui/modal";
88 import { Select } from "../components/ui/select";
89 import { Alert } from "../components/ui/alert";
90 import {
91 Card,
92 CardContent,
93 CardHeader,
94 CardTitle,
95 CardDescription,
96 } from "../components/ui/card";
97 import { Badge } from "../components/ui/badge";
98 import TripAttributesSection from "../components/trip-form/TripAttributesSection";
99 import { HelpText } from "../components/ui/help-text";
100 import { getCurrencySymbol } from "../data/currencies";
101 import { ItinerarySection } from "../components/trip-form/sections/ItinerarySection";
102 import { IncludedSection } from "../components/trip-form/sections/IncludedSection";
103 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
104 import { useToast } from "../components/ui/toast";
105 import { MultiSelect } from "../components/ui/multi-select";
106 import { TestimonialsSelector } from "../components/trip-form/TestimonialsSelector";
107 import { LocationPicker } from "../components/trip-form/LocationPicker";
108 import { ProFeature, ProBadge } from "../components/ProFeature";
109 import { getErrorContext } from "../lib/errors";
110 import {
111 buildYatraSinglePublicUrls,
112 isWordPressPlainPermalink,
113 } from "../lib/frontend-permalink-urls";
114
115 type SectionId =
116 | "basic" // 1. Basic Information (title, description, highlights, featured image, trip type, duration)
117 | "location" // 2. Location & Geography
118 | "duration" // 3. Schedule & Availability (renamed)
119 | "pricing" // 4. Pricing & Payment
120 | "booking" // 5. Booking Requirements
121 | "attributes" // 6. Trip Attributes
122 | "itinerary" // 7. Itinerary Builder (includes Included/Excluded)
123 | "included" // 8. What's Included/Excluded (deprecated - merged into itinerary)
124 | "media" // 9. Media & Content (gallery, video, story, testimonials)
125 | "downloads" // 9b. Downloads (Pro module)
126 | "categorization" // 10. Categorization & Tags (category, activities, difficulty, tags)
127 | "faqs" // 11. FAQs
128 | "seo" // 12. SEO Settings
129 | "advanced"; // 13. Advanced Settings (status, scheduling, frontend tabs)
130
131 interface Section {
132 id: SectionId;
133 label: string;
134 icon: React.ComponentType<{ className?: string }>;
135 required: boolean;
136 completed: boolean;
137 hasErrors?: boolean;
138 }
139
140 interface FAQ {
141 question: string;
142 answer: string;
143 }
144
145 interface TravelerCategory {
146 id: number;
147 label: string;
148 description: string;
149 age_min?: number;
150 age_max?: number;
151 status: "active" | "inactive" | "publish" | "draft";
152 pricing_mode?: "per_person" | "per_group";
153 min_pax?: number | null;
154 max_pax?: number | null;
155 }
156
157 interface PriceType {
158 category_id: number;
159 original_price: string;
160 discounted_price: string;
161 is_default?: boolean;
162 }
163
164 interface TripCategoryOption {
165 id: number;
166 name: string;
167 slug?: string;
168 description?: string;
169 parent_id: number | null;
170 status?: string;
171 subcategories?: TripCategoryOption[];
172 }
173
174 interface DifficultyLevelOption {
175 id: number;
176 name: string;
177 slug?: string;
178 description?: string;
179 level_order?: number;
180 status?: string;
181 }
182
183 interface TripAmenityItem {
184 title: string;
185 description: string;
186 }
187
188 interface DownloadableItem {
189 id?: number | null;
190 title: string;
191 description: string;
192 attachment_id: number | null;
193 attachment_url?: string;
194 attachment_title?: string;
195 visibility: "public" | "logged_in" | "booked_only";
196 enabled: boolean;
197 sort_order?: number;
198 }
199
200 const buildCategoryOptionNodes = (
201 categories: TripCategoryOption[],
202 depth = 0,
203 ): React.ReactNode[] => {
204 return categories.flatMap((category) => {
205 const optionValue =
206 category.slug || category.name || category.id?.toString() || "";
207 const labelPrefix =
208 depth > 0 ? `${"-- ".repeat(depth)}${category.name}` : category.name;
209 const nodes: React.ReactNode[] = [
210 <option key={`category-option-${category.id}`} value={optionValue}>
211 {labelPrefix}
212 </option>,
213 ];
214
215 if (
216 Array.isArray(category.subcategories) &&
217 category.subcategories.length > 0
218 ) {
219 nodes.push(
220 ...buildCategoryOptionNodes(category.subcategories, depth + 1),
221 );
222 }
223
224 return nodes;
225 });
226 };
227
228 const extractArrayPayload = (payload: any): any[] => {
229 if (!payload) return [];
230 if (Array.isArray(payload)) return payload;
231 if (Array.isArray(payload.data)) return payload.data;
232 if (Array.isArray(payload?.data?.data)) return payload.data.data;
233 return [];
234 };
235
236 const normalizeAmenityItems = (items: unknown): TripAmenityItem[] => {
237 if (!items) return [];
238 if (Array.isArray(items)) {
239 return items
240 .map((item) => {
241 if (typeof item === "string") {
242 return { title: item, description: "" };
243 }
244 if (item && typeof item === "object") {
245 const obj = item as Partial<TripAmenityItem>;
246 return {
247 title: (obj.title ?? "").toString(),
248 description: (obj.description ?? "").toString(),
249 };
250 }
251 return { title: String(item), description: "" };
252 })
253 .filter((item) => item.title.trim().length > 0);
254 }
255 if (typeof items === "string") {
256 try {
257 const parsed = JSON.parse(items);
258 if (Array.isArray(parsed)) {
259 return normalizeAmenityItems(parsed);
260 }
261 } catch {
262 return [{ title: items, description: "" }];
263 }
264 }
265 return [];
266 };
267
268 interface TimeSlot {
269 id: string;
270 time: string; // HH:MM format
271 label: string;
272 }
273
274 interface TripFormData {
275 // Overview
276 title: string;
277 slug: string;
278 description: string;
279 highlights: string[];
280 trip_details: string;
281 short_description: string;
282 what_makes_special: string; // What Makes This Trip Special
283 trip_story: string; // Trip Story/Narrative
284 video_url: string; // Video Embed URL
285 virtual_tour_url: string; // 360° Virtual Tour URL
286 testimonial_review_ids: number[]; // Array of review IDs to display as testimonials
287
288 // Location & Geography
289 destinations: number[]; // Array of destination IDs
290 starting_location: string;
291 ending_location: string;
292 countries: string[];
293 regions: string[];
294 starting_latitude: string;
295 starting_longitude: string;
296 ending_latitude: string;
297 ending_longitude: string;
298 landmarks: string[]; // Geographic Tags - Landmarks
299
300 // Duration & Schedule
301 trip_type: "single_day" | "multi_day" | "flexible";
302 duration_days: string;
303 duration_nights: string;
304 available_from: string;
305 available_to: string;
306 booking_window_days: string;
307 seasonal_availability: string;
308 best_season: string; // Best season indicator
309 peak_season: string; // Peak season indicator
310 off_season: string; // Off-season indicator
311
312 // Activity & Category
313 activity_types: number[]; // Array of activity IDs (changed from string[])
314 difficulty_level: string;
315 trip_category: number[]; // Array of category IDs
316 tags: string[];
317 featured_priority: "none" | "featured" | "new" | "limited"; // Featured Priority
318
319 // Accommodation
320 accommodation_type: string;
321 meal_plan: string;
322 accommodation_details: string;
323
324 // Transportation
325 transportation_included: boolean;
326 pickup_location: string;
327 dropoff_location: string;
328 transportation_details: string;
329
330 // Pricing
331 pricing_type: "regular" | "traveler_based";
332 original_price: string;
333 discounted_price: string;
334 price_types: PriceType[];
335 deposit_amount: string;
336 deposit_percentage: string;
337 payment_terms: string;
338 max_travelers: string;
339 min_travelers: string;
340 booking_deadline_hours: string;
341 cancellation_policy: string;
342 age_min: string;
343 age_max: string;
344 physical_requirements: string;
345 visa_requirements: string;
346 vaccination_requirements: string;
347 disable_booking: boolean; // Pro: enquiry-only mode (stored in custom_fields)
348
349 // Fallback Settings (for trips without availability dates/rules)
350 has_default_time_slots: boolean; // For day tours: enable multiple time slots
351 default_time_slots: TimeSlot[]; // Array of time slot objects for day tours
352 departure_time: string; // Default departure time
353
354 // Included/Excluded
355 included_items: TripAmenityItem[];
356 excluded_items: TripAmenityItem[];
357
358 // Attributes & Properties
359 attributes: Record<number, any>; // attribute_id -> value mapping
360
361 // Itinerary
362 itinerary_days: ItineraryDay[];
363
364 // Gallery
365 gallery_images: Array<{
366 id: number;
367 url: string;
368 thumbnail_url?: string;
369 alt_text?: string;
370 caption?: string;
371 }>;
372 featured_image: number | null;
373
374 // Downloads
375 downloadable_items: DownloadableItem[];
376
377 // FAQs
378 faqs: FAQ[];
379
380 // Frontend Tabs
381 frontend_tabs: FrontendTab[];
382
383 // Availability
384 availability_dates: AvailabilityDate[];
385
386 // Status & Lifecycle
387 status:
388 | "draft"
389 | "review"
390 | "approved"
391 | "publish"
392 | "archived"
393 | "suspended";
394 scheduled_publish_date: string; // Scheduled Publishing
395 scheduled_unpublish_date: string; // Scheduled Unpublishing
396 version: number; // Version Control
397 seasonal_auto_enable: boolean; // Auto-enable/disable based on dates
398 seasonal_enable_date: string; // Date to auto-enable
399 seasonal_disable_date: string; // Date to auto-disable
400
401 // SEO
402 meta_title: string;
403 meta_description: string;
404 meta_keywords: string;
405 }
406
407 interface FrontendTab {
408 id: string;
409 label: string;
410 enabled: boolean;
411 order: number;
412 content_type:
413 | "overview"
414 | "itinerary"
415 | "included_excluded"
416 | "location"
417 | "important_info"
418 | "downloads"
419 | "faq"
420 | "trip_story"
421 | "what_makes_special"
422 | "testimonials"
423 | "reviews"
424 | "downloads"
425 | "custom"
426 | "general"
427 | "gallery"
428 | "faqs";
429 custom_content?: string;
430 icon?: IconPickerValue | null;
431 }
432
433 interface AvailabilityDate {
434 id: string;
435 /** Present when availability row is tied to a specific trip */
436 trip_id?: number;
437 departure_date: string;
438 arrival_date: string;
439 seats_remaining: string;
440 original_price: string;
441 discounted_price: string;
442 discount_percentage: string;
443 status: "available" | "sold_out" | "limited" | "closed";
444 from_location?: string;
445 to_location?: string;
446 from_latitude?: string;
447 from_longitude?: string;
448 to_latitude?: string;
449 to_longitude?: string;
450 }
451
452 interface ItineraryEntry {
453 id: string;
454 day: number;
455 day_title?: string;
456 // Use IDs to match ItineraryForm structure
457 item_type_id: string; // ID of the item type (Activity, Meal, Accommodation, Transportation, Rest)
458 item_id: string; // ID of the specific item (Hiking, Breakfast, Hotel, etc.)
459 // Keep legacy fields for backward compatibility
460 item_type?: "Meal" | "Activity" | "Accommodation" | "Transportation";
461 item_name?: string;
462 item_icon?: string;
463 // Entry details
464 title: string;
465 description: string;
466 location?: string;
467 location_latitude?: string;
468 location_longitude?: string;
469 duration?: string;
470 start_time: string;
471 end_time: string;
472 time_type: "exact" | "duration" | "flexible";
473 cost?: string;
474 cost_per_person: boolean;
475 notes?: string;
476 included_items: string[];
477 excluded_items: string[];
478 images: string[];
479 status?: "active" | "inactive";
480 }
481
482 interface ItineraryDay {
483 day: number;
484 day_title?: string;
485 entries: ItineraryEntry[];
486 }
487
488 // getCurrencySymbol is now imported from '../data/currencies'
489
490 const TripForm: React.FC = () => {
491 const queryClient = useQueryClient();
492 const { can, isPro } = usePermissions();
493 const { showToast } = useToast();
494 // Downloads is now a FREE feature - always show the UI
495 const showDownloadsUI = true;
496
497 const [featuredImagePreview, setFeaturedImagePreview] = useState<string>("");
498 const [isResolvingFeaturedImage, setIsResolvingFeaturedImage] =
499 useState<boolean>(false);
500 const featuredImageCache = useRef<Record<number, string>>({});
501 const mediaBaseUrl = useMemo(() => {
502 const apiUrl = (window as any)?.yatraAdmin?.apiUrl;
503 return apiUrl ? apiUrl.replace(/\/yatra\/v1\/?$/, "") : "";
504 }, []);
505
506 // Get section from URL on initial load
507 const getInitialSection = (): SectionId => {
508 const urlParams = new URLSearchParams(window.location.search);
509 const sectionFromUrl = urlParams.get("section") as SectionId | null;
510 const validSections: SectionId[] = [
511 "basic",
512 "location",
513 "duration",
514 "pricing",
515 "booking",
516 "attributes",
517 "itinerary",
518 "included",
519 "media",
520 "downloads",
521 "categorization",
522 "faqs",
523 "seo",
524 "advanced",
525 ];
526 if (sectionFromUrl && validSections.includes(sectionFromUrl)) {
527 return sectionFromUrl;
528 }
529 return "basic";
530 };
531
532 const [currentSection, setCurrentSection] =
533 useState<SectionId>(getInitialSection);
534 const [visitedSections, setVisitedSections] = useState<Set<SectionId>>(
535 () => new Set([getInitialSection()]),
536 );
537 const [showCategorySelector, setShowCategorySelector] = useState(false);
538 const [showLandmarkDialog, setShowLandmarkDialog] = useState(false);
539 const [landmarkInput, setLandmarkInput] = useState("");
540
541 // Sub-tab state for Trip Details section (itinerary)
542 const [tripDetailsTab, setTripDetailsTab] = useState<
543 "itinerary" | "included"
544 >("itinerary");
545
546 // Track visited sections for lazy loading and update URL
547 useEffect(() => {
548 setVisitedSections((prev) => new Set([...prev, currentSection]));
549
550 // Update URL with current section (without page reload)
551 const url = new URL(window.location.href);
552 url.searchParams.set("section", currentSection);
553 window.history.replaceState({}, "", url.toString());
554 }, [currentSection]);
555
556 // Modal states for adding items
557 const [showHighlightModal, setShowHighlightModal] = useState(false);
558 const [modalInput, setModalInput] = useState({
559 text: "",
560 question: "",
561 answer: "",
562 });
563
564 // Revision states
565 const [showRevisionsDialog, setShowRevisionsDialog] = useState(false);
566 const [selectedRevisionId, setSelectedRevisionId] = useState<number | null>(
567 null,
568 );
569 const [showRevisionConfirm, setShowRevisionConfirm] = useState(false);
570
571 // UI Enhancement states
572 const [simpleMode, setSimpleMode] = useState(false); // Quick Start mode
573 const [showSlugPreview, setShowSlugPreview] = useState(true);
574 const [dummyDataIndex, setDummyDataIndex] = useState(0); // Track which dummy data set to use
575
576 // Static/dummy revisions data for UI only
577 const dummyRevisions = [
578 {
579 id: 1,
580 version: 3,
581 created_by_name: "Admin User",
582 created_at: new Date().toISOString(),
583 },
584 {
585 id: 2,
586 version: 2,
587 created_by_name: "Admin User",
588 created_at: new Date(Date.now() - 86400000).toISOString(),
589 },
590 {
591 id: 3,
592 version: 1,
593 created_by_name: "Admin User",
594 created_at: new Date(Date.now() - 172800000).toISOString(),
595 },
596 ];
597
598 // Comprehensive dummy trip data sets (3 different trips)
599 const dummyTripsData: TripFormData[] = [
600 {
601 // Trip 1: Beach Adventure
602 title: "7-Day Bali Beach Adventure",
603 slug: "7-day-bali-beach-adventure",
604 description:
605 "Escape to paradise with our 7-day Bali beach adventure. Experience pristine beaches, explore ancient temples, enjoy world-class spa treatments, and immerse yourself in the rich Balinese culture. This carefully curated journey combines relaxation with adventure, offering the perfect balance for travelers seeking both tranquility and excitement.",
606 short_description:
607 "Escape to paradise with our 7-day Bali beach adventure featuring pristine beaches, ancient temples, and cultural immersion.",
608 highlights: [
609 "Pristine white sand beaches",
610 "Ancient temple visits",
611 "Traditional spa treatments",
612 "Cultural dance performances",
613 "Sunset dinners by the ocean",
614 ],
615 trip_details:
616 "This comprehensive 7-day journey takes you through the best of Bali. Start your adventure in Seminyak with its trendy beach clubs and world-class restaurants. Visit the iconic Tanah Lot Temple perched on a rock formation in the sea. Explore the cultural heart of Ubud, known for its rice terraces, monkey forest, and art galleries. Enjoy traditional Balinese spa treatments and witness captivating cultural performances. End your trip with a relaxing stay at a beachfront resort where you can unwind and reflect on your incredible journey.",
617 what_makes_special:
618 "This trip offers exclusive access to private beach areas, personalized cultural experiences, and a perfect blend of relaxation and adventure. Our local guides share insider knowledge and hidden gems that most tourists never discover.",
619 trip_story:
620 "Imagine waking up to the gentle sound of waves lapping against the shore. As the sun rises over the horizon, you step onto your private balcony to witness a breathtaking sunrise. Your day begins with a traditional Balinese breakfast before heading out to explore ancient temples that have stood for centuries. In the afternoon, you find yourself surrounded by emerald-green rice terraces, learning about traditional farming methods from local farmers. As evening approaches, you're treated to a mesmerizing cultural dance performance followed by a candlelit dinner on the beach. This is more than a vacation—it's a journey into the heart and soul of Bali.",
621 video_url: "https://www.youtube.com/watch?v=example1",
622 virtual_tour_url: "",
623 testimonial_review_ids: [], // Will be populated from actual reviews
624 destinations: [], // Will be populated based on available destinations
625 starting_location: "Ngurah Rai International Airport (DPS)",
626 ending_location: "Seminyak Beach Resort",
627 countries: ["Indonesia"],
628 regions: ["Bali"],
629 starting_latitude: "-8.3405",
630 starting_longitude: "115.0920",
631 ending_latitude: "-8.5069",
632 ending_longitude: "115.2625",
633 landmarks: [
634 "Tanah Lot Temple",
635 "Ubud Monkey Forest",
636 "Tegallalang Rice Terrace",
637 "Seminyak Beach",
638 ],
639 trip_type: "multi_day",
640 duration_days: "7",
641 duration_nights: "6",
642 available_from: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
643 .toISOString()
644 .split("T")[0], // 30 days from now
645 available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
646 .toISOString()
647 .split("T")[0], // 1 year from now
648 booking_window_days: "30",
649 seasonal_availability: "Year-round",
650 best_season: "April to October",
651 peak_season: "July to August",
652 off_season: "November to March",
653 activity_types: [], // Will be populated based on available activities
654 difficulty_level: "",
655 trip_category: [],
656 tags: ["family-friendly", "beach", "relaxation", "cultural", "spa"],
657 featured_priority: "featured",
658 accommodation_type: "Resort",
659 meal_plan: "breakfast",
660 accommodation_details:
661 "4-star beachfront resort with private balconies, infinity pool, and spa facilities",
662 transportation_included: true,
663 pickup_location: "Ngurah Rai International Airport",
664 dropoff_location: "Seminyak Beach Resort",
665 transportation_details:
666 "Private air-conditioned vehicle with professional driver",
667 pricing_type: "traveler_based",
668 original_price: "",
669 discounted_price: "",
670 price_types: [
671 { category_id: 1, original_price: "1250", discounted_price: "" },
672 { category_id: 2, original_price: "625", discounted_price: "" },
673 { category_id: 3, original_price: "0", discounted_price: "" },
674 ],
675 deposit_amount: "300",
676 deposit_percentage: "",
677 payment_terms:
678 "50% deposit required at booking, remaining 50% due 30 days before departure",
679 max_travelers: "12",
680 min_travelers: "2",
681 booking_deadline_hours: "24",
682 cancellation_policy:
683 "Free cancellation up to 30 days before departure. 50% refund for cancellations 15-30 days before. No refund for cancellations less than 15 days before.",
684 age_min: "8",
685 age_max: "",
686 physical_requirements:
687 "Moderate fitness level required. Some walking involved but no strenuous activities.",
688 visa_requirements:
689 "Visa on arrival available for most nationalities. Valid passport required with at least 6 months validity.",
690 vaccination_requirements:
691 "No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.",
692 disable_booking: false,
693 has_default_time_slots: false,
694 default_time_slots: [],
695 departure_time: "",
696 included_items: [
697 {
698 title: "Accommodation",
699 description: "6 nights at 4-star beachfront resort",
700 },
701 { title: "Breakfast", description: "Daily breakfast included" },
702 {
703 title: "Airport transfers",
704 description: "Private transfers to and from airport",
705 },
706 {
707 title: "Temple visits",
708 description: "Entrance fees to all temples included",
709 },
710 {
711 title: "Cultural performances",
712 description: "Traditional dance show tickets",
713 },
714 {
715 title: "Professional guide",
716 description: "English-speaking local guide",
717 },
718 ],
719 excluded_items: [
720 {
721 title: "International flights",
722 description: "Flights to and from Bali not included",
723 },
724 {
725 title: "Lunch and dinner",
726 description: "Meals other than breakfast",
727 },
728 {
729 title: "Travel insurance",
730 description: "Travel insurance recommended but not included",
731 },
732 {
733 title: "Personal expenses",
734 description: "Souvenirs, tips, and personal items",
735 },
736 ],
737 itinerary_days: [],
738 gallery_images: [],
739 featured_image: null,
740 downloadable_items: [],
741 faqs: [
742 {
743 question: "What is the best time to visit Bali?",
744 answer:
745 "The best time to visit Bali is during the dry season from April to October, when you can expect sunny days and minimal rainfall.",
746 },
747 {
748 question: "Do I need a visa?",
749 answer:
750 "Most nationalities can get a visa on arrival at the airport. A valid passport with at least 6 months validity is required.",
751 },
752 {
753 question: "What should I pack?",
754 answer:
755 "Pack light, breathable clothing, swimwear, sunscreen, insect repellent, and comfortable walking shoes. Modest clothing is required for temple visits.",
756 },
757 ],
758 frontend_tabs: [
759 {
760 id: "overview",
761 label: "Overview",
762 enabled: true,
763 order: 1,
764 content_type: "general",
765 icon: { type: "icon", value: "book-open" },
766 },
767 {
768 id: "itinerary",
769 label: "Itinerary",
770 enabled: true,
771 order: 2,
772 content_type: "itinerary",
773 icon: { type: "icon", value: "calendar" },
774 },
775 {
776 id: "included",
777 label: "Included",
778 enabled: true,
779 order: 3,
780 content_type: "included_excluded",
781 icon: { type: "icon", value: "check" },
782 },
783 {
784 id: "location",
785 label: "Location",
786 enabled: true,
787 order: 4,
788 content_type: "gallery",
789 icon: { type: "icon", value: "map-pin" },
790 },
791 {
792 id: "important_info",
793 label: "Important Info",
794 enabled: true,
795 order: 5,
796 content_type: "general",
797 icon: { type: "icon", value: "file-text" },
798 },
799 {
800 id: "downloads",
801 label: "Downloads",
802 enabled: true,
803 order: 6,
804 content_type: "downloads",
805 icon: { type: "icon", value: "download" },
806 },
807 {
808 id: "faq",
809 label: "FAQ",
810 enabled: true,
811 order: 7,
812 content_type: "faqs",
813 icon: { type: "icon", value: "help-circle" },
814 },
815 {
816 id: "trip_story",
817 label: "Story",
818 enabled: true,
819 order: 8,
820 content_type: "custom",
821 custom_content: "",
822 icon: { type: "icon", value: "book" },
823 },
824 {
825 id: "what_makes_special",
826 label: "Special",
827 enabled: true,
828 order: 9,
829 content_type: "custom",
830 custom_content: "",
831 icon: { type: "icon", value: "star" },
832 },
833 {
834 id: "testimonials",
835 label: "Testimonials",
836 enabled: true,
837 order: 10,
838 content_type: "reviews",
839 icon: { type: "icon", value: "message-circle" },
840 },
841 ],
842 availability_dates: [],
843 status: "draft",
844 scheduled_publish_date: "",
845 scheduled_unpublish_date: "",
846 version: 1,
847 seasonal_auto_enable: false,
848 seasonal_enable_date: "",
849 seasonal_disable_date: "",
850 meta_title: "7-Day Bali Beach Adventure | Luxury Beach Resort Experience",
851 meta_description:
852 "Experience the best of Bali with our 7-day beach adventure. Pristine beaches, ancient temples, cultural immersion, and world-class spa treatments await.",
853 meta_keywords:
854 "Bali, beach vacation, cultural tour, spa retreat, Indonesia travel",
855 attributes: {},
856 },
857 {
858 // Trip 2: Mountain Trekking Adventure
859 title: "Everest Base Camp Trek - 14 Days",
860 slug: "everest-base-camp-trek-14-days",
861 description:
862 "Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek. This challenging yet rewarding journey takes you through the heart of the Himalayas, passing through traditional Sherpa villages, ancient monasteries, and breathtaking mountain landscapes. Experience the rich culture of the Khumbu region while pushing your limits to reach the base of the world's highest mountain.",
863 short_description:
864 "Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek through the heart of the Himalayas.",
865 highlights: [
866 "Trek to Everest Base Camp (5,364m)",
867 "Visit ancient Buddhist monasteries",
868 "Experience Sherpa culture",
869 "Breathtaking mountain views",
870 "Professional mountain guides",
871 ],
872 trip_details:
873 "This 14-day trekking adventure begins in Kathmandu, where you'll prepare for your journey and meet your experienced guides. Fly to Lukla, the gateway to the Khumbu region, and begin your trek through the stunning Himalayan landscape. Pass through traditional Sherpa villages like Namche Bazaar, Tengboche, and Dingboche, each offering unique cultural experiences and acclimatization opportunities. Visit ancient monasteries, learn about Sherpa traditions, and witness the daily life of mountain communities. The journey culminates at Everest Base Camp, where you'll stand in the shadow of the world's highest peak. Along the way, you'll be treated to spectacular views of peaks like Ama Dablam, Lhotse, and of course, Mount Everest itself.",
874 what_makes_special:
875 "Our trek includes experienced mountain guides, proper acclimatization schedules, high-altitude porters, and comprehensive safety equipment. We prioritize responsible tourism, supporting local communities and ensuring minimal environmental impact.",
876 trip_story:
877 "The crisp mountain air fills your lungs as you take your first steps on the trail. With each passing day, the mountains grow larger, the air thinner, and the sense of accomplishment greater. You wake before dawn to witness the sun painting the peaks in shades of gold and pink. You share meals with Sherpa families, learning about their way of life and the challenges they face in this harsh yet beautiful environment. As you approach Base Camp, the anticipation builds. When you finally arrive, standing at 5,364 meters with Everest towering above, you realize this is more than a trek—it's a transformation. The journey changes you, teaching resilience, appreciation for nature, and respect for the mountains and the people who call them home.",
878 video_url: "https://www.youtube.com/watch?v=example2",
879 virtual_tour_url: "",
880 testimonial_review_ids: [], // Will be populated from actual reviews
881 destinations: [], // Will be populated
882 starting_location: "Kathmandu International Airport",
883 ending_location: "Lukla Airport",
884 countries: ["Nepal"],
885 regions: ["Khumbu Region"],
886 starting_latitude: "27.9881",
887 starting_longitude: "86.9250",
888 ending_latitude: "27.6837",
889 ending_longitude: "86.7330",
890 landmarks: [
891 "Mount Everest",
892 "Namche Bazaar",
893 "Tengboche Monastery",
894 "Kala Patthar",
895 ],
896 trip_type: "multi_day",
897 duration_days: "14",
898 duration_nights: "13",
899 available_from: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000)
900 .toISOString()
901 .split("T")[0],
902 available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
903 .toISOString()
904 .split("T")[0],
905 booking_window_days: "60",
906 seasonal_availability: "March to May, September to November",
907 best_season: "October to November",
908 peak_season: "October to November",
909 off_season: "December to February, June to August",
910 activity_types: [],
911 difficulty_level: "",
912 trip_category: [],
913 tags: ["trekking", "mountains", "adventure", "challenging", "everest"],
914 featured_priority: "featured",
915 accommodation_type: "Teahouse",
916 meal_plan: "full_board",
917 accommodation_details:
918 "Traditional teahouses along the route with basic amenities. Rooms are shared, and facilities become more basic as altitude increases.",
919 transportation_included: true,
920 pickup_location: "Kathmandu International Airport",
921 dropoff_location: "Lukla Airport",
922 transportation_details:
923 "Domestic flights Kathmandu-Lukla-Kathmandu included. Airport transfers included.",
924 pricing_type: "regular",
925 original_price: "1899",
926 discounted_price: "1699",
927 deposit_amount: "500",
928 deposit_percentage: "",
929 payment_terms:
930 "50% deposit required at booking, remaining 50% due 60 days before departure",
931 max_travelers: "12",
932 min_travelers: "2",
933 booking_deadline_hours: "24",
934 cancellation_policy:
935 "Free cancellation up to 60 days before departure. 50% refund for cancellations 30-60 days before. No refund for cancellations less than 30 days before.",
936 age_min: "18",
937 age_max: "65",
938 physical_requirements:
939 "Excellent physical fitness required. Previous trekking experience recommended. Must be able to walk 6-8 hours daily at high altitude.",
940 visa_requirements:
941 "Tourist visa required for Nepal. Can be obtained on arrival at airport or in advance. Valid passport required.",
942 vaccination_requirements:
943 "Recommended: Hepatitis A, Typhoid, Japanese Encephalitis, and routine vaccinations. Consult with travel health clinic.",
944 disable_booking: false,
945 has_default_time_slots: false,
946 default_time_slots: [],
947 departure_time: "",
948 included_items: [
949 {
950 title: "Accommodation",
951 description: "13 nights in teahouses along the route",
952 },
953 {
954 title: "All meals",
955 description: "Breakfast, lunch, and dinner included",
956 },
957 {
958 title: "Professional guides",
959 description: "Experienced mountain guides and porters",
960 },
961 {
962 title: "Permits",
963 description: "TIMS and Sagarmatha National Park permits",
964 },
965 {
966 title: "Domestic flights",
967 description: "Kathmandu-Lukla-Kathmandu flights",
968 },
969 {
970 title: "Equipment",
971 description: "Sleeping bag and down jacket rental",
972 },
973 ],
974 excluded_items: [
975 {
976 title: "International flights",
977 description: "Flights to and from Kathmandu",
978 },
979 {
980 title: "Travel insurance",
981 description: "Comprehensive travel and medical insurance required",
982 },
983 {
984 title: "Personal equipment",
985 description: "Trekking boots, clothing, and personal items",
986 },
987 {
988 title: "Tips",
989 description: "Tips for guides and porters (recommended)",
990 },
991 {
992 title: "Personal expenses",
993 description: "Drinks, snacks, and souvenirs",
994 },
995 ],
996 itinerary_days: [],
997 gallery_images: [],
998 featured_image: null,
999 downloadable_items: [],
1000 faqs: [
1001 {
1002 question: "How difficult is the trek?",
1003 answer:
1004 "This is a challenging trek requiring excellent physical fitness. You'll be walking 6-8 hours daily at high altitude. Previous trekking experience is recommended.",
1005 },
1006 {
1007 question: "What is the altitude at Base Camp?",
1008 answer:
1009 "Everest Base Camp is located at 5,364 meters (17,598 feet) above sea level.",
1010 },
1011 {
1012 question: "What happens if I get altitude sickness?",
1013 answer:
1014 "Our guides are trained to recognize and treat altitude sickness. We have proper acclimatization schedules and emergency descent plans in place.",
1015 },
1016 ],
1017 frontend_tabs: [
1018 {
1019 id: "overview",
1020 label: "Overview",
1021 enabled: true,
1022 order: 1,
1023 content_type: "general",
1024 icon: { type: "icon", value: "book-open" },
1025 },
1026 {
1027 id: "itinerary",
1028 label: "Itinerary",
1029 enabled: true,
1030 order: 2,
1031 content_type: "itinerary",
1032 icon: { type: "icon", value: "calendar" },
1033 },
1034 {
1035 id: "included",
1036 label: "Included",
1037 enabled: true,
1038 order: 3,
1039 content_type: "included_excluded",
1040 icon: { type: "icon", value: "check" },
1041 },
1042 {
1043 id: "location",
1044 label: "Location",
1045 enabled: true,
1046 order: 4,
1047 content_type: "gallery",
1048 icon: { type: "icon", value: "map-pin" },
1049 },
1050 {
1051 id: "important_info",
1052 label: "Important Info",
1053 enabled: true,
1054 order: 5,
1055 content_type: "general",
1056 icon: { type: "icon", value: "info" },
1057 },
1058 {
1059 id: "downloads",
1060 label: "Downloads",
1061 enabled: true,
1062 order: 6,
1063 content_type: "downloads",
1064 icon: { type: "icon", value: "download" },
1065 },
1066 {
1067 id: "faq",
1068 label: "FAQ",
1069 enabled: true,
1070 order: 7,
1071 content_type: "faqs",
1072 icon: { type: "icon", value: "help-circle" },
1073 },
1074 {
1075 id: "trip_story",
1076 label: "Story",
1077 enabled: true,
1078 order: 8,
1079 content_type: "custom",
1080 custom_content: "",
1081 icon: { type: "icon", value: "book" },
1082 },
1083 {
1084 id: "what_makes_special",
1085 label: "Special",
1086 enabled: true,
1087 order: 9,
1088 content_type: "custom",
1089 custom_content: "",
1090 icon: { type: "icon", value: "star" },
1091 },
1092 {
1093 id: "testimonials",
1094 label: "Testimonials",
1095 enabled: true,
1096 order: 10,
1097 content_type: "reviews",
1098 icon: { type: "icon", value: "message-circle" },
1099 },
1100 ],
1101 availability_dates: [],
1102 status: "draft",
1103 scheduled_publish_date: "",
1104 scheduled_unpublish_date: "",
1105 version: 1,
1106 seasonal_auto_enable: false,
1107 seasonal_enable_date: "",
1108 seasonal_disable_date: "",
1109 meta_title:
1110 "Everest Base Camp Trek - 14 Days | Ultimate Himalayan Adventure",
1111 meta_description:
1112 "Embark on the adventure of a lifetime with our 14-day Everest Base Camp trek. Experience Sherpa culture, ancient monasteries, and breathtaking mountain views.",
1113 meta_keywords:
1114 "Everest Base Camp, trekking, Nepal, Himalayas, adventure travel, mountain trek",
1115 attributes: {},
1116 price_types: [],
1117 },
1118 {
1119 // Trip 3: European City Tour
1120 title: "European Grand Tour - 10 Days",
1121 slug: "european-grand-tour-10-days",
1122 description:
1123 "Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona. Experience world-famous landmarks, indulge in exquisite cuisine, explore rich history and art, and immerse yourself in diverse European cultures. This carefully crafted journey takes you through three of Europe's most iconic cities, each offering unique experiences and unforgettable memories.",
1124 short_description:
1125 "Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona.",
1126 highlights: [
1127 "Eiffel Tower and Louvre Museum",
1128 "Colosseum and Vatican City",
1129 "Sagrada Familia and Park Güell",
1130 "World-class cuisine",
1131 "Professional local guides",
1132 ],
1133 trip_details:
1134 "Begin your European adventure in the City of Light—Paris. Explore iconic landmarks like the Eiffel Tower, Louvre Museum, and Notre-Dame Cathedral. Stroll along the Champs-Élysées, enjoy a Seine River cruise, and indulge in French pastries at charming cafés. Next, travel to Rome, the Eternal City, where ancient history comes alive. Visit the Colosseum, Roman Forum, and Vatican City with its stunning Sistine Chapel. Enjoy authentic Italian cuisine and gelato while exploring cobblestone streets. Conclude your journey in Barcelona, Spain's vibrant cultural capital. Admire Gaudí's architectural masterpieces including the Sagrada Familia and Park Güell. Experience the lively atmosphere of Las Ramblas, enjoy tapas and sangria, and relax on beautiful Mediterranean beaches.",
1135 what_makes_special:
1136 "This tour includes skip-the-line tickets to major attractions, private guided tours in each city, and carefully selected accommodations in prime locations. Our small group size ensures personalized attention and authentic local experiences.",
1137 trip_story:
1138 "Your European adventure begins as you step off the plane in Paris, greeted by the elegant architecture and romantic atmosphere that has inspired artists for centuries. Each day brings new discoveries—from the artistic treasures of the Louvre to the bohemian charm of Montmartre. In Rome, you walk in the footsteps of emperors and gladiators, feeling the weight of history in every ancient stone. The Vatican's art and architecture leave you in awe, while a simple plate of pasta in a local trattoria reminds you that the best experiences are often the simplest. Barcelona welcomes you with its unique blend of Gothic and Modernist architecture, vibrant street life, and Mediterranean warmth. As you watch the sunset from Park Güell, you realize that this journey has not just shown you three cities—it has shown you three different ways of living, three different approaches to art and culture, and three different reasons to fall in love with Europe.",
1139 video_url: "https://www.youtube.com/watch?v=example3",
1140 virtual_tour_url: "",
1141 testimonial_review_ids: [], // Will be populated from actual reviews
1142 destinations: [],
1143 starting_location: "Charles de Gaulle Airport (CDG)",
1144 ending_location: "El Prat Airport (BCN)",
1145 countries: ["France", "Italy", "Spain"],
1146 regions: ["Île-de-France", "Lazio", "Catalonia"],
1147 starting_latitude: "48.8566",
1148 starting_longitude: "2.3522",
1149 ending_latitude: "41.3792",
1150 ending_longitude: "2.1281",
1151 landmarks: [
1152 "Eiffel Tower",
1153 "Colosseum",
1154 "Sagrada Familia",
1155 "Louvre Museum",
1156 "Vatican City",
1157 ],
1158 trip_type: "multi_day",
1159 duration_days: "10",
1160 duration_nights: "9",
1161 available_from: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000)
1162 .toISOString()
1163 .split("T")[0],
1164 available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
1165 .toISOString()
1166 .split("T")[0],
1167 booking_window_days: "45",
1168 seasonal_availability: "Year-round",
1169 best_season: "April to June, September to October",
1170 peak_season: "June to August",
1171 off_season: "November to March",
1172 activity_types: [],
1173 difficulty_level: "",
1174 trip_category: [],
1175 tags: ["cultural", "city-tour", "history", "art", "food"],
1176 featured_priority: "featured",
1177 accommodation_type: "Hotel",
1178 meal_plan: "breakfast",
1179 accommodation_details:
1180 "4-star hotels in city centers with easy access to major attractions",
1181 transportation_included: true,
1182 pickup_location: "Charles de Gaulle Airport",
1183 dropoff_location: "El Prat Airport",
1184 transportation_details:
1185 "High-speed train between cities, private transfers, and metro passes included",
1186 pricing_type: "regular",
1187 original_price: "2499",
1188 discounted_price: "",
1189 price_types: [],
1190 deposit_amount: "600",
1191 deposit_percentage: "",
1192 payment_terms:
1193 "40% deposit required at booking, remaining 60% due 45 days before departure",
1194 max_travelers: "20",
1195 min_travelers: "4",
1196 booking_deadline_hours: "24",
1197 cancellation_policy:
1198 "Free cancellation up to 45 days before departure. 75% refund for cancellations 30-45 days before. 50% refund for cancellations 15-30 days before. No refund for cancellations less than 15 days before.",
1199 age_min: "",
1200 age_max: "",
1201 physical_requirements:
1202 "Moderate walking required. Some sites involve stairs and uneven surfaces. Suitable for most fitness levels.",
1203 visa_requirements:
1204 "Schengen visa required for most non-EU nationals. Apply well in advance as processing can take several weeks.",
1205 vaccination_requirements:
1206 "No mandatory vaccinations. Routine vaccinations recommended.",
1207 disable_booking: false,
1208 has_default_time_slots: false,
1209 default_time_slots: [],
1210 departure_time: "",
1211 included_items: [
1212 {
1213 title: "Accommodation",
1214 description: "9 nights in 4-star city center hotels",
1215 },
1216 { title: "Breakfast", description: "Daily breakfast included" },
1217 {
1218 title: "Transportation",
1219 description:
1220 "High-speed trains, airport transfers, and city metro passes",
1221 },
1222 {
1223 title: "Guided tours",
1224 description: "Professional local guides in each city",
1225 },
1226 {
1227 title: "Skip-the-line tickets",
1228 description: "Priority access to major attractions",
1229 },
1230 {
1231 title: "Welcome dinner",
1232 description: "Traditional welcome dinner in Paris",
1233 },
1234 ],
1235 excluded_items: [
1236 {
1237 title: "International flights",
1238 description: "Flights to Paris and from Barcelona",
1239 },
1240 {
1241 title: "Lunch and dinner",
1242 description: "Meals other than breakfast and welcome dinner",
1243 },
1244 {
1245 title: "Travel insurance",
1246 description: "Travel insurance recommended",
1247 },
1248 {
1249 title: "Personal expenses",
1250 description: "Souvenirs, tips, and personal items",
1251 },
1252 ],
1253 itinerary_days: [],
1254 gallery_images: [],
1255 featured_image: null,
1256 downloadable_items: [],
1257 faqs: [
1258 {
1259 question: "Do I need a visa?",
1260 answer:
1261 "Most non-EU nationals need a Schengen visa. Apply at the embassy of your first entry country (France) well in advance.",
1262 },
1263 {
1264 question: "What languages are spoken?",
1265 answer:
1266 "English-speaking guides provided. Local languages are French, Italian, and Spanish, but English is widely spoken in tourist areas.",
1267 },
1268 {
1269 question: "Is this suitable for families?",
1270 answer:
1271 "Yes, this tour is family-friendly. However, some museums and sites may have age restrictions for children.",
1272 },
1273 ],
1274 frontend_tabs: [
1275 {
1276 id: "overview",
1277 label: "Overview",
1278 enabled: true,
1279 order: 1,
1280 content_type: "general",
1281 icon: { type: "icon", value: "book-open" },
1282 },
1283 {
1284 id: "itinerary",
1285 label: "Itinerary",
1286 enabled: true,
1287 order: 2,
1288 content_type: "itinerary",
1289 icon: { type: "icon", value: "calendar" },
1290 },
1291 {
1292 id: "included",
1293 label: "Included",
1294 enabled: true,
1295 order: 3,
1296 content_type: "included_excluded",
1297 icon: { type: "icon", value: "check" },
1298 },
1299 {
1300 id: "location",
1301 label: "Location",
1302 enabled: true,
1303 order: 4,
1304 content_type: "gallery",
1305 icon: { type: "icon", value: "map-pin" },
1306 },
1307 {
1308 id: "important_info",
1309 label: "Important Info",
1310 enabled: true,
1311 order: 5,
1312 content_type: "general",
1313 icon: { type: "icon", value: "info" },
1314 },
1315 {
1316 id: "downloads",
1317 label: "Downloads",
1318 enabled: true,
1319 order: 6,
1320 content_type: "downloads",
1321 icon: { type: "icon", value: "download" },
1322 },
1323 {
1324 id: "faq",
1325 label: "FAQ",
1326 enabled: true,
1327 order: 7,
1328 content_type: "faqs",
1329 icon: { type: "icon", value: "help-circle" },
1330 },
1331 {
1332 id: "trip_story",
1333 label: "Story",
1334 enabled: true,
1335 order: 8,
1336 content_type: "custom",
1337 custom_content: "",
1338 icon: { type: "icon", value: "book" },
1339 },
1340 {
1341 id: "what_makes_special",
1342 label: "Special",
1343 enabled: true,
1344 order: 9,
1345 content_type: "custom",
1346 custom_content: "",
1347 icon: { type: "icon", value: "star" },
1348 },
1349 {
1350 id: "testimonials",
1351 label: "Testimonials",
1352 enabled: true,
1353 order: 10,
1354 content_type: "reviews",
1355 icon: { type: "icon", value: "message-circle" },
1356 },
1357 ],
1358 availability_dates: [],
1359 status: "draft",
1360 scheduled_publish_date: "",
1361 scheduled_unpublish_date: "",
1362 version: 1,
1363 seasonal_auto_enable: false,
1364 seasonal_enable_date: "",
1365 seasonal_disable_date: "",
1366 meta_title: "European Grand Tour - 10 Days | Paris, Rome & Barcelona",
1367 meta_description:
1368 "Discover the best of Europe with our 10-day grand tour covering Paris, Rome, and Barcelona. Experience iconic landmarks, world-class cuisine, and rich history.",
1369 meta_keywords:
1370 "Europe tour, Paris, Rome, Barcelona, European travel, cultural tour",
1371 attributes: {},
1372 },
1373 ];
1374
1375 const [formData, setFormData] = useState<TripFormData>({
1376 title: "",
1377 slug: "",
1378 description: "",
1379 highlights: [],
1380 trip_details: "",
1381 short_description: "",
1382 what_makes_special: "",
1383 trip_story: "",
1384 video_url: "",
1385 virtual_tour_url: "",
1386 testimonial_review_ids: [],
1387 destinations: [], // Array of destination IDs
1388 starting_location: "",
1389 ending_location: "",
1390 countries: [],
1391 regions: [],
1392 starting_latitude: "",
1393 starting_longitude: "",
1394 ending_latitude: "",
1395 ending_longitude: "",
1396 landmarks: [],
1397 trip_type: "multi_day",
1398 duration_days: "",
1399 duration_nights: "",
1400 available_from: "",
1401 available_to: "",
1402 booking_window_days: "",
1403 seasonal_availability: "",
1404 best_season: "",
1405 peak_season: "",
1406 off_season: "",
1407 activity_types: [], // Array of activity IDs
1408 difficulty_level: "",
1409 trip_category: [],
1410 tags: [],
1411 featured_priority: "none",
1412 accommodation_type: "",
1413 meal_plan: "",
1414 accommodation_details: "",
1415 transportation_included: false,
1416 pickup_location: "",
1417 dropoff_location: "",
1418 transportation_details: "",
1419 pricing_type: "regular",
1420 original_price: "",
1421 discounted_price: "",
1422 price_types: [],
1423 deposit_amount: "",
1424 deposit_percentage: "",
1425 payment_terms: "",
1426 max_travelers: "10",
1427 min_travelers: "2",
1428 booking_deadline_hours: "24",
1429 cancellation_policy: "Flexible",
1430 age_min: "18",
1431 age_max: "65",
1432 physical_requirements: "Moderate",
1433 visa_requirements: "Schengen visa",
1434 vaccination_requirements: "COVID-19 vaccination",
1435 disable_booking: false,
1436 has_default_time_slots: false,
1437 default_time_slots: [],
1438 departure_time: "09:00",
1439 included_items: [],
1440 excluded_items: [],
1441 attributes: {}, // attribute_id -> value mapping
1442 itinerary_days: [],
1443 gallery_images: [],
1444 featured_image: null,
1445 downloadable_items: [],
1446 faqs: [],
1447 frontend_tabs: [
1448 {
1449 id: "overview",
1450 label: "Overview",
1451 enabled: true,
1452 order: 1,
1453 content_type: "general",
1454 icon: { type: "icon", value: "book-open" },
1455 },
1456 {
1457 id: "itinerary",
1458 label: "Itinerary",
1459 enabled: true,
1460 order: 2,
1461 content_type: "itinerary",
1462 icon: { type: "icon", value: "calendar" },
1463 },
1464 {
1465 id: "included",
1466 label: "Included",
1467 enabled: true,
1468 order: 3,
1469 content_type: "included_excluded",
1470 icon: { type: "icon", value: "check" },
1471 },
1472 {
1473 id: "location",
1474 label: "Location",
1475 enabled: true,
1476 order: 4,
1477 content_type: "gallery",
1478 icon: { type: "icon", value: "map-pin" },
1479 },
1480 {
1481 id: "important_info",
1482 label: "Important Info",
1483 enabled: true,
1484 order: 5,
1485 content_type: "general",
1486 icon: { type: "icon", value: "info" },
1487 },
1488 {
1489 id: "downloads",
1490 label: "Downloads",
1491 enabled: true,
1492 order: 6,
1493 content_type: "downloads",
1494 icon: { type: "icon", value: "download" },
1495 },
1496 {
1497 id: "faq",
1498 label: "FAQ",
1499 enabled: true,
1500 order: 7,
1501 content_type: "faqs",
1502 icon: { type: "icon", value: "help-circle" },
1503 },
1504 {
1505 id: "trip_story",
1506 label: "Story",
1507 enabled: true,
1508 order: 8,
1509 content_type: "custom",
1510 custom_content: "",
1511 icon: { type: "icon", value: "book" },
1512 },
1513 {
1514 id: "what_makes_special",
1515 label: "Special",
1516 enabled: true,
1517 order: 9,
1518 content_type: "custom",
1519 custom_content: "",
1520 icon: { type: "icon", value: "star" },
1521 },
1522 {
1523 id: "testimonials",
1524 label: "Testimonials",
1525 enabled: true,
1526 order: 10,
1527 content_type: "reviews",
1528 icon: { type: "icon", value: "message-circle" },
1529 },
1530 ],
1531 availability_dates: [],
1532 status: "draft",
1533 scheduled_publish_date: "",
1534 scheduled_unpublish_date: "",
1535 version: 1,
1536 seasonal_auto_enable: false,
1537 seasonal_enable_date: "",
1538 seasonal_disable_date: "",
1539 meta_title: "",
1540 meta_description: "",
1541 meta_keywords: "",
1542 });
1543
1544 const [errors, setErrors] = useState<Record<string, string>>({});
1545 const [isSubmitting, setIsSubmitting] = useState(false);
1546 const [copiedErrorDetails, setCopiedErrorDetails] = useState(false);
1547
1548 const copyErrorDetailsToClipboard = (text: string) => {
1549 if (!text) return;
1550 const fallbackCopy = () => {
1551 const textarea = document.createElement("textarea");
1552 textarea.value = text;
1553 textarea.style.position = "fixed";
1554 textarea.style.opacity = "0";
1555 document.body.appendChild(textarea);
1556 textarea.select();
1557 try {
1558 document.execCommand("copy");
1559 } catch (e) {
1560 // ignore
1561 }
1562 document.body.removeChild(textarea);
1563 setCopiedErrorDetails(true);
1564 setTimeout(() => setCopiedErrorDetails(false), 1500);
1565 };
1566
1567 try {
1568 if (navigator?.clipboard?.writeText) {
1569 navigator.clipboard
1570 .writeText(text)
1571 .then(() => {
1572 setCopiedErrorDetails(true);
1573 setTimeout(() => setCopiedErrorDetails(false), 1500);
1574 })
1575 .catch(() => fallbackCopy());
1576 } else {
1577 fallbackCopy();
1578 }
1579 } catch {
1580 fallbackCopy();
1581 }
1582 };
1583
1584 // Get action and id from URL
1585 const action = useMemo(() => {
1586 const params = new URLSearchParams(window.location.search);
1587 return params.get("action") || "create";
1588 }, []);
1589
1590 const tripId = useMemo(() => {
1591 const params = new URLSearchParams(window.location.search);
1592 return params.get("id") ? parseInt(params.get("id") || "0") : null;
1593 }, []);
1594
1595 const isEditMode = action === "edit" && tripId !== null;
1596
1597 // Fetch traveler categories - LAZY LOAD: only when pricing section is visited
1598 const { data: travelerCategoriesData, isLoading: isLoadingCategories } =
1599 useQuery({
1600 queryKey: ["traveler-categories"],
1601 queryFn: async () => {
1602 try {
1603 const response = await apiClient.get("/traveler-categories", {
1604 params: {
1605 per_page: 100,
1606 status: "publish", // Only get published categories
1607 },
1608 });
1609 const categories =
1610 response?.data?.data || response?.data || response || [];
1611 return Array.isArray(categories) ? categories : [];
1612 } catch (error: any) {
1613 showToast(
1614 error?.message || __("Failed to load traveler categories", "yatra"),
1615 "error",
1616 );
1617 return [];
1618 }
1619 },
1620 enabled:
1621 can("yatra_view_trips") &&
1622 (currentSection === "pricing" || visitedSections.has("pricing")),
1623 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1624 });
1625
1626 // Get only active/published categories
1627 const activeCategories = useMemo(() => {
1628 const categories = travelerCategoriesData || [];
1629 return categories.filter(
1630 (cat: TravelerCategory) =>
1631 cat.status === "active" || cat.status === "publish",
1632 );
1633 }, [travelerCategoriesData]);
1634
1635 // Fetch settings to get global currency
1636 const { data: settingsData } = useQuery({
1637 queryKey: ["settings"],
1638 queryFn: async () => {
1639 try {
1640 const response = await fetchSettings();
1641 return response;
1642 } catch (error: any) {
1643 // Return default currency if settings fetch fails
1644 return { currency: "USD" };
1645 }
1646 },
1647 enabled: can("yatra_view_trips"),
1648 });
1649
1650 // Get global currency from settings, default to USD
1651 const cur = settingsData?.currency;
1652 const defCur = settingsData?.default_currency;
1653 const globalCurrency =
1654 typeof cur === "string" && cur.length > 0
1655 ? cur
1656 : typeof defCur === "string" && defCur.length > 0
1657 ? defCur
1658 : "USD";
1659
1660 // Fetch activities from API - LAZY LOAD: only when categorization section is visited
1661 const { data: activitiesData } = useQuery({
1662 queryKey: ["activities-published"],
1663 queryFn: async () => {
1664 try {
1665 const response = await apiClient.get("/activities", {
1666 params: {
1667 per_page: 100,
1668 status: "publish", // Only get published activities
1669 },
1670 });
1671 return response.data || [];
1672 } catch (error: any) {
1673 showToast(
1674 error?.message || __("Failed to load activities", "yatra"),
1675 "error",
1676 );
1677 return [];
1678 }
1679 },
1680 enabled:
1681 can("yatra_view_trips") &&
1682 (currentSection === "categorization" ||
1683 visitedSections.has("categorization")),
1684 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1685 });
1686
1687 // LAZY LOAD: only when categorization section is visited
1688 const { data: tripCategoriesResponse } = useQuery({
1689 queryKey: ["trip-categories", "published"],
1690 queryFn: async () => {
1691 try {
1692 const response = await apiClient.get("/trip-categories", {
1693 params: {
1694 per_page: 100,
1695 status: "publish",
1696 hierarchical: true,
1697 orderby: "name",
1698 order: "ASC",
1699 },
1700 });
1701 return response;
1702 } catch (error: any) {
1703 showToast(
1704 error?.message || __("Failed to load trip categories", "yatra"),
1705 "error",
1706 );
1707 return [];
1708 }
1709 },
1710 enabled: can("yatra_view_trips"),
1711 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1712 });
1713
1714 // LAZY LOAD: only when categorization section is visited
1715 const {
1716 data: difficultyLevelsResponse,
1717 isLoading: isLoadingDifficultyLevels,
1718 } = useQuery({
1719 queryKey: ["difficulty-levels", "published"],
1720 queryFn: async () => {
1721 try {
1722 const response = await apiClient.get("/difficulty-levels", {
1723 params: {
1724 per_page: 100,
1725 status: "publish",
1726 orderby: "sorting",
1727 order: "ASC",
1728 },
1729 });
1730 return response.data || [];
1731 } catch (error: any) {
1732 showToast(
1733 error?.message || __("Failed to load difficulty levels", "yatra"),
1734 "error",
1735 );
1736 return [];
1737 }
1738 },
1739 enabled:
1740 can("yatra_view_trips") &&
1741 (currentSection === "categorization" ||
1742 visitedSections.has("categorization")),
1743 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1744 });
1745
1746 const tripCategories: TripCategoryOption[] = useMemo(() => {
1747 const payload = extractArrayPayload(tripCategoriesResponse);
1748 return (payload as TripCategoryOption[]).filter((category) => {
1749 if (!category) return false;
1750 if (typeof category !== "object") return false;
1751 if (
1752 "status" in category &&
1753 category.status &&
1754 category.status !== "publish"
1755 ) {
1756 return false;
1757 }
1758 return true;
1759 });
1760 }, [tripCategoriesResponse]);
1761
1762 const difficultyLevels: DifficultyLevelOption[] = useMemo(() => {
1763 const payload = extractArrayPayload(difficultyLevelsResponse);
1764 return (payload as DifficultyLevelOption[]).filter((level) => {
1765 if (!level) return false;
1766 if (typeof level !== "object") return false;
1767 if ("status" in level && level.status && level.status !== "publish") {
1768 return false;
1769 }
1770 return true;
1771 });
1772 }, [difficultyLevelsResponse]);
1773
1774 // Fetch destinations from API - LAZY LOAD: only when location section is visited
1775 const { data: destinationsData } = useQuery({
1776 queryKey: ["destinations-published"],
1777 queryFn: async () => {
1778 try {
1779 const response = await apiClient.get("/destinations", {
1780 params: {
1781 per_page: 100,
1782 status: "publish", // Only get published destinations
1783 },
1784 });
1785 return response.data || [];
1786 } catch (error: any) {
1787 showToast(
1788 error?.message || __("Failed to load destinations", "yatra"),
1789 "error",
1790 );
1791 return [];
1792 }
1793 },
1794 enabled:
1795 can("yatra_view_trips") &&
1796 (isEditMode ||
1797 currentSection === "location" ||
1798 visitedSections.has("location")),
1799 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1800 });
1801
1802 // Fetch trip data if editing
1803 const {
1804 data: tripData,
1805 isLoading: isLoadingTrip,
1806 error: tripError,
1807 } = useQuery({
1808 queryKey: ["trip", tripId],
1809 queryFn: async () => {
1810 if (!tripId) return null;
1811 try {
1812 const response = await apiClient.get(`/trips/${tripId}`);
1813 // WordPress REST API returns data directly, but check if it's wrapped
1814 // Some endpoints return { data: {...} }, others return {...} directly
1815 const tripData = response?.data || response;
1816 return tripData;
1817 } catch (error: any) {
1818 console.error("Error loading trip:", error);
1819 showToast(
1820 error?.message || __("Failed to load trip data", "yatra"),
1821 "error",
1822 );
1823 throw error;
1824 }
1825 },
1826 enabled: !!tripId && isEditMode,
1827 staleTime: 0, // Always fetch fresh data to ensure downloadable items persist after save
1828 });
1829
1830 // Build error context for edit error state
1831 const tripErrorContext = useMemo(
1832 () => getErrorContext(tripError),
1833 [tripError],
1834 );
1835
1836 // Fetch trip attributes if editing (TripAttributesSection waits until resolved to avoid empty-first-render bug)
1837 const tripAttributesQueryEnabled = Boolean(tripId && isEditMode);
1838 const { data: tripAttributesData, isPending: isTripAttributesPending } =
1839 useQuery({
1840 queryKey: ["trip-attributes", tripId],
1841 queryFn: async () => {
1842 if (!tripId) return {};
1843 try {
1844 const response = await apiClient.get(`/trips/${tripId}/attributes`);
1845
1846 // Try different ways to extract the data
1847 let payload = response?.data;
1848 if (!payload || !Array.isArray(payload)) {
1849 payload = response;
1850 }
1851 if (!payload || !Array.isArray(payload)) {
1852 payload = response?.data?.data;
1853 }
1854
1855 const attributes = Array.isArray(payload)
1856 ? payload
1857 : Array.isArray(payload?.data)
1858 ? payload.data
1859 : [];
1860
1861 // Convert to attributeId => value mapping for form
1862 const attributesMap: Record<number, any> = {};
1863 attributes.forEach((attr: any) => {
1864 const attributeId = Number(attr.attribute_id || attr.id);
1865 if (attributeId) {
1866 let value = "";
1867
1868 // Read from relationship_metadata (contains complete attribute data)
1869 if (attr.relationship_metadata) {
1870 try {
1871 const metadata =
1872 typeof attr.relationship_metadata === "string"
1873 ? JSON.parse(attr.relationship_metadata)
1874 : attr.relationship_metadata;
1875
1876 value = metadata.value || "";
1877 } catch (e) {
1878 console.warn(
1879 "Failed to parse relationship_metadata:",
1880 attr.relationship_metadata,
1881 e,
1882 );
1883 value = "";
1884 }
1885 } else {
1886 // Fallback to old format if metadata is not available
1887 value = attr.value || "";
1888 }
1889
1890 attributesMap[attributeId] = value;
1891 }
1892 });
1893
1894 return attributesMap;
1895 } catch (error) {
1896 console.error("Failed to fetch trip attributes:", error);
1897 return {};
1898 }
1899 },
1900 enabled: tripAttributesQueryEnabled,
1901 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
1902 });
1903 const tripAttributesReady =
1904 !tripAttributesQueryEnabled || !isTripAttributesPending;
1905
1906 // Helper function to normalize array of items to IDs
1907 // Helper to normalize highlights (can be array of strings or objects)
1908 const normalizeHighlights = (highlights: any): string[] => {
1909 if (!highlights) return [];
1910 if (Array.isArray(highlights)) {
1911 return highlights
1912 .map((h: any) => {
1913 if (typeof h === "string") return h;
1914 if (h && typeof h === "object") {
1915 return h.highlight_text || h.text || h.title || String(h);
1916 }
1917 return String(h);
1918 })
1919 .filter((h: string) => h.trim().length > 0);
1920 }
1921 if (typeof highlights === "string") {
1922 try {
1923 const parsed = JSON.parse(highlights);
1924 return normalizeHighlights(parsed);
1925 } catch {
1926 return [highlights];
1927 }
1928 }
1929 return [];
1930 };
1931
1932 const normalizeDownloadableItems = (items: any): DownloadableItem[] => {
1933 if (!items || !Array.isArray(items)) return [];
1934
1935 return items
1936 .filter((row: any) => row && typeof row === "object")
1937 .map((row: any, idx: number) => {
1938 const rawVisibility = (row.visibility ?? "booked_only") as any;
1939 const mappedVisibility =
1940 rawVisibility === "paid_only" ? "booked_only" : rawVisibility;
1941 const safeVisibility: DownloadableItem["visibility"] = [
1942 "public",
1943 "logged_in",
1944 "booked_only",
1945 ].includes(mappedVisibility)
1946 ? mappedVisibility
1947 : "booked_only";
1948
1949 const title = (
1950 row.title ??
1951 row.download_title ??
1952 row.downlaod_title ??
1953 ""
1954 ).toString();
1955 const description = (
1956 row.description ??
1957 row.download_description ??
1958 row.downlaod_description ??
1959 ""
1960 ).toString();
1961 const attachmentIdRaw =
1962 row.attachment_id ?? row.download_file ?? row.downlaod_file;
1963 const attachmentUrl = (
1964 row.attachment_url ??
1965 row.content_url ??
1966 ""
1967 ).toString();
1968 const attachmentTitle = (row.attachment_title ?? "").toString();
1969 const enabledRaw =
1970 row.enabled ??
1971 row.is_downloadable ??
1972 row.download_enabled ??
1973 row.downlaod_enabled;
1974
1975 return {
1976 id: row.id != null ? Number(row.id) : null,
1977 title,
1978 description,
1979 attachment_id:
1980 attachmentIdRaw != null ? Number(attachmentIdRaw) : null,
1981 attachment_url: attachmentUrl,
1982 attachment_title: attachmentTitle,
1983 visibility: safeVisibility,
1984 enabled: enabledRaw != null ? Boolean(enabledRaw) : true,
1985 sort_order: row.sort_order != null ? Number(row.sort_order) : idx + 1,
1986 };
1987 });
1988 };
1989
1990 // Helper to extract IDs from mixed arrays
1991 const extractIds = (items: any): number[] => {
1992 if (!items || !Array.isArray(items)) return [];
1993 return items
1994 .map((item: any) => {
1995 if (typeof item === "number") return item;
1996 if (typeof item === "string") return parseInt(item) || 0;
1997 if (item && typeof item === "object") {
1998 return (
1999 item.id ||
2000 item.destination_id ||
2001 item.activity_id ||
2002 item.category_id ||
2003 0
2004 );
2005 }
2006 return 0;
2007 })
2008 .filter((id: number) => !isNaN(id) && id > 0);
2009 };
2010
2011 // Helper to normalize itinerary days
2012 const normalizeItineraryDays = (days: any): any[] => {
2013 if (!days) return [];
2014 if (Array.isArray(days)) return days;
2015 if (typeof days === "string") {
2016 try {
2017 const parsed = JSON.parse(days);
2018 return Array.isArray(parsed) ? parsed : [];
2019 } catch {
2020 return [];
2021 }
2022 }
2023 return [];
2024 };
2025
2026 // Helper to normalize availability dates
2027 const normalizeAvailabilityDates = (dates: any): any[] => {
2028 if (!dates) return [];
2029 if (Array.isArray(dates)) return dates;
2030 if (typeof dates === "string") {
2031 try {
2032 const parsed = JSON.parse(dates);
2033 return Array.isArray(parsed) ? parsed : [];
2034 } catch {
2035 return [];
2036 }
2037 }
2038 return [];
2039 };
2040
2041 // Utility function to normalize gallery images
2042 const normalizeGalleryImages = (
2043 images: any,
2044 ): Array<{
2045 id: number;
2046 url: string;
2047 thumbnail_url?: string;
2048 alt_text?: string;
2049 caption?: string;
2050 }> => {
2051 if (!images) return [];
2052 if (Array.isArray(images)) {
2053 return images
2054 .map((img: any) => {
2055 if (typeof img === "string") {
2056 return { id: 0, url: img };
2057 }
2058 if (img && typeof img === "object") {
2059 return {
2060 id: img.id || img.image_id || 0,
2061 url: img.url || img.image_url || img.src || "",
2062 thumbnail_url: img.thumbnail_url || img.thumb_url || "",
2063 alt_text: img.alt_text || img.alt || "",
2064 caption: img.caption || img.title || "",
2065 };
2066 }
2067 return { id: 0, url: "" };
2068 })
2069 .filter((item: any) => item.url);
2070 }
2071 if (typeof images === "string") {
2072 try {
2073 const parsed = JSON.parse(images);
2074 return normalizeGalleryImages(parsed);
2075 } catch {
2076 return [{ id: 0, url: images }];
2077 }
2078 }
2079 return [];
2080 };
2081
2082 // Helper to normalize FAQs
2083 const normalizeFaqs = (faqs: any): FAQ[] => {
2084 if (!faqs || !Array.isArray(faqs)) return [];
2085 return faqs
2086 .map((faq: any) => {
2087 if (faq && typeof faq === "object") {
2088 return {
2089 question: faq.question || "",
2090 answer: faq.answer || "",
2091 };
2092 }
2093 return { question: "", answer: "" };
2094 })
2095 .filter((faq: any) => faq.question && faq.answer);
2096 };
2097
2098 useEffect(() => {
2099 if (!tripData || !isEditMode) {
2100 return;
2101 }
2102
2103 // Preserve current itinerary_days and gallery_images if they exist and have content
2104 // This prevents wiping out data when user updates trip in itinerary builder
2105 const currentItineraryDays = formData.itinerary_days || [];
2106 const currentGalleryImages = formData.gallery_images || [];
2107
2108 // Only use database data if current data is empty or if this is initial load
2109 const shouldPreserveItinerary = currentItineraryDays.length > 0;
2110 const shouldPreserveGallery = currentGalleryImages.length > 0;
2111
2112 setFormData({
2113 title: tripData.title || "",
2114 slug: tripData.slug || "",
2115 description: tripData.description || "",
2116 highlights: normalizeHighlights(tripData.highlights),
2117 trip_details: tripData.trip_details || "",
2118 short_description: tripData.short_description || "",
2119 what_makes_special: tripData.what_makes_special || "",
2120 trip_story: tripData.trip_story || "",
2121 video_url: tripData.video_url || "",
2122 virtual_tour_url: tripData.virtual_tour_url || "",
2123 testimonial_review_ids: Array.isArray(tripData.testimonial_review_ids)
2124 ? tripData.testimonial_review_ids
2125 : [],
2126 destinations: extractIds(tripData.destinations || []),
2127 starting_location: tripData.starting_location || "",
2128 ending_location: tripData.ending_location || "",
2129 countries: Array.isArray(tripData.countries) ? tripData.countries : [],
2130 regions: Array.isArray(tripData.regions) ? tripData.regions : [],
2131 starting_latitude: tripData.starting_latitude?.toString() || "",
2132 starting_longitude: tripData.starting_longitude?.toString() || "",
2133 ending_latitude: tripData.ending_latitude?.toString() || "",
2134 ending_longitude: tripData.ending_longitude?.toString() || "",
2135 landmarks: Array.isArray(tripData.landmarks) ? tripData.landmarks : [],
2136 trip_type: (tripData.trip_type ||
2137 (tripData.duration_days &&
2138 parseInt(tripData.duration_days?.toString() || "0") === 1
2139 ? "single_day"
2140 : "multi_day")) as "single_day" | "multi_day" | "flexible",
2141 duration_days: tripData.duration_days?.toString() || "",
2142 duration_nights: tripData.duration_nights?.toString() || "",
2143 available_from: tripData.available_from || "",
2144 available_to: tripData.available_to || "",
2145 booking_window_days: tripData.booking_window_days?.toString() || "",
2146 seasonal_availability: tripData.seasonal_availability || "",
2147 best_season: tripData.best_season || "",
2148 peak_season: tripData.peak_season || "",
2149 off_season: tripData.off_season || "",
2150 activity_types: extractIds(tripData.activity_types || []),
2151 difficulty_level: tripData.difficulty_level?.toString() || "",
2152 trip_category: extractIds(tripData.trip_category || []),
2153 tags: Array.isArray(tripData.tags) ? tripData.tags : [],
2154 featured_priority: tripData.featured_priority || "none",
2155 accommodation_type: tripData.accommodation_type || "",
2156 meal_plan: tripData.meal_plan || "",
2157 accommodation_details: tripData.accommodation_details || "",
2158 transportation_included: tripData.transportation_included || false,
2159 pickup_location: tripData.pickup_location || "",
2160 dropoff_location: tripData.dropoff_location || "",
2161 transportation_details: tripData.transportation_details || "",
2162 pricing_type: (tripData.pricing_type ||
2163 (tripData.price_types &&
2164 Array.isArray(tripData.price_types) &&
2165 tripData.price_types.length > 0
2166 ? "traveler_based"
2167 : "regular")) as "regular" | "traveler_based",
2168 original_price: tripData.original_price?.toString() || "",
2169 discounted_price: tripData.discounted_price?.toString() || "",
2170 price_types: Array.isArray(tripData.price_types)
2171 ? tripData.price_types.map((pt: any) => ({
2172 category_id: Number(pt.category_id) || 0,
2173 original_price: pt.original_price?.toString() || "",
2174 discounted_price: pt.discounted_price?.toString() || "",
2175 is_default: Boolean(pt.is_default),
2176 }))
2177 : [],
2178 deposit_amount: tripData.deposit_amount?.toString() || "",
2179 deposit_percentage: tripData.deposit_percentage?.toString() || "",
2180 payment_terms: tripData.payment_terms || "",
2181 max_travelers: tripData.max_travelers?.toString() || "",
2182 min_travelers: tripData.min_travelers?.toString() || "",
2183 booking_deadline_hours: tripData.booking_deadline_hours || "",
2184 cancellation_policy: tripData.cancellation_policy || "",
2185 age_min: tripData.age_min?.toString() || "",
2186 age_max: tripData.age_max?.toString() || "",
2187 physical_requirements: tripData.physical_requirements || "",
2188 visa_requirements: tripData.visa_requirements || "",
2189 vaccination_requirements: tripData.vaccination_requirements || "",
2190 disable_booking: Boolean(tripData.custom_fields?.disable_booking),
2191 // tinyint(1) columns can serialize from PHP/wpdb as the string "0"/"1".
2192 // JS treats "0" as truthy, so a plain `value || false` would leave the
2193 // checkbox stuck on after the user un-checked + saved. Coerce explicitly.
2194 has_default_time_slots:
2195 tripData.has_default_time_slots === true
2196 || tripData.has_default_time_slots === 1
2197 || tripData.has_default_time_slots === "1",
2198 default_time_slots: Array.isArray(tripData.default_time_slots)
2199 ? tripData.default_time_slots
2200 : tripData.default_time_slots
2201 ? JSON.parse(tripData.default_time_slots)
2202 : [],
2203 departure_time: tripData.departure_time || "09:00",
2204 included_items: normalizeAmenityItems(tripData.included_items),
2205 excluded_items: normalizeAmenityItems(tripData.excluded_items),
2206 // Preserve current itinerary data if it exists, otherwise use database data
2207 itinerary_days: shouldPreserveItinerary
2208 ? currentItineraryDays
2209 : normalizeItineraryDays(tripData.itinerary_days),
2210 // Preserve current gallery data if it exists, otherwise use database data
2211 gallery_images: shouldPreserveGallery
2212 ? currentGalleryImages
2213 : normalizeGalleryImages(tripData.gallery_images),
2214 featured_image: tripData.featured_image
2215 ? Number(tripData.featured_image)
2216 : null,
2217 downloadable_items: normalizeDownloadableItems(
2218 tripData.downloadable_items,
2219 ),
2220 faqs: normalizeFaqs(tripData.faqs),
2221 frontend_tabs: Array.isArray(tripData.frontend_tabs)
2222 ? tripData.frontend_tabs
2223 : [
2224 // Core sections (always present) - in logical order
2225 {
2226 id: "overview",
2227 label: "Overview",
2228 enabled: true,
2229 order: 1,
2230 content_type: "overview",
2231 icon: { type: "icon", value: "book" },
2232 },
2233 {
2234 id: "itinerary",
2235 label: "Itinerary",
2236 enabled: true,
2237 order: 2,
2238 content_type: "itinerary",
2239 icon: { type: "icon", value: "calendar" },
2240 },
2241 {
2242 id: "included",
2243 label: "Included",
2244 enabled: true,
2245 order: 3,
2246 content_type: "included_excluded",
2247 icon: { type: "icon", value: "check" },
2248 },
2249 {
2250 id: "location",
2251 label: "Location",
2252 enabled: true,
2253 order: 4,
2254 content_type: "location",
2255 icon: { type: "icon", value: "map-pin" },
2256 },
2257 {
2258 id: "important_info",
2259 label: "Important Info",
2260 enabled: true,
2261 order: 5,
2262 content_type: "important_info",
2263 icon: { type: "icon", value: "info" },
2264 },
2265
2266 // Conditional sections (enabled by default, shown conditionally on frontend)
2267 {
2268 id: "downloads",
2269 label: "Downloads",
2270 enabled: true,
2271 order: 6,
2272 content_type: "downloads",
2273 icon: { type: "icon", value: "download" },
2274 },
2275 {
2276 id: "faq",
2277 label: "FAQ",
2278 enabled: true,
2279 order: 7,
2280 content_type: "faq",
2281 icon: { type: "icon", value: "help-circle" },
2282 },
2283 {
2284 id: "trip_story",
2285 label: "Story",
2286 enabled: true,
2287 order: 8,
2288 content_type: "trip_story",
2289 custom_content: "",
2290 icon: { type: "icon", value: "book" },
2291 },
2292 {
2293 id: "what_makes_special",
2294 label: "Special",
2295 enabled: true,
2296 order: 9,
2297 content_type: "what_makes_special",
2298 custom_content: "",
2299 icon: { type: "icon", value: "star" },
2300 },
2301 {
2302 id: "testimonials",
2303 label: "Testimonials",
2304 enabled: true,
2305 order: 10,
2306 content_type: "testimonials",
2307 icon: { type: "icon", value: "message-circle" },
2308 },
2309 ],
2310 availability_dates: normalizeAvailabilityDates(
2311 tripData.availability_dates,
2312 ),
2313 status: (tripData.status || "draft") as
2314 | "draft"
2315 | "review"
2316 | "approved"
2317 | "publish"
2318 | "archived"
2319 | "suspended",
2320 scheduled_publish_date: tripData.scheduled_publish_date || "",
2321 scheduled_unpublish_date: tripData.scheduled_unpublish_date || "",
2322 version: tripData.version || 1,
2323 seasonal_auto_enable: tripData.seasonal_auto_enable || false,
2324 seasonal_enable_date: tripData.seasonal_enable_date || "",
2325 seasonal_disable_date: tripData.seasonal_disable_date || "",
2326 meta_title: tripData.meta_title || "",
2327 meta_description: tripData.meta_description || "",
2328 meta_keywords: tripData.meta_keywords || "",
2329 attributes: tripAttributesData || {},
2330 });
2331
2332 if (tripData.featured_image_url && tripData.featured_image) {
2333 const numericId = Number(tripData.featured_image) || 0;
2334 setFeaturedImagePreview(tripData.featured_image_url);
2335 if (numericId > 0) {
2336 featuredImageCache.current[numericId] = tripData.featured_image_url;
2337 }
2338 } else if (!tripData.featured_image) {
2339 setFeaturedImagePreview("");
2340 }
2341 }, [tripData, tripAttributesData, destinationsData, isEditMode, tripId]);
2342
2343 useEffect(() => {
2344 let isMounted = true;
2345
2346 const resolveFeaturedImage = async () => {
2347 const attachmentId = formData.featured_image;
2348
2349 if (!attachmentId) {
2350 setFeaturedImagePreview("");
2351 return;
2352 }
2353
2354 const cachedUrl = featuredImageCache.current[attachmentId];
2355 if (cachedUrl) {
2356 setFeaturedImagePreview(cachedUrl);
2357 return;
2358 }
2359
2360 if (!mediaBaseUrl) {
2361 setFeaturedImagePreview("");
2362 return;
2363 }
2364
2365 setIsResolvingFeaturedImage(true);
2366 try {
2367 const data = await wpService.getMedia(attachmentId);
2368 const url = data?.source_url || "";
2369 if (url && isMounted) {
2370 featuredImageCache.current[attachmentId] = url;
2371 setFeaturedImagePreview(url);
2372 } else if (isMounted) {
2373 setFeaturedImagePreview("");
2374 }
2375 } catch (error) {
2376 console.error("Failed to resolve featured image URL:", error);
2377 if (isMounted) {
2378 setFeaturedImagePreview("");
2379 }
2380 } finally {
2381 if (isMounted) {
2382 setIsResolvingFeaturedImage(false);
2383 }
2384 }
2385 };
2386
2387 resolveFeaturedImage();
2388
2389 return () => {
2390 isMounted = false;
2391 };
2392 }, [formData.featured_image, mediaBaseUrl]);
2393
2394 // Map errors to sections - also check for price_type errors
2395 const getSectionErrors = (sectionId: SectionId): string[] => {
2396 const errorMap: Record<SectionId, string[]> = {
2397 basic: [
2398 "title",
2399 "slug",
2400 "description",
2401 "featured_image",
2402 "trip_type",
2403 "duration_days",
2404 "duration_nights",
2405 ],
2406 location: ["destinations", "starting_location", "ending_location"],
2407 duration: ["available_from", "available_to", "booking_window_days"],
2408 pricing: ["original_price", "discounted_price", "price_types"],
2409 booking: ["min_travelers", "max_travelers", "age_min", "age_max"],
2410 attributes: ["attributes"],
2411 itinerary: ["itinerary_days"],
2412 included: ["included_items", "excluded_items"],
2413 media: ["gallery_images", "video_url", "virtual_tour_url"], // Removed featured_image - it's in basic section
2414 downloads: ["downloadable_items"],
2415 categorization: ["trip_category", "activity_types"],
2416 faqs: ["faqs"],
2417 seo: ["meta_title", "meta_description"],
2418 advanced: [],
2419 };
2420
2421 const sectionFields = errorMap[sectionId] || [];
2422 const fieldErrors = sectionFields.filter((field) => errors[field]);
2423
2424 // Also check for price_type errors (they have dynamic keys like price_type_0_original)
2425 if (sectionId === "pricing") {
2426 const priceTypeErrors = Object.keys(errors).filter((key) =>
2427 key.startsWith("price_type_"),
2428 );
2429 return [...fieldErrors, ...priceTypeErrors];
2430 }
2431
2432 return fieldErrors;
2433 };
2434
2435 // Define sections - Reorganized for beginner-friendly UX
2436 // PHASE 1: ESSENTIALS (Must complete for publishable trip)
2437 const essentialsSections: Section[] = [
2438 // 1. Trip Basics - What you're offering
2439 {
2440 id: "basic",
2441 label: __("Trip Basics", "yatra"),
2442 icon: FileText,
2443 required: true,
2444 completed: !!(formData.title?.trim() && formData.slug?.trim()),
2445 hasErrors: getSectionErrors("basic").length > 0,
2446 },
2447
2448 // 2. Location & Route - Where it happens
2449 {
2450 id: "location",
2451 label: __("Location & Route", "yatra"),
2452 icon: MapPin,
2453 required: false,
2454 completed: !!(formData.destinations.length > 0),
2455 hasErrors: getSectionErrors("location").length > 0,
2456 },
2457
2458 // 3. Pricing - How much it costs
2459 {
2460 id: "pricing",
2461 label: __("Pricing", "yatra"),
2462 icon: DollarSign,
2463 required: true,
2464 completed:
2465 formData.pricing_type === "regular"
2466 ? !!(
2467 formData.original_price && parseFloat(formData.original_price) > 0
2468 )
2469 : formData.price_types.some(
2470 (pt) => pt.original_price && parseFloat(pt.original_price) > 0,
2471 ),
2472 hasErrors: getSectionErrors("pricing").length > 0,
2473 },
2474
2475 // 4. Availability & Booking - When available + booking rules (merged duration + booking)
2476 {
2477 id: "duration",
2478 label: __("Availability & Booking", "yatra"),
2479 icon: Calendar,
2480 required: false,
2481 completed: !!(
2482 formData.available_from ||
2483 formData.available_to ||
2484 (formData.min_travelers && formData.max_travelers)
2485 ),
2486 hasErrors:
2487 getSectionErrors("duration").length > 0 ||
2488 getSectionErrors("booking").length > 0,
2489 },
2490 ];
2491
2492 // PHASE 2: DETAILS (Enhance trip quality)
2493 const detailsSections: Section[] = [
2494 // 5. Trip Details - Description + Itinerary + Included/Excluded (merged 3 sections)
2495 {
2496 id: "itinerary",
2497 label: __("Trip Details", "yatra"),
2498 icon: BookOpen,
2499 required: false,
2500 completed:
2501 formData.itinerary_days.length > 0 ||
2502 formData.included_items.length > 0 ||
2503 formData.excluded_items.length > 0,
2504 hasErrors:
2505 getSectionErrors("itinerary").length > 0 ||
2506 getSectionErrors("included").length > 0,
2507 },
2508 ];
2509
2510 // PHASE 3: OPTIMIZATION (Improve discoverability)
2511 const optimizationSections: Section[] = [
2512 // 6. Media & Gallery - Photos, videos, testimonials
2513 {
2514 id: "media",
2515 label: __("Media & Gallery", "yatra"),
2516 icon: Image,
2517 required: false,
2518 completed: formData.gallery_images.length > 0 || !!formData.video_url,
2519 hasErrors: getSectionErrors("media").length > 0,
2520 },
2521 ...(showDownloadsUI
2522 ? ([
2523 {
2524 id: "downloads",
2525 label: __("Downloads", "yatra"),
2526 icon: Download,
2527 required: false,
2528 completed: (formData.downloadable_items || []).length > 0,
2529 hasErrors: getSectionErrors("downloads").length > 0,
2530 },
2531 ] as Section[])
2532 : []),
2533
2534 // 7. Categories & Attributes - Classification + Custom Attributes (merged 2 sections)
2535 {
2536 id: "categorization",
2537 label: __("Categories & Attributes", "yatra"),
2538 icon: Tag,
2539 required: false,
2540 completed: !!(
2541 formData.trip_category ||
2542 formData.activity_types.length > 0 ||
2543 formData.tags.length > 0 ||
2544 (formData.attributes && Object.keys(formData.attributes).length > 0)
2545 ),
2546 hasErrors:
2547 getSectionErrors("categorization").length > 0 ||
2548 getSectionErrors("attributes").length > 0,
2549 },
2550
2551 // 8. SEO & Marketing - SEO + FAQs (merged, fixed duplicate)
2552 {
2553 id: "seo",
2554 label: __("SEO & Marketing", "yatra"),
2555 icon: Search,
2556 required: false,
2557 completed: !!(
2558 formData.meta_title ||
2559 formData.meta_description ||
2560 formData.faqs.length > 0
2561 ),
2562 hasErrors:
2563 getSectionErrors("seo").length > 0 ||
2564 getSectionErrors("faqs").length > 0,
2565 },
2566 ];
2567
2568 // PHASE 4: ADVANCED (Power users only)
2569 const advancedSections: Section[] = [
2570 // 9. Advanced Settings - Publishing, scheduling, technical
2571 {
2572 id: "advanced",
2573 label: __("Advanced Settings", "yatra"),
2574 icon: Settings,
2575 required: false,
2576 completed: false,
2577 hasErrors: false,
2578 },
2579 ];
2580
2581 const allSections = [
2582 ...essentialsSections,
2583 ...detailsSections,
2584 ...optimizationSections,
2585 ...advancedSections,
2586 ];
2587 const currentStepIndex = allSections.findIndex(
2588 (s) => s.id === currentSection,
2589 );
2590
2591 // Navigation helpers
2592 const goToNextSection = () => {
2593 if (currentStepIndex < allSections.length - 1) {
2594 setCurrentSection(allSections[currentStepIndex + 1].id);
2595 }
2596 };
2597
2598 const goToPreviousSection = () => {
2599 if (currentStepIndex > 0) {
2600 setCurrentSection(allSections[currentStepIndex - 1].id);
2601 }
2602 };
2603
2604 // Smart defaults - Auto-calculate nights from days (only if nights is empty)
2605 useEffect(() => {
2606 if (
2607 formData.duration_days &&
2608 formData.trip_type === "multi_day" &&
2609 (!formData.duration_nights || formData.duration_nights === "")
2610 ) {
2611 const days = parseInt(formData.duration_days);
2612 if (days > 0 && !isNaN(days)) {
2613 setFormData((prev) => ({
2614 ...prev,
2615 duration_nights: String(Math.max(0, days - 1)),
2616 }));
2617 }
2618 }
2619 }, [formData.duration_days, formData.trip_type]);
2620
2621 // Auto-calculate days from nights (only if days is empty)
2622 useEffect(() => {
2623 if (
2624 formData.duration_nights &&
2625 formData.trip_type === "multi_day" &&
2626 (!formData.duration_days || formData.duration_days === "")
2627 ) {
2628 const nights = parseInt(formData.duration_nights);
2629 if (nights > 0 && !isNaN(nights)) {
2630 setFormData((prev) => ({ ...prev, duration_days: String(nights + 1) }));
2631 }
2632 }
2633 }, [formData.duration_nights, formData.trip_type]);
2634
2635 // Auto-generate slug from title
2636 const generateSlug = (title: string): string => {
2637 return title
2638 .toLowerCase()
2639 .trim()
2640 .replace(/[^\w\s-]/g, "")
2641 .replace(/[\s_-]+/g, "-")
2642 .replace(/^-+|-+$/g, "");
2643 };
2644
2645 const handleTitleChange = (value: string) => {
2646 setFormData((prev) => ({
2647 ...prev,
2648 title: value,
2649 slug: prev.slug || generateSlug(value),
2650 }));
2651 if (errors.title) {
2652 setErrors((prev) => ({ ...prev, title: "" }));
2653 }
2654 };
2655
2656 const handleFieldChange = (field: keyof TripFormData, value: any) => {
2657 setFormData((prev) => ({ ...prev, [field]: value }));
2658 if (errors[field]) {
2659 setErrors((prev) => ({ ...prev, [field]: "" }));
2660 }
2661
2662 if (field === "featured_image") {
2663 // Handle explicit null (when removing image)
2664 if (value === null || value === "" || value === undefined) {
2665 setFeaturedImagePreview("");
2666 } else {
2667 const numericValue = typeof value === "number" ? value : Number(value);
2668 if (!numericValue) {
2669 setFeaturedImagePreview("");
2670 } else if (featuredImageCache.current[numericValue]) {
2671 setFeaturedImagePreview(featuredImageCache.current[numericValue]);
2672 }
2673 }
2674 }
2675 };
2676
2677 const handleHighlightAdd = () => {
2678 setShowHighlightModal(true);
2679 setModalInput({ text: "", question: "", answer: "" });
2680 };
2681
2682 const handleHighlightSave = () => {
2683 if (modalInput.text && modalInput.text.trim()) {
2684 setFormData((prev) => ({
2685 ...prev,
2686 highlights: [...prev.highlights, modalInput.text.trim()],
2687 }));
2688 setShowHighlightModal(false);
2689 setModalInput({ text: "", question: "", answer: "" });
2690 }
2691 };
2692
2693 const handleHighlightRemove = (index: number) => {
2694 setFormData((prev) => ({
2695 ...prev,
2696 highlights: prev.highlights.filter((_, i) => i !== index),
2697 }));
2698 };
2699
2700 // Included/Excluded handlers - will be integrated into ItinerarySection component
2701 // Keeping for future integration when included/excluded are merged into itinerary section
2702 // const handleIncludedAdd = () => { ... };
2703 // const handleIncludedRemove = (index: number) => { ... };
2704 // const handleExcludedAdd = () => { ... };
2705 // const handleExcludedRemove = (index: number) => { ... };
2706
2707 const handleFAQAdd = () => {
2708 setFormData((prev) => ({
2709 ...prev,
2710 faqs: [...prev.faqs, { question: "", answer: "" }],
2711 }));
2712 };
2713
2714 const handleFAQRemove = (index: number) => {
2715 setFormData((prev) => ({
2716 ...prev,
2717 faqs: prev.faqs.filter((_, i) => i !== index),
2718 }));
2719 };
2720
2721 const handleFAQChange = (
2722 index: number,
2723 field: "question" | "answer",
2724 value: string,
2725 ) => {
2726 setFormData((prev) => ({
2727 ...prev,
2728 faqs: prev.faqs.map((faq, i) =>
2729 i === index ? { ...faq, [field]: value } : faq,
2730 ),
2731 }));
2732 };
2733
2734 const handleGalleryAdd = () => {
2735 // Use WordPress media library with multiple selection
2736 if (window.wp && window.wp.media) {
2737 const mediaUploader = window.wp.media({
2738 title: __("Select Gallery Images", "yatra"),
2739 button: { text: __("Add to Gallery", "yatra") },
2740 multiple: true, // Allow multiple image selection
2741 library: { type: "image" },
2742 });
2743
2744 mediaUploader.on("select", () => {
2745 const selection = mediaUploader.state().get("selection");
2746 const newImages: Array<{
2747 id: number;
2748 url: string;
2749 thumbnail_url?: string;
2750 alt_text?: string;
2751 caption?: string;
2752 }> = [];
2753
2754 selection.each((attachment: any) => {
2755 const image = attachment.toJSON();
2756 if (image.url) {
2757 newImages.push({
2758 id: image.id || 0,
2759 url: image.url,
2760 thumbnail_url:
2761 image.sizes?.thumbnail?.url ||
2762 image.sizes?.medium?.url ||
2763 image.url,
2764 alt_text: image.alt || "",
2765 caption: image.caption || "",
2766 });
2767 }
2768 });
2769
2770 if (newImages.length > 0) {
2771 setFormData((prev) => ({
2772 ...prev,
2773 gallery_images: [...prev.gallery_images, ...newImages],
2774 }));
2775 }
2776 });
2777
2778 prepareWordPressMediaFrameOpen();
2779 mediaUploader.open();
2780 } else {
2781 // Fallback for when wp.media is not available
2782 showToast(
2783 __(
2784 "Media library not available. Please ensure you are logged in as admin.",
2785 "yatra",
2786 ),
2787 "error",
2788 );
2789 }
2790 };
2791
2792 const handleGalleryRemove = (index: number) => {
2793 setFormData((prev) => ({
2794 ...prev,
2795 gallery_images: prev.gallery_images.filter((_, i) => i !== index),
2796 }));
2797 };
2798
2799 const handleGalleryReorder = (fromIndex: number, toIndex: number) => {
2800 setFormData((prev) => {
2801 const newImages = [...prev.gallery_images];
2802 const [movedImage] = newImages.splice(fromIndex, 1);
2803 newImages.splice(toIndex, 0, movedImage);
2804 return { ...prev, gallery_images: newImages };
2805 });
2806 };
2807
2808 const handleDownloadableItemAdd = () => {
2809 setFormData((prev) => {
2810 const nextOrder = (prev.downloadable_items?.length || 0) + 1;
2811 return {
2812 ...prev,
2813 downloadable_items: [
2814 ...(prev.downloadable_items || []),
2815 {
2816 id: null,
2817 title: "",
2818 description: "",
2819 attachment_id: null,
2820 attachment_url: "",
2821 attachment_title: "",
2822 visibility: "booked_only",
2823 enabled: true,
2824 sort_order: nextOrder,
2825 },
2826 ],
2827 };
2828 });
2829 };
2830
2831 const handleDownloadableItemRemove = (index: number) => {
2832 setFormData((prev) => ({
2833 ...prev,
2834 downloadable_items: (prev.downloadable_items || []).filter(
2835 (_, i) => i !== index,
2836 ),
2837 }));
2838 };
2839
2840 const handleDownloadableItemMove = (fromIndex: number, toIndex: number) => {
2841 setFormData((prev) => {
2842 const items = [...(prev.downloadable_items || [])];
2843 const [moved] = items.splice(fromIndex, 1);
2844 items.splice(toIndex, 0, moved);
2845
2846 const normalized = items.map((item, idx) => ({
2847 ...item,
2848 sort_order: idx + 1,
2849 }));
2850
2851 return { ...prev, downloadable_items: normalized };
2852 });
2853 };
2854
2855 const handleDownloadableItemChange = (
2856 index: number,
2857 field: keyof DownloadableItem,
2858 value: any,
2859 ) => {
2860 setFormData((prev) => ({
2861 ...prev,
2862 downloadable_items: (prev.downloadable_items || []).map((item, i) =>
2863 i === index ? { ...item, [field]: value } : item,
2864 ),
2865 }));
2866 };
2867
2868 const handleDownloadableItemSelectFile = (index: number) => {
2869 if (window.wp && window.wp.media) {
2870 const mediaUploader = window.wp.media({
2871 title: __("Select File", "yatra"),
2872 button: { text: __("Use this file", "yatra") },
2873 multiple: false,
2874 });
2875
2876 mediaUploader.on("select", () => {
2877 const selection = mediaUploader.state().get("selection");
2878 const attachment = selection.first();
2879 if (!attachment) return;
2880 const file = attachment.toJSON();
2881
2882 handleDownloadableItemChange(index, "attachment_id", file.id || null);
2883 handleDownloadableItemChange(index, "attachment_url", file.url || "");
2884 handleDownloadableItemChange(
2885 index,
2886 "attachment_title",
2887 file.title || file.filename || "",
2888 );
2889 });
2890
2891 prepareWordPressMediaFrameOpen();
2892 mediaUploader.open();
2893 } else {
2894 // Fallback for when wp.media is not available
2895 showToast(
2896 __(
2897 "Media library not available. Please ensure you are logged in as admin.",
2898 "yatra",
2899 ),
2900 "error",
2901 );
2902 }
2903 };
2904
2905 const handlePriceTypeAdd = (categoryId: number) => {
2906 // Check if category already exists (compare as numbers to handle string/number mismatch)
2907 if (
2908 formData.price_types.some(
2909 (pt) => Number(pt.category_id) === Number(categoryId),
2910 )
2911 ) {
2912 showToast(
2913 __("This category already has pricing set", "yatra"),
2914 "warning",
2915 );
2916 return;
2917 }
2918 setFormData((prev) => ({
2919 ...prev,
2920 price_types: [
2921 ...prev.price_types,
2922 {
2923 category_id: categoryId,
2924 original_price: "",
2925 discounted_price: "",
2926 is_default: false,
2927 },
2928 ],
2929 }));
2930 };
2931
2932 const handlePriceTypeRemove = (categoryId: number) => {
2933 setFormData((prev) => ({
2934 ...prev,
2935 price_types: prev.price_types.filter(
2936 (pt) => Number(pt.category_id) !== Number(categoryId),
2937 ),
2938 }));
2939 };
2940
2941 const handlePriceTypeChange = (
2942 categoryId: number,
2943 field: "original_price" | "discounted_price",
2944 value: string,
2945 ) => {
2946 setFormData((prev) => ({
2947 ...prev,
2948 price_types: prev.price_types.map((pt) =>
2949 Number(pt.category_id) === Number(categoryId)
2950 ? { ...pt, [field]: value }
2951 : pt,
2952 ),
2953 }));
2954 };
2955
2956 const handlePriceTypeDefaultChange = (
2957 categoryId: number,
2958 isDefault: boolean,
2959 ) => {
2960 setFormData((prev) => ({
2961 ...prev,
2962 price_types: prev.price_types.map((pt) => {
2963 if (Number(pt.category_id) === Number(categoryId)) {
2964 return { ...pt, is_default: isDefault };
2965 }
2966 // Only allow one default at a time.
2967 return isDefault ? { ...pt, is_default: false } : pt;
2968 }),
2969 }));
2970 };
2971
2972 // Frontend Tabs Handlers
2973 const handleTabToggle = (tabId: string) => {
2974 setFormData((prev) => ({
2975 ...prev,
2976 frontend_tabs: prev.frontend_tabs.map((tab) =>
2977 tab.id === tabId ? { ...tab, enabled: !tab.enabled } : tab,
2978 ),
2979 }));
2980 };
2981
2982 // Handle dummy data fill
2983 const handleFillDummyData = () => {
2984 // Cycle through dummy data sets
2985 const nextIndex = (dummyDataIndex + 1) % dummyTripsData.length;
2986 setDummyDataIndex(nextIndex);
2987
2988 const dummyData = dummyTripsData[nextIndex];
2989
2990 // Populate form with dummy data
2991 setFormData({
2992 ...dummyData,
2993 // Ensure arrays are properly set
2994 destinations: dummyData.destinations || [],
2995 activity_types: dummyData.activity_types || [],
2996 price_types: dummyData.price_types || [],
2997 included_items: dummyData.included_items || [],
2998 excluded_items: dummyData.excluded_items || [],
2999 highlights: dummyData.highlights || [],
3000 faqs: dummyData.faqs || [],
3001 gallery_images: dummyData.gallery_images || [],
3002 itinerary_days: dummyData.itinerary_days || [],
3003 tags: dummyData.tags || [],
3004 testimonial_review_ids: dummyData.testimonial_review_ids || [],
3005 countries: dummyData.countries || [],
3006 regions: dummyData.regions || [],
3007 landmarks: dummyData.landmarks || [],
3008 availability_dates: dummyData.availability_dates || [],
3009 frontend_tabs: dummyData.frontend_tabs || [],
3010 has_default_time_slots: dummyData.has_default_time_slots || false,
3011 default_time_slots: dummyData.default_time_slots || [],
3012 departure_time: dummyData.departure_time || "09:00",
3013 downloadable_items: dummyData.downloadable_items || [],
3014 });
3015
3016 // Show toast notification
3017 showToast(
3018 __("Dummy data filled", "yatra") +
3019 ` (${nextIndex + 1}/${dummyTripsData.length})`,
3020 "success",
3021 );
3022 };
3023
3024 const handleTabLabelChange = (tabId: string, label: string) => {
3025 setFormData((prev) => ({
3026 ...prev,
3027 frontend_tabs: prev.frontend_tabs.map((tab) =>
3028 tab.id === tabId ? { ...tab, label } : tab,
3029 ),
3030 }));
3031 };
3032
3033 const handleTabMove = (tabId: string, direction: "up" | "down") => {
3034 setFormData((prev) => {
3035 const tabs = [...prev.frontend_tabs];
3036 const index = tabs.findIndex((t) => t.id === tabId);
3037 if (index === -1) return prev;
3038
3039 const newIndex = direction === "up" ? index - 1 : index + 1;
3040 if (newIndex < 0 || newIndex >= tabs.length) return prev;
3041
3042 [tabs[index], tabs[newIndex]] = [tabs[newIndex], tabs[index]];
3043 // Update order
3044 tabs.forEach((tab, i) => {
3045 tab.order = i + 1;
3046 });
3047
3048 return { ...prev, frontend_tabs: tabs };
3049 });
3050 };
3051
3052 const handleTabRemove = (tabId: string) => {
3053 setFormData((prev) => ({
3054 ...prev,
3055 frontend_tabs: prev.frontend_tabs
3056 .filter((tab) => tab.id !== tabId)
3057 .map((tab, i) => ({ ...tab, order: i + 1 })),
3058 }));
3059 };
3060
3061 const handleTabAdd = () => {
3062 const newTabId = `custom_${Date.now()}`;
3063 const maxOrder = Math.max(...formData.frontend_tabs.map((t) => t.order), 0);
3064 setFormData((prev) => ({
3065 ...prev,
3066 frontend_tabs: [
3067 ...prev.frontend_tabs,
3068 {
3069 id: newTabId,
3070 label: __("New Tab", "yatra"),
3071 enabled: true,
3072 order: maxOrder + 1,
3073 content_type: "custom",
3074 custom_content: "",
3075 },
3076 ],
3077 }));
3078 };
3079
3080 const handleTabContentTypeChange = (
3081 tabId: string,
3082 contentType: FrontendTab["content_type"],
3083 ) => {
3084 setFormData((prev) => ({
3085 ...prev,
3086 frontend_tabs: prev.frontend_tabs.map((tab) =>
3087 tab.id === tabId ? { ...tab, content_type: contentType } : tab,
3088 ),
3089 }));
3090 };
3091
3092 const handleTabCustomContentChange = (tabId: string, content: string) => {
3093 setFormData((prev) => ({
3094 ...prev,
3095 frontend_tabs: prev.frontend_tabs.map((tab) =>
3096 tab.id === tabId ? { ...tab, custom_content: content } : tab,
3097 ),
3098 }));
3099 };
3100
3101 const handleTabIconChange = (tabId: string, icon: IconPickerValue | null) => {
3102 setFormData((prev) => ({
3103 ...prev,
3104 frontend_tabs: prev.frontend_tabs.map((tab) =>
3105 tab.id === tabId ? { ...tab, icon } : tab,
3106 ),
3107 }));
3108 };
3109
3110 // Drag and drop handlers
3111 const [draggedTab, setDraggedTab] = useState<string | null>(null);
3112 const [dragOverTab, setDragOverTab] = useState<string | null>(null);
3113
3114 const handleDragStart = (e: React.DragEvent, tabId: string) => {
3115 setDraggedTab(tabId);
3116 e.dataTransfer.effectAllowed = "move";
3117 };
3118
3119 const handleDragOver = (e: React.DragEvent) => {
3120 e.preventDefault();
3121 e.dataTransfer.dropEffect = "move";
3122 };
3123
3124 const handleDragEnter = (tabId: string) => {
3125 setDragOverTab(tabId);
3126 };
3127
3128 const handleDragLeave = () => {
3129 setDragOverTab(null);
3130 };
3131
3132 const handleDrop = (e: React.DragEvent, targetTabId: string) => {
3133 e.preventDefault();
3134 setDragOverTab(null);
3135
3136 if (!draggedTab || draggedTab === targetTabId) {
3137 setDraggedTab(null);
3138 return;
3139 }
3140
3141 setFormData((prev) => {
3142 const tabs = [...prev.frontend_tabs];
3143 const draggedIndex = tabs.findIndex((t) => t.id === draggedTab);
3144 const targetIndex = tabs.findIndex((t) => t.id === targetTabId);
3145
3146 if (draggedIndex === -1 || targetIndex === -1) {
3147 return prev;
3148 }
3149
3150 // Remove dragged tab and insert at new position
3151 const [draggedTabObj] = tabs.splice(draggedIndex, 1);
3152 tabs.splice(targetIndex, 0, draggedTabObj);
3153
3154 // Update order values
3155 tabs.forEach((tab, i) => {
3156 tab.order = i + 1;
3157 });
3158
3159 return { ...prev, frontend_tabs: tabs };
3160 });
3161
3162 setDraggedTab(null);
3163 };
3164
3165 const handleDragEnd = () => {
3166 setDraggedTab(null);
3167 setDragOverTab(null);
3168 };
3169
3170 const buildEssentialFieldErrors = (): Record<string, string> => {
3171 const newErrors: Record<string, string> = {};
3172
3173 if (!formData.title.trim()) {
3174 newErrors.title = __("Title is required", "yatra");
3175 }
3176
3177 if (!formData.slug.trim()) {
3178 newErrors.slug = __("Slug is required", "yatra");
3179 } else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) {
3180 newErrors.slug = __(
3181 "Slug can only contain letters, numbers, and hyphens",
3182 "yatra",
3183 );
3184 }
3185
3186 return newErrors;
3187 };
3188
3189 const validateForm = (): boolean => {
3190 const newErrors = buildEssentialFieldErrors();
3191 setErrors(newErrors);
3192 return Object.keys(newErrors).length === 0;
3193 };
3194
3195 // Save mutation
3196 const saveMutation = useMutation({
3197 mutationFn: async (
3198 data: TripFormData & { status?: "draft" | "publish" },
3199 ) => {
3200 const payload = {
3201 title: data.title.trim(),
3202 slug: data.slug.trim(),
3203 description: data.description.trim(),
3204 short_description: data.short_description.trim(),
3205 highlights: data.highlights,
3206 trip_details: data.trip_details.trim(),
3207 what_makes_special: data.what_makes_special.trim(),
3208 trip_story: data.trip_story.trim(),
3209 video_url: data.video_url.trim(),
3210 virtual_tour_url: data.virtual_tour_url.trim(),
3211 testimonial_review_ids: Array.isArray(data.testimonial_review_ids)
3212 ? data.testimonial_review_ids.filter(
3213 (id): id is number => id !== null && id !== undefined && id > 0,
3214 )
3215 : [],
3216 destinations: data.destinations || [], // Array of destination IDs
3217 starting_location: data.starting_location.trim(),
3218 ending_location: data.ending_location.trim(),
3219 countries: data.countries || [],
3220 regions: data.regions || [],
3221 starting_latitude: data.starting_latitude
3222 ? parseFloat(data.starting_latitude)
3223 : null,
3224 starting_longitude: data.starting_longitude
3225 ? parseFloat(data.starting_longitude)
3226 : null,
3227 ending_latitude: data.ending_latitude
3228 ? parseFloat(data.ending_latitude)
3229 : null,
3230 ending_longitude: data.ending_longitude
3231 ? parseFloat(data.ending_longitude)
3232 : null,
3233 landmarks: data.landmarks || [],
3234 trip_type: data.trip_type,
3235 duration_days: data.duration_days ? parseInt(data.duration_days) : null,
3236 duration_nights: data.duration_nights
3237 ? parseInt(data.duration_nights)
3238 : null,
3239 available_from: data.available_from || null,
3240 available_to: data.available_to || null,
3241 booking_window_days: data.booking_window_days
3242 ? parseInt(data.booking_window_days)
3243 : null,
3244 seasonal_availability: data.seasonal_availability || "",
3245 best_season: data.best_season.trim(),
3246 peak_season: data.peak_season.trim(),
3247 off_season: data.off_season.trim(),
3248 activity_types: data.activity_types || [], // Array of activity IDs
3249 difficulty_level: parseInt(data.difficulty_level) || null,
3250 trip_category: (() => {
3251 const rawCategories = data.trip_category || [];
3252
3253 // If no trip categories available yet, don't filter - send raw data
3254 if (tripCategories.length === 0) {
3255 return rawCategories;
3256 }
3257
3258 // Filter valid categories - save exactly what user selects
3259 const validCategories = rawCategories.filter((_catId) => {
3260 // Always return true - save all selected categories regardless of availability
3261 return true;
3262 });
3263
3264 return validCategories;
3265 })(),
3266 tags: data.tags || [],
3267 accommodation_type: data.accommodation_type || "",
3268 meal_plan: data.meal_plan || "",
3269 accommodation_details: data.accommodation_details.trim(),
3270 transportation_included: data.transportation_included || false,
3271 pickup_location: data.pickup_location.trim(),
3272 dropoff_location: data.dropoff_location.trim(),
3273 transportation_details: data.transportation_details.trim(),
3274 pricing_type: data.pricing_type,
3275 original_price:
3276 data.pricing_type === "regular"
3277 ? data.original_price
3278 ? parseFloat(data.original_price)
3279 : null
3280 : null,
3281 discounted_price:
3282 data.pricing_type === "regular"
3283 ? data.discounted_price
3284 ? parseFloat(data.discounted_price)
3285 : null
3286 : null,
3287 price_types:
3288 data.pricing_type === "traveler_based"
3289 ? data.price_types.map((pt) => ({
3290 category_id: pt.category_id,
3291 is_default: Boolean((pt as any).is_default),
3292 original_price: pt.original_price
3293 ? parseFloat(pt.original_price)
3294 : 0,
3295 discounted_price: pt.discounted_price
3296 ? parseFloat(pt.discounted_price)
3297 : null,
3298 }))
3299 : [],
3300 deposit_amount: data.deposit_amount
3301 ? parseFloat(data.deposit_amount)
3302 : null,
3303 deposit_percentage: data.deposit_percentage
3304 ? parseFloat(data.deposit_percentage)
3305 : null,
3306 payment_terms: data.payment_terms.trim(),
3307 max_travelers: data.max_travelers ? parseInt(data.max_travelers) : null,
3308 min_travelers: data.min_travelers ? parseInt(data.min_travelers) : null,
3309 booking_deadline_hours: data.booking_deadline_hours || null,
3310 cancellation_policy: data.cancellation_policy || "",
3311 custom_fields: { disable_booking: data.disable_booking },
3312 age_min: data.age_min ? parseInt(data.age_min) : null,
3313 age_max: data.age_max ? parseInt(data.age_max) : null,
3314 physical_requirements: data.physical_requirements.trim(),
3315 visa_requirements: data.visa_requirements.trim(),
3316 vaccination_requirements: data.vaccination_requirements.trim(),
3317 has_default_time_slots: data.has_default_time_slots || false,
3318 default_time_slots: JSON.stringify(data.default_time_slots || []),
3319 departure_time: data.departure_time || "09:00",
3320 included_items: (data.included_items || [])
3321 .map((item) => ({
3322 title: item.title?.trim() || "",
3323 description: item.description?.trim() || "",
3324 }))
3325 .filter((item) => item.title),
3326 excluded_items: (data.excluded_items || [])
3327 .map((item) => ({
3328 title: item.title?.trim() || "",
3329 description: item.description?.trim() || "",
3330 }))
3331 .filter((item) => item.title),
3332 itinerary_days: data.itinerary_days || [],
3333 gallery_images: data.gallery_images || [],
3334 featured_image: data.featured_image ?? null,
3335 faqs: data.faqs || [],
3336 frontend_tabs: data.frontend_tabs.map((tab) => ({
3337 id: tab.id,
3338 label: tab.label,
3339 enabled: tab.enabled,
3340 order: tab.order,
3341 content_type: tab.content_type,
3342 custom_content: tab.custom_content || null,
3343 icon: tab.icon || null,
3344 })),
3345 availability_dates: data.availability_dates.map((avail) => ({
3346 id: avail.id,
3347 departure_date: avail.departure_date || null,
3348 arrival_date: avail.arrival_date || null,
3349 seats_remaining: avail.seats_remaining || null,
3350 original_price: avail.original_price
3351 ? parseFloat(avail.original_price)
3352 : null,
3353 discounted_price: avail.discounted_price
3354 ? parseFloat(avail.discounted_price)
3355 : null,
3356 discount_percentage: avail.discount_percentage
3357 ? parseFloat(avail.discount_percentage)
3358 : null,
3359 status: avail.status || "available",
3360 from_location: avail.from_location || null,
3361 to_location: avail.to_location || null,
3362 from_latitude: avail.from_latitude || null,
3363 from_longitude: avail.from_longitude || null,
3364 to_latitude: avail.to_latitude || null,
3365 to_longitude: avail.to_longitude || null,
3366 })),
3367 status: data.status === "publish" ? "publish" : "draft",
3368 scheduled_publish_date: data.scheduled_publish_date || null,
3369 scheduled_unpublish_date: data.scheduled_unpublish_date || null,
3370 version: data.version || 1,
3371 seasonal_auto_enable: data.seasonal_auto_enable || false,
3372 seasonal_enable_date: data.seasonal_enable_date || null,
3373 seasonal_disable_date: data.seasonal_disable_date || null,
3374 meta_title: data.meta_title || "",
3375 meta_description: data.meta_description || "",
3376 meta_keywords: data.meta_keywords || "",
3377 attributes: data.attributes || {},
3378 featured_priority: data.featured_priority,
3379 };
3380
3381 if (showDownloadsUI) {
3382 (payload as any).downloadable_items = (data.downloadable_items || [])
3383 .map((item, idx) => ({
3384 id: item.id ?? null,
3385 title: (item.title || "").trim(),
3386 description: item.description || "",
3387 attachment_id: item.attachment_id ?? null,
3388 visibility: item.visibility || "booked_only",
3389 enabled: item.enabled !== false,
3390 sort_order: item.sort_order != null ? item.sort_order : idx + 1,
3391 attachment_url: item.attachment_url || "",
3392 attachment_title: item.attachment_title || "",
3393
3394 // Prefixed keys (requested)
3395 download_title: (item.title || "").trim(),
3396 download_description: item.description || "",
3397 download_visibility: item.visibility || "booked_only",
3398 download_enabled: item.enabled !== false,
3399 download_file: item.attachment_id ?? null,
3400 }))
3401 .filter((item) => item.title);
3402 }
3403
3404 if (isEditMode && tripId) {
3405 const response = await apiClient.put(`/trips/${tripId}`, payload);
3406 return response.data || response;
3407 } else {
3408 const response = await apiClient.post("/trips", payload);
3409 return response.data || response;
3410 }
3411 },
3412 onSuccess: (data, variables) => {
3413 queryClient.invalidateQueries({ queryKey: ["trips"] });
3414 queryClient.invalidateQueries({ queryKey: ["trip", tripId] });
3415 // Force refetch the trip data immediately
3416 queryClient.refetchQueries({ queryKey: ["trip", tripId] });
3417 setIsSubmitting(false);
3418
3419 // Show success message - different for edit vs create mode
3420 let successMessage: string;
3421 if (isEditMode) {
3422 // Edit mode: updating existing trip
3423 successMessage =
3424 variables.status === "publish"
3425 ? __("Trip updated and published successfully", "yatra")
3426 : __("Trip updated successfully", "yatra");
3427 } else {
3428 // Create mode: creating new trip
3429 successMessage =
3430 variables.status === "publish"
3431 ? __("Trip created and published successfully", "yatra")
3432 : __("Trip saved as draft successfully", "yatra");
3433 }
3434 showToast(successMessage, "success");
3435
3436 // Only redirect when creating a NEW trip - stay on page when editing
3437 if (!isEditMode && data?.id) {
3438 // If creating new trip, redirect to edit mode so user can continue editing
3439 setTimeout(() => {
3440 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${data.id}`;
3441 }, 1000);
3442 }
3443 // When editing (isEditMode), always stay on the same page - no redirect
3444 },
3445 onError: (error: any) => {
3446 const errorMessage =
3447 error?.response?.data?.message ||
3448 error?.message ||
3449 __("An error occurred while saving", "yatra");
3450 showToast(errorMessage, "error");
3451 setErrors({ submit: errorMessage });
3452 setIsSubmitting(false);
3453 },
3454 });
3455
3456 // Light validation for draft saves (only essential fields)
3457 const validateDraft = (): boolean => {
3458 const newErrors = buildEssentialFieldErrors();
3459 setErrors(newErrors);
3460 return Object.keys(newErrors).length === 0;
3461 };
3462
3463 const handleSaveDraft = async () => {
3464 if (!validateDraft()) {
3465 showToast(
3466 __("Please add a trip title and URL before saving.", "yatra"),
3467 "error",
3468 );
3469 return;
3470 }
3471 setIsSubmitting(true);
3472 saveMutation.mutate({ ...formData, status: "draft" });
3473 };
3474
3475 const handlePublish = async () => {
3476 if (!validateForm()) {
3477 const firstError = Object.keys(errors)[0];
3478 if (firstError) {
3479 showToast(
3480 __("Trip title and slug are required before publishing.", "yatra"),
3481 "error",
3482 );
3483 const errorElement = document.querySelector(
3484 `[name="${firstError}"], #${firstError}`,
3485 );
3486 if (errorElement) {
3487 errorElement.scrollIntoView({ behavior: "smooth", block: "center" });
3488 }
3489 }
3490 return;
3491 }
3492 setIsSubmitting(true);
3493 saveMutation.mutate({ ...formData, status: "publish" });
3494 };
3495
3496 // Enter should trigger publish/update (skip multiline and interactive controls)
3497 useEffect(() => {
3498 const onKeyDown = (e: KeyboardEvent) => {
3499 if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
3500 return;
3501
3502 const target = e.target as HTMLElement | null;
3503 if (!target) return;
3504
3505 const tag = target.tagName.toLowerCase();
3506 const isTextarea = tag === "textarea";
3507 const isButton = tag === "button";
3508 const isLink = tag === "a";
3509 const isContentEditable =
3510 target.getAttribute("contenteditable") === "true";
3511
3512 // Ignore Enter in multiline or interactive elements
3513 if (isTextarea || isButton || isLink || isContentEditable) return;
3514
3515 const inputType = (target as HTMLInputElement).type;
3516 if (["submit", "button", "file"].includes(inputType)) return;
3517
3518 e.preventDefault();
3519 if (!isSubmitting) {
3520 handlePublish();
3521 }
3522 };
3523
3524 window.addEventListener("keydown", onKeyDown);
3525 return () => window.removeEventListener("keydown", onKeyDown);
3526 }, [isSubmitting, handlePublish]);
3527
3528 // Using static revisions data for UI only
3529 const revisions = dummyRevisions;
3530 const isLoadingRevisions = false;
3531
3532 const handlePreview = async () => {
3533 const slug = (formData.slug || "").trim();
3534 if (!slug) {
3535 showToast(
3536 __(
3537 "Trip slug is missing. Please add a slug before previewing.",
3538 "yatra",
3539 ),
3540 "error",
3541 );
3542 return;
3543 }
3544
3545 const siteUrl = (window as any)?.yatraAdmin?.siteUrl || "";
3546 const { plainUrl, prettyUrl } = buildYatraSinglePublicUrls({
3547 entity: "trip",
3548 slug,
3549 siteUrl,
3550 bases: settingsData as Record<string, unknown> | null,
3551 });
3552
3553 if (isWordPressPlainPermalink()) {
3554 window.open(plainUrl, "_blank", "noopener,noreferrer");
3555 return;
3556 }
3557
3558 let apiPermalink = (tripData as any)?.permalink || (tripData as any)?.url;
3559
3560 if (!apiPermalink && tripId) {
3561 try {
3562 const detail = await apiClient.get(`/trips/${tripId}`);
3563 apiPermalink =
3564 (detail as any)?.permalink || (detail as any)?.url || apiPermalink;
3565 } catch (error) {
3566 // fall through to prettyUrl
3567 }
3568 }
3569
3570 if (apiPermalink) {
3571 window.open(apiPermalink, "_blank", "noopener,noreferrer");
3572 return;
3573 }
3574
3575 window.open(prettyUrl, "_blank", "noopener,noreferrer");
3576 };
3577
3578 const handleRevisionClick = (revisionId: number) => {
3579 setSelectedRevisionId(revisionId);
3580 setShowRevisionConfirm(true);
3581 };
3582
3583 const handleRevisionConfirm = () => {
3584 // UI only - no actual functionality
3585 if (selectedRevisionId) {
3586 showToast(__("Revision feature is coming soon", "yatra"), "info");
3587 }
3588 setShowRevisionConfirm(false);
3589 setShowRevisionsDialog(false);
3590 setSelectedRevisionId(null);
3591 };
3592
3593 // Skeleton loader for edit mode
3594 if (isEditMode && isLoadingTrip) {
3595 return (
3596 <div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden">
3597 {/* Header Skeleton */}
3598 <div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-4 flex items-center justify-between">
3599 <div className="flex items-center gap-4 min-w-0 flex-1">
3600 <div className="min-w-0">
3601 <div className="h-6 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
3602 <div className="flex items-center gap-3">
3603 <div className="h-5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3604 <div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
3605 </div>
3606 </div>
3607 </div>
3608 <div className="flex items-center gap-2">
3609 <div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3610 <div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3611 <div className="h-9 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3612 <div className="h-9 w-36 bg-blue-200 dark:bg-blue-800 rounded animate-pulse" />
3613 </div>
3614 </div>
3615
3616 {/* Navigation Bar Skeleton */}
3617 <div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
3618 <div className="flex items-center justify-between gap-4">
3619 <div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3620 <div className="flex-1 flex flex-col items-center gap-1 max-w-md mx-4">
3621 <div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3622 <div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
3623 <div className="h-2.5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3624 </div>
3625 <div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3626 </div>
3627 </div>
3628
3629 {/* Main Content Skeleton */}
3630 <div className="flex-1 flex overflow-hidden">
3631 {/* Sidebar Skeleton */}
3632 <div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 overflow-y-auto">
3633 <div className="p-5 pb-8 space-y-6">
3634 {/* Essentials Section Skeleton */}
3635 <div>
3636 <div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" />
3637 <div className="space-y-0.5">
3638 {[...Array(8)].map((_, i) => (
3639 <div
3640 key={`essentials-${i}`}
3641 className="flex items-center gap-2 px-3 py-2.5 rounded-md"
3642 >
3643 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3644 <div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3645 </div>
3646 ))}
3647 </div>
3648 </div>
3649
3650 {/* Marketing Section Skeleton */}
3651 <div>
3652 <div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" />
3653 <div className="space-y-0.5">
3654 {[...Array(9)].map((_, i) => (
3655 <div
3656 key={`marketing-${i}`}
3657 className="flex items-center gap-2 px-3 py-2.5 rounded-md"
3658 >
3659 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3660 <div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3661 </div>
3662 ))}
3663 </div>
3664 </div>
3665
3666 {/* Lifecycle Section Skeleton */}
3667 <div>
3668 <div className="flex items-center gap-2 px-3 py-2.5 rounded-md">
3669 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3670 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3671 </div>
3672 </div>
3673 </div>
3674 </div>
3675
3676 {/* Main Form Area Skeleton */}
3677 <div className="flex-1 overflow-y-auto p-6">
3678 <div className="max-w-4xl mx-auto space-y-6">
3679 {/* Section Header Skeleton */}
3680 <div className="flex items-center gap-2 mb-4">
3681 <div className="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3682 <div className="h-6 w-56 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3683 </div>
3684 <div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-6" />
3685
3686 {/* Form Fields Skeleton */}
3687 <Card>
3688 <CardContent className="p-6 space-y-6">
3689 {/* Title Field */}
3690 <div className="space-y-2">
3691 <div className="h-3.5 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3692 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3693 </div>
3694
3695 {/* Slug Field */}
3696 <div className="space-y-2">
3697 <div className="h-3.5 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3698 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3699 </div>
3700
3701 {/* Description Field */}
3702 <div className="space-y-2">
3703 <div className="h-3.5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3704 <div className="h-32 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3705 </div>
3706
3707 {/* Featured Image Skeleton */}
3708 <div className="space-y-2">
3709 <div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3710 <div className="h-48 w-full bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
3711 </div>
3712
3713 {/* Grid Fields Skeleton */}
3714 <div className="grid grid-cols-2 gap-4">
3715 {[...Array(4)].map((_, i) => (
3716 <div key={`grid-${i}`} className="space-y-3">
3717 <div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3718 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3719 </div>
3720 ))}
3721 </div>
3722 </CardContent>
3723 </Card>
3724 </div>
3725 </div>
3726 </div>
3727 </div>
3728 );
3729 }
3730
3731 if (isEditMode && tripError) {
3732 const requestInfo =
3733 tripErrorContext.requestInfo || (tripError as any)?.config || {};
3734 const method = (requestInfo.method || "GET").toUpperCase?.() || "GET";
3735 const url = requestInfo.url || (tripError as any)?.config?.url || "";
3736 const payload =
3737 requestInfo.payload || (tripError as any)?.config?.data || "";
3738 const composedErrorText = `Method: ${method}\nURL: ${url}\nPayload: ${payload || "N/A"}\n\n${tripErrorContext.details || ""}`;
3739
3740 return (
3741 <div className="min-h-screen flex items-center justify-center bg-white">
3742 <div className="w-full max-w-5xl px-6 py-12">
3743 <div className="relative flex flex-col items-center text-center gap-4 rounded-2xl border border-dashed border-[#fddfe1] bg-gradient-to-br from-[#fff7f8] via-white to-[#fff9f5] p-10 shadow-sm">
3744 <div className="w-16 h-16 rounded-2xl bg-white border border-[#ffdede] flex items-center justify-center">
3745 <svg
3746 className="w-10 h-10 text-red-500"
3747 fill="none"
3748 stroke="currentColor"
3749 viewBox="0 0 24 24"
3750 strokeWidth="1.8"
3751 >
3752 <path
3753 strokeLinecap="round"
3754 strokeLinejoin="round"
3755 d="M12 9v4m0 4h.01M10.29 3.86L2.82 17a2 2 0 001.71 3h14.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
3756 />
3757 </svg>
3758 </div>
3759 <div className="space-y-1">
3760 <h2 className="text-2xl font-semibold text-gray-900">
3761 {__("Error Loading Trips", "yatra")}
3762 </h2>
3763 <p className="text-sm text-gray-600">
3764 {__(
3765 "We couldn’t connect to the trips service. Please refresh or try again shortly.",
3766 "yatra",
3767 )}
3768 </p>
3769 </div>
3770 <div className="flex flex-wrap gap-2 justify-center">
3771 <Button
3772 onClick={() =>
3773 queryClient.invalidateQueries({ queryKey: ["trip", tripId] })
3774 }
3775 >
3776 {__("Try again", "yatra")}
3777 </Button>
3778 <Button
3779 variant="outline"
3780 onClick={() => {
3781 if (typeof window !== "undefined") {
3782 window.open(
3783 "https://wordpress.org/support/plugin/yatra",
3784 "_blank",
3785 );
3786 }
3787 }}
3788 >
3789 {__("Visit support center", "yatra")}
3790 </Button>
3791 </div>
3792
3793 <div className="w-full mt-10">
3794 <div className="relative w-full text-left rounded-2xl border border-gray-200 bg-white shadow-sm space-y-0">
3795 <div className="flex items-center justify-between px-5 py-3 border-b border-gray-200">
3796 <p className="text-sm font-medium text-gray-800">
3797 {__("Technical details", "yatra")}
3798 </p>
3799 <Button
3800 type="button"
3801 size="sm"
3802 variant="ghost"
3803 className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
3804 onClick={() =>
3805 copyErrorDetailsToClipboard(composedErrorText)
3806 }
3807 >
3808 {copiedErrorDetails ? (
3809 <>
3810 <Check className="w-4 h-4" />
3811 {__("Copied", "yatra")}
3812 </>
3813 ) : (
3814 <>
3815 <Copy className="w-4 h-4" />
3816 {__("Copy details", "yatra")}
3817 </>
3818 )}
3819 </Button>
3820 </div>
3821 <div className="px-5 py-3 border-b border-gray-200 space-y-2 text-sm text-left text-gray-800">
3822 <div>
3823 <span className="font-medium">
3824 {__("Method:", "yatra")}
3825 </span>{" "}
3826 <span className="font-mono">{method}</span>
3827 </div>
3828 <div className="break-all">
3829 <span className="font-medium">{__("URL:", "yatra")}</span>{" "}
3830 <span className="font-mono">
3831 {url || __("N/A", "yatra")}
3832 </span>
3833 </div>
3834 {payload && (
3835 <div>
3836 <span className="font-medium block mb-1">
3837 {__("Payload:", "yatra")}
3838 </span>
3839 <pre className="max-h-40 overflow-auto px-3 py-2 rounded bg-gray-50 text-xs font-mono text-gray-900 whitespace-pre-wrap border border-gray-200">
3840 {payload}
3841 </pre>
3842 </div>
3843 )}
3844 </div>
3845 <pre className="max-h-56 overflow-auto px-5 py-3 text-xs leading-relaxed font-mono text-gray-900 whitespace-pre-wrap bg-white">
3846 {tripErrorContext.details ||
3847 JSON.stringify(
3848 {
3849 message:
3850 tripError instanceof Error
3851 ? tripError.message
3852 : __("Failed to load trip data", "yatra"),
3853 method,
3854 url,
3855 payload,
3856 },
3857 null,
3858 2,
3859 )}
3860 </pre>
3861 </div>
3862 </div>
3863 </div>
3864 </div>
3865 </div>
3866 );
3867 }
3868
3869 const renderSectionContent = () => {
3870 switch (currentSection) {
3871 case "basic":
3872 return (
3873 <div className="space-y-6">
3874 <div className="flex items-center gap-2 mb-3">
3875 <FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
3876 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
3877 {__("Basic Information", "yatra")}
3878 </h2>
3879 <Badge
3880 variant="outline"
3881 className="ml-2 text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800"
3882 >
3883 {__("Start Here", "yatra")}
3884 </Badge>
3885 </div>
3886 <div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500 dark:border-blue-400 p-3 rounded-r-md mb-6">
3887 <p className="text-sm text-blue-900 dark:text-blue-100 font-medium mb-1">
3888 {__("💡 Getting Started", "yatra")}
3889 </p>
3890 <p className="text-xs text-blue-800 dark:text-blue-200">
3891 {__(
3892 "Only the Trip Title and Trip URL are required to create a draft. Everything else is optional for now, but filling it in is highly recommended for better discovery and conversions.",
3893 "yatra",
3894 )}
3895 </p>
3896 </div>
3897
3898 {/* Essential Fields - Single View (No Tabs) */}
3899 <div className="space-y-6">
3900 {/* Tour Title */}
3901 <div className="mb-4">
3902 <div className="flex items-center justify-between mb-1.5">
3903 <label
3904 htmlFor="title"
3905 className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
3906 >
3907 {__("Trip Title", "yatra")}
3908 <Badge
3909 variant="outline"
3910 className="ml-1 text-[10px] px-1.5 py-0 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"
3911 >
3912 {__("Required", "yatra")}
3913 </Badge>
3914 <button
3915 type="button"
3916 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
3917 title={__(
3918 "A catchy title that describes your trip. Recommended: 50-60 characters for best SEO results.",
3919 "yatra",
3920 )}
3921 >
3922 <HelpCircle className="w-3.5 h-3.5" />
3923 </button>
3924 </label>
3925 <span
3926 className={`text-xs font-medium ${
3927 formData.title.length > 60
3928 ? "text-red-600 dark:text-red-400"
3929 : formData.title.length >= 50
3930 ? "text-green-600 dark:text-green-400"
3931 : "text-gray-500 dark:text-gray-400"
3932 }`}
3933 >
3934 {formData.title.length}/60
3935 </span>
3936 </div>
3937 <HelpText
3938 text={__(
3939 '💡 Tip: Include your destination and trip length. Example: "Bali Beach Retreat - 7 Days" or "Paris City Tour - Half Day"',
3940 "yatra",
3941 )}
3942 className="mb-2"
3943 />
3944 <Input
3945 id="title"
3946 type="text"
3947 value={formData.title}
3948 onChange={(e) => handleTitleChange(e.target.value)}
3949 placeholder={__("e.g., Bali Beach Retreat - 7 Days", "yatra")}
3950 maxLength={100}
3951 className={`${errors.title ? "border-red-500" : formData.title && formData.title.length <= 60 ? "border-green-500" : ""} transition-colors`}
3952 required
3953 />
3954 {errors.title ? (
3955 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
3956 <AlertCircle className="w-4 h-4" />
3957 {errors.title}
3958 </p>
3959 ) : (
3960 formData.title &&
3961 formData.title.length > 60 && (
3962 <p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
3963 <AlertCircle className="w-3.5 h-3.5" />
3964 {__(
3965 "Title is longer than recommended for SEO (60 characters)",
3966 "yatra",
3967 )}
3968 </p>
3969 )
3970 )}
3971 </div>
3972
3973 {/* URL Slug */}
3974 <div className="mb-4">
3975 <div className="flex items-center justify-between mb-1.5">
3976 <label
3977 htmlFor="slug"
3978 className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
3979 >
3980 {__("Trip URL", "yatra")}
3981 <Badge
3982 variant="outline"
3983 className="ml-1 text-[10px] px-1.5 py-0 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"
3984 >
3985 {__("Required", "yatra")}
3986 </Badge>
3987 <button
3988 type="button"
3989 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
3990 title={__(
3991 "The web address for your trip page. Auto-generated from your title, but you can customize it.",
3992 "yatra",
3993 )}
3994 >
3995 <HelpCircle className="w-3.5 h-3.5" />
3996 </button>
3997 </label>
3998 {showSlugPreview && formData.slug && (
3999 <button
4000 type="button"
4001 onClick={() => setShowSlugPreview(false)}
4002 className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
4003 >
4004 {__("Hide", "yatra")}
4005 </button>
4006 )}
4007 </div>
4008 <HelpText
4009 text={__(
4010 "💡 This is automatically created from your title. You usually don't need to change it unless you want a custom web address.",
4011 "yatra",
4012 )}
4013 className="mb-2"
4014 />
4015 <Input
4016 id="slug"
4017 type="text"
4018 value={formData.slug}
4019 onChange={(e) => handleFieldChange("slug", e.target.value)}
4020 placeholder={__("bali-beach-retreat-7-days", "yatra")}
4021 className={`font-mono text-sm ${errors.slug ? "border-red-500" : ""}`}
4022 required
4023 />
4024 {errors.slug && (
4025 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4026 <AlertCircle className="w-4 h-4" />
4027 {errors.slug}
4028 </p>
4029 )}
4030 {showSlugPreview && formData.slug && !errors.slug && (
4031 <div className="mt-2 p-2.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs text-gray-600 dark:text-gray-400 border border-blue-200 dark:border-blue-800 flex items-center justify-between">
4032 <div>
4033 <span className="text-gray-500 dark:text-gray-400">
4034 {__("Preview URL:", "yatra")}{" "}
4035 </span>
4036 <span className="font-mono text-blue-600 dark:text-blue-400">
4037 {(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/
4038 {settingsData?.trip_base || "trip"}/{formData.slug}
4039 </span>
4040 </div>
4041 <button
4042 type="button"
4043 onClick={() => {
4044 const tripBase = settingsData?.trip_base || "trip";
4045 const url = `${(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/${tripBase}/${formData.slug}`;
4046 navigator.clipboard.writeText(url);
4047 showToast(
4048 __("URL copied to clipboard", "yatra"),
4049 "success",
4050 );
4051 }}
4052 className="ml-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
4053 title={__("Copy URL", "yatra")}
4054 >
4055 <Copy className="w-3.5 h-3.5" />
4056 </button>
4057 </div>
4058 )}
4059 {!showSlugPreview && formData.slug && (
4060 <button
4061 type="button"
4062 onClick={() => setShowSlugPreview(true)}
4063 className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
4064 >
4065 {__("Show URL Preview", "yatra")}
4066 </button>
4067 )}
4068 </div>
4069
4070 {/* Short Description */}
4071 <div className="mb-4">
4072 <div className="flex items-center justify-between mb-1.5">
4073 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
4074 {__("Short Description", "yatra")}
4075 <Badge
4076 variant="outline"
4077 className="ml-1 text-[10px] px-1.5 py-0 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800"
4078 >
4079 {__("Recommended", "yatra")}
4080 </Badge>
4081 <button
4082 type="button"
4083 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4084 title={__(
4085 "A brief summary that appears in listings. Recommended: 100-150 characters for best results.",
4086 "yatra",
4087 )}
4088 >
4089 <HelpCircle className="w-3.5 h-3.5" />
4090 </button>
4091 </label>
4092 <span
4093 className={`text-xs font-medium ${
4094 formData.short_description.length > 200
4095 ? "text-red-600 dark:text-red-400"
4096 : formData.short_description.length >= 100 &&
4097 formData.short_description.length <= 150
4098 ? "text-green-600 dark:text-green-400"
4099 : formData.short_description.length > 0
4100 ? "text-yellow-600 dark:text-yellow-400"
4101 : "text-gray-500 dark:text-gray-400"
4102 }`}
4103 >
4104 {formData.short_description.length}/200
4105 </span>
4106 </div>
4107 <HelpText
4108 text={__(
4109 '💡 Write 2-3 sentences that grab attention! This appears in trip listings. Example: "Escape to paradise with our 7-day Bali beach retreat. Experience stunning sunsets, world-class diving, and authentic local culture."',
4110 "yatra",
4111 )}
4112 className="mb-2"
4113 />
4114 <RichTextEditor
4115 value={formData.short_description}
4116 onChange={(value) =>
4117 handleFieldChange("short_description", value)
4118 }
4119 placeholder={__(
4120 "Escape to paradise with our 7-day Bali beach retreat...",
4121 "yatra",
4122 )}
4123 minHeight={120}
4124 />
4125 {formData.short_description.length > 0 &&
4126 formData.short_description.length < 100 && (
4127 <p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
4128 <AlertCircle className="w-3.5 h-3.5" />
4129 {__(
4130 "Consider adding more details (recommended: 100-150 characters)",
4131 "yatra",
4132 )}
4133 </p>
4134 )}
4135 </div>
4136
4137 {/* Tour Description */}
4138 <div className="mb-4">
4139 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5">
4140 {__("Trip Description", "yatra")}
4141 <Badge
4142 variant="outline"
4143 className="ml-1 text-[10px] px-1.5 py-0 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800"
4144 >
4145 {__("Recommended", "yatra")}
4146 </Badge>
4147 </label>
4148 <HelpText
4149 text={__(
4150 "💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!",
4151 "💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!",
4152 )}
4153 className="mb-2"
4154 />
4155 <RichTextEditor
4156 value={formData.description}
4157 onChange={(value) => handleFieldChange("description", value)}
4158 placeholder={__(
4159 "Escape to paradise with our 7-day Bali beach retreat... Or describe your single day trip experience...",
4160 "yatra",
4161 )}
4162 minHeight={260}
4163 />
4164 {errors.description && (
4165 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4166 <AlertCircle className="w-4 h-4" />
4167 {errors.description}
4168 </p>
4169 )}
4170 </div>
4171
4172 {/* Featured Image */}
4173 <div className="mb-6">
4174 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5">
4175 {__("Featured Image", "yatra")}
4176 <Badge
4177 variant="outline"
4178 className="ml-1 text-[10px] px-1.5 py-0 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800"
4179 >
4180 {__("Recommended", "yatra")}
4181 </Badge>
4182 <button
4183 type="button"
4184 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4185 title={__(
4186 "The main image that represents your trip. This appears in listings and as the hero image on the trip page.",
4187 "yatra",
4188 )}
4189 >
4190 <HelpCircle className="w-3.5 h-3.5" />
4191 </button>
4192 </label>
4193 <HelpText
4194 text={__(
4195 "💡 Choose your best photo! This is the first thing travelers see. Use a high-quality image (1200x800px recommended) that shows what makes your trip special.",
4196 "yatra",
4197 )}
4198 className="mb-2"
4199 />
4200 {formData.featured_image ? (
4201 <div className="relative group">
4202 <div
4203 className={`aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden border-2 ${
4204 errors.featured_image
4205 ? "border-red-500 dark:border-red-600"
4206 : "border-gray-200 dark:border-gray-700"
4207 }`}
4208 >
4209 {featuredImagePreview ? (
4210 <img
4211 src={featuredImagePreview}
4212 alt={__("Featured Image", "yatra")}
4213 className="w-full h-full object-cover"
4214 />
4215 ) : (
4216 <div className="flex items-center justify-center w-full h-full text-sm text-gray-500 dark:text-gray-400">
4217 {isResolvingFeaturedImage
4218 ? __("Loading image...", "yatra")
4219 : __("Preview unavailable", "yatra")}
4220 </div>
4221 )}
4222 </div>
4223 <button
4224 type="button"
4225 onClick={() => handleFieldChange("featured_image", null)}
4226 className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1.5 opacity-0 group-hover:opacity-100 transition-opacity"
4227 title={__("Remove image", "yatra")}
4228 >
4229 <X className="w-4 h-4" />
4230 </button>
4231 </div>
4232 ) : (
4233 <button
4234 type="button"
4235 onClick={() => {
4236 // Use WordPress media library
4237 if (window.wp && window.wp.media) {
4238 const mediaUploader = window.wp.media({
4239 title: __("Select Featured Image", "yatra"),
4240 button: { text: __("Use this image", "yatra") },
4241 multiple: false,
4242 });
4243 mediaUploader.on("select", () => {
4244 const attachment = mediaUploader
4245 .state()
4246 .get("selection")
4247 .first()
4248 .toJSON();
4249 handleFieldChange("featured_image", attachment.id);
4250 });
4251 prepareWordPressMediaFrameOpen();
4252 mediaUploader.open();
4253 }
4254 }}
4255 className={`w-full aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${
4256 errors.featured_image
4257 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4258 : "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400"
4259 }`}
4260 >
4261 <Upload
4262 className={`w-10 h-10 mb-2 ${errors.featured_image ? "text-red-400" : "text-gray-400"}`}
4263 />
4264 <span
4265 className={`text-sm font-medium ${errors.featured_image ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`}
4266 >
4267 {__("Upload Featured Image", "yatra")}
4268 </span>
4269 <span
4270 className={`text-xs mt-1 ${errors.featured_image ? "text-red-500 dark:text-red-500" : "text-gray-500 dark:text-gray-500"}`}
4271 >
4272 {__("Recommended: 1200x800px", "yatra")}
4273 </span>
4274 </button>
4275 )}
4276 {errors.featured_image && (
4277 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4278 <AlertCircle className="w-4 h-4" />
4279 {errors.featured_image}
4280 </p>
4281 )}
4282 </div>
4283
4284 {/* Trip Highlights */}
4285 <Card className="mt-6">
4286 <CardHeader>
4287 <CardTitle>{__("Trip Highlights", "yatra")}</CardTitle>
4288 <CardDescription>
4289 {__(
4290 "Add key highlights that make your trip special. These will be displayed prominently on your trip page.",
4291 "yatra",
4292 )}
4293 </CardDescription>
4294 </CardHeader>
4295 <CardContent className="space-y-3">
4296 {formData.highlights.length > 0 ? (
4297 <div className="space-y-2">
4298 {formData.highlights.map((highlight, index) => (
4299 <div
4300 key={index}
4301 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
4302 >
4303 <CheckCircle2 className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
4304 <span className="flex-1 text-sm text-gray-900 dark:text-white">
4305 {highlight}
4306 </span>
4307 <Button
4308 type="button"
4309 variant="ghost"
4310 size="sm"
4311 onClick={() => handleHighlightRemove(index)}
4312 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
4313 >
4314 <X className="w-4 h-4" />
4315 </Button>
4316 </div>
4317 ))}
4318 </div>
4319 ) : (
4320 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
4321 <Sparkles className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
4322 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
4323 {__("No highlights added yet", "yatra")}
4324 </p>
4325 <p className="text-xs text-gray-500 dark:text-gray-500 mb-4">
4326 {__(
4327 'Add key selling points like "Private guide", "All meals included", or "Skip-the-line tickets"',
4328 "yatra",
4329 )}
4330 </p>
4331 </div>
4332 )}
4333 <Button
4334 type="button"
4335 variant="outline"
4336 onClick={handleHighlightAdd}
4337 className="w-full"
4338 >
4339 <Plus className="w-4 h-4 mr-2" />
4340 {__("Add Highlight", "yatra")}
4341 </Button>
4342 </CardContent>
4343 </Card>
4344
4345 {/* Trip Type & Duration - Moved from Duration section */}
4346 <div className="mt-6 space-y-4">
4347 <div className="border-t border-gray-200 dark:border-gray-700 pt-6">
4348 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
4349 {__("Trip Type & Duration", "yatra")}
4350 </h3>
4351
4352 {/* Trip Type */}
4353 <div className="mb-4">
4354 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3">
4355 {__("Trip Type", "yatra")}
4356 </label>
4357 <div
4358 className={`grid grid-cols-1 md:grid-cols-2 gap-4 ${
4359 errors.trip_type ? "mb-2" : ""
4360 }`}
4361 >
4362 <label
4363 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
4364 formData.trip_type === "single_day"
4365 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
4366 : errors.trip_type
4367 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4368 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
4369 }`}
4370 >
4371 <input
4372 type="radio"
4373 name="trip_type"
4374 value="single_day"
4375 checked={formData.trip_type === "single_day"}
4376 onChange={(e) => {
4377 handleFieldChange("trip_type", e.target.value);
4378 if (e.target.value === "single_day") {
4379 setFormData((prev) => ({
4380 ...prev,
4381 duration_days: "1",
4382 duration_nights: "0",
4383 }));
4384 }
4385 }}
4386 className="sr-only"
4387 />
4388 <div className="flex flex-1">
4389 <div className="flex flex-col">
4390 <span
4391 className={`block text-sm font-semibold ${
4392 formData.trip_type === "single_day"
4393 ? "text-blue-900 dark:text-blue-300"
4394 : "text-gray-900 dark:text-white"
4395 }`}
4396 >
4397 {__("Single Day Trip", "yatra")}
4398 </span>
4399 <span
4400 className={`mt-1 flex items-center text-sm ${
4401 formData.trip_type === "single_day"
4402 ? "text-blue-700 dark:text-blue-400"
4403 : "text-gray-500 dark:text-gray-400"
4404 }`}
4405 >
4406 {__(
4407 "Trip completed within one day (no overnight stay)",
4408 "yatra",
4409 )}
4410 </span>
4411 </div>
4412 </div>
4413 {formData.trip_type === "single_day" && (
4414 <div className="absolute top-4 right-4">
4415 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
4416 </div>
4417 )}
4418 </label>
4419
4420 <label
4421 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
4422 formData.trip_type === "multi_day"
4423 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
4424 : errors.trip_type
4425 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4426 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
4427 }`}
4428 >
4429 <input
4430 type="radio"
4431 name="trip_type"
4432 value="multi_day"
4433 checked={formData.trip_type === "multi_day"}
4434 onChange={(e) =>
4435 handleFieldChange("trip_type", e.target.value)
4436 }
4437 className="sr-only"
4438 />
4439 <div className="flex flex-1">
4440 <div className="flex flex-col">
4441 <span
4442 className={`block text-sm font-semibold ${
4443 formData.trip_type === "multi_day"
4444 ? "text-blue-900 dark:text-blue-300"
4445 : "text-gray-900 dark:text-white"
4446 }`}
4447 >
4448 {__("Multi-Day Trip", "yatra")}
4449 </span>
4450 <span
4451 className={`mt-1 flex items-center text-sm ${
4452 formData.trip_type === "multi_day"
4453 ? "text-blue-700 dark:text-blue-400"
4454 : "text-gray-500 dark:text-gray-400"
4455 }`}
4456 >
4457 {__(
4458 "Trip spans multiple days with overnight stays",
4459 "yatra",
4460 )}
4461 </span>
4462 </div>
4463 </div>
4464 {formData.trip_type === "multi_day" && (
4465 <div className="absolute top-4 right-4">
4466 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
4467 </div>
4468 )}
4469 </label>
4470 </div>
4471 {errors.trip_type && (
4472 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4473 <AlertCircle className="w-4 h-4" />
4474 {errors.trip_type}
4475 </p>
4476 )}
4477 </div>
4478
4479 {/* Duration Days & Nights */}
4480 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
4481 <div>
4482 <label
4483 htmlFor="duration_days"
4484 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4485 >
4486 {__("Duration (Days)", "yatra")}
4487 </label>
4488 <Input
4489 id="duration_days"
4490 type="number"
4491 min="1"
4492 value={formData.duration_days}
4493 onChange={(e) => {
4494 const days = e.target.value;
4495 handleFieldChange("duration_days", days);
4496 // Auto-set nights based on trip type
4497 if (formData.trip_type === "single_day") {
4498 setFormData((prev) => ({
4499 ...prev,
4500 duration_nights: "0",
4501 }));
4502 } else if (days && parseInt(days) > 1) {
4503 // For multi-day, typically nights = days - 1
4504 const nights = Math.max(
4505 0,
4506 parseInt(days) - 1,
4507 ).toString();
4508 setFormData((prev) => ({
4509 ...prev,
4510 duration_nights: nights,
4511 }));
4512 }
4513 }}
4514 placeholder={
4515 formData.trip_type === "single_day"
4516 ? "1"
4517 : __("e.g., 7", "yatra")
4518 }
4519 className={errors.duration_days ? "border-red-500" : ""}
4520 disabled={formData.trip_type === "single_day"}
4521 />
4522 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4523 {formData.trip_type === "single_day"
4524 ? __("Single day trips are always 1 day", "yatra")
4525 : __(
4526 "Enter the number of days for your trip",
4527 "yatra",
4528 )}
4529 </p>
4530 {errors.duration_days && (
4531 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4532 <AlertCircle className="w-4 h-4" />
4533 {errors.duration_days}
4534 </p>
4535 )}
4536 </div>
4537 <div>
4538 <label
4539 htmlFor="duration_nights"
4540 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4541 >
4542 {__("Duration (Nights)", "yatra")}
4543 </label>
4544 <Input
4545 id="duration_nights"
4546 type="number"
4547 min="0"
4548 value={formData.duration_nights}
4549 onChange={(e) =>
4550 handleFieldChange("duration_nights", e.target.value)
4551 }
4552 placeholder={
4553 formData.trip_type === "single_day"
4554 ? "0"
4555 : __("e.g., 6", "yatra")
4556 }
4557 className={
4558 errors.duration_nights ? "border-red-500" : ""
4559 }
4560 disabled={formData.trip_type === "single_day"}
4561 />
4562 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4563 {formData.trip_type === "single_day"
4564 ? __(
4565 "Single day trips have 0 nights (no overnight stay)",
4566 "yatra",
4567 )
4568 : __(
4569 "Enter the number of nights (typically days - 1)",
4570 "yatra",
4571 )}
4572 </p>
4573 {errors.duration_nights && (
4574 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4575 <AlertCircle className="w-4 h-4" />
4576 {errors.duration_nights}
4577 </p>
4578 )}
4579 </div>
4580 </div>
4581 </div>
4582 </div>
4583 </div>
4584 </div>
4585 );
4586
4587 case "location":
4588 return (
4589 <div className="space-y-4">
4590 <div className="flex flex-wrap items-center gap-2 mb-2">
4591 <MapPin className="w-5 h-5 text-gray-500" />
4592 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
4593 {__("Location & Geography", "yatra")}
4594 </h2>
4595 <Badge
4596 variant="outline"
4597 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
4598 >
4599 {__("Recommended", "yatra")}
4600 </Badge>
4601 </div>
4602 <div className="bg-amber-50 dark:bg-amber-900/20 border-l-4 border-amber-500 dark:border-amber-400 p-3 rounded-r-md">
4603 <p className="text-sm font-medium text-amber-900 dark:text-amber-100 mb-1">
4604 {__(
4605 "Specify where your trip takes place, including destinations, coordinates, and key landmarks.",
4606 "yatra",
4607 )}
4608 </p>
4609 <p className="text-xs text-amber-800 dark:text-amber-200">
4610 {__(
4611 "Optional but highly recommended to help travelers understand the experience.",
4612 "yatra",
4613 )}
4614 </p>
4615 </div>
4616
4617 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
4618 {__(
4619 "Optimize how your trip appears in search engines and social shares",
4620 "yatra",
4621 )}
4622 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
4623 {__(
4624 "Optional, but completing these fields improves SEO and click-through rates.",
4625 "yatra",
4626 )}
4627 </span>
4628 </p>
4629 <div className="space-y-4">
4630 {/* Destinations - Multiple Selection */}
4631 <div>
4632 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
4633 {__("Destinations", "yatra")}
4634 </label>
4635 {destinationsData && destinationsData.length > 0 ? (
4636 <MultiSelect
4637 value={formData.destinations.map((id) => id.toString())}
4638 onChange={(values) =>
4639 handleFieldChange(
4640 "destinations",
4641 values.map((v) => Number(v)),
4642 )
4643 }
4644 options={destinationsData.map((destination: any) => ({
4645 value: destination.id.toString(),
4646 label:
4647 destination.name ||
4648 sprintf(
4649 __("Destination #%s", "yatra"),
4650 String(destination.id),
4651 ),
4652 }))}
4653 placeholder={__("Select destinations...", "yatra")}
4654 searchPlaceholder={__("Search destinations...", "yatra")}
4655 error={!!errors.destinations}
4656 />
4657 ) : (
4658 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
4659 <p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
4660 {__(
4661 "No destinations available. Please create destinations first.",
4662 "yatra",
4663 )}
4664 </p>
4665 <div className="space-y-2">
4666 <p className="text-xs text-gray-400 dark:text-gray-500">
4667 {__("To create destinations:", "yatra")}
4668 </p>
4669 <ol className="text-xs text-gray-400 dark:text-gray-500 text-left list-decimal list-inside space-y-1 max-w-md mx-auto">
4670 <li>
4671 {__(
4672 "Go to Yatra → Destinations in your WordPress admin",
4673 "yatra",
4674 )}
4675 </li>
4676 <li>{__('Click "Add New Destination"', "yatra")}</li>
4677 <li>
4678 {__("Enter destination name and details", "yatra")}
4679 </li>
4680 <li>{__('Set status to "Published"', "yatra")}</li>
4681 </ol>
4682 <a
4683 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=create"
4684 target="_blank"
4685 rel="noopener noreferrer"
4686 className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
4687 >
4688 <svg
4689 className="w-3 h-3 mr-1"
4690 fill="none"
4691 stroke="currentColor"
4692 viewBox="0 0 24 24"
4693 >
4694 <path
4695 strokeLinecap="round"
4696 strokeLinejoin="round"
4697 strokeWidth={2}
4698 d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
4699 />
4700 </svg>
4701 {__("Create Destinations", "yatra")}
4702 </a>
4703 </div>
4704 </div>
4705 )}
4706 <HelpText
4707 text={__(
4708 "Select all destinations included in this trip",
4709 "yatra",
4710 )}
4711 className="mt-2"
4712 />
4713 {errors.destinations && (
4714 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4715 <AlertCircle className="w-4 h-4" />
4716 {errors.destinations}
4717 </p>
4718 )}
4719 </div>
4720
4721 {/* Locations Section - Senior UI/UX Design */}
4722 <div className="space-y-8">
4723 {/* Section Header */}
4724 <div className="text-center space-y-2">
4725 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center justify-center gap-2">
4726 <div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
4727 <MapPin className="w-3 h-3 text-white" />
4728 </div>
4729 {__("Trip Locations", "yatra")}
4730 </h3>
4731 <p className="text-sm text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
4732 {__(
4733 "Set precise starting and ending points with location names and GPS coordinates. Use manual entry or visual map selection for maximum accuracy.",
4734 "yatra",
4735 )}
4736 </p>
4737 </div>
4738
4739 {/* Locations Grid */}
4740 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
4741 {/* STARTING LOCATION */}
4742 <div className="space-y-6">
4743 {/* Location Header */}
4744 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl border border-blue-200 dark:border-blue-800">
4745 <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
4746 <MapPin className="w-5 h-5 text-white" />
4747 </div>
4748 <div className="flex-1">
4749 <h4 className="text-base font-semibold text-blue-900 dark:text-blue-100">
4750 {__("Starting Point", "yatra")}
4751 </h4>
4752 <p className="text-xs text-blue-700 dark:text-blue-300">
4753 {__("Where the journey begins", "yatra")}
4754 </p>
4755 </div>
4756 <div className="flex items-center gap-2">
4757 {formData.starting_location && (
4758 <div
4759 className="w-2 h-2 bg-green-500 rounded-full animate-pulse"
4760 title={__("Location set", "yatra")}
4761 ></div>
4762 )}
4763 {formData.starting_latitude &&
4764 formData.starting_longitude && (
4765 <div
4766 className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"
4767 title={__("Coordinates set", "yatra")}
4768 ></div>
4769 )}
4770 </div>
4771 </div>
4772
4773 {/* Integrated Location Input with Map */}
4774 <div className="space-y-4">
4775 <div className="flex items-center justify-between">
4776 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
4777 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
4778 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
4779 1
4780 </span>
4781 </div>
4782 {__("Starting Location", "yatra")}
4783 </label>
4784 <div className="flex items-center gap-2">
4785 <span className="text-xs text-gray-500 dark:text-gray-400">
4786 {formData.starting_location
4787 ? "✓"
4788 : __("Required", "yatra")}
4789 </span>
4790 {formData.starting_latitude &&
4791 formData.starting_longitude && (
4792 <div className="flex items-center gap-1 text-xs text-green-600">
4793 <div className="w-2 h-2 bg-green-500 rounded-full"></div>
4794 {__("Coords Set", "yatra")}
4795 </div>
4796 )}
4797 </div>
4798 </div>
4799
4800 {/* LocationPicker - Primary Interface */}
4801 <div className="space-y-3">
4802 <LocationPicker
4803 value={{
4804 name: formData.starting_location,
4805 latitude: formData.starting_latitude,
4806 longitude: formData.starting_longitude,
4807 }}
4808 onChange={(locationData) => {
4809 handleFieldChange(
4810 "starting_location",
4811 locationData.name,
4812 );
4813 handleFieldChange(
4814 "starting_latitude",
4815 locationData.latitude,
4816 );
4817 handleFieldChange(
4818 "starting_longitude",
4819 locationData.longitude,
4820 );
4821 }}
4822 label=""
4823 placeholder={__(
4824 "Search for starting location...",
4825 "yatra",
4826 )}
4827 helpText=""
4828 required={false}
4829 defaultMapCenter={
4830 formData.starting_latitude &&
4831 formData.starting_longitude
4832 ? [
4833 parseFloat(formData.starting_latitude),
4834 parseFloat(formData.starting_longitude),
4835 ]
4836 : [20, 0]
4837 }
4838 defaultZoom={
4839 formData.starting_latitude &&
4840 formData.starting_longitude
4841 ? 13
4842 : 2
4843 }
4844 mapHeight="300px"
4845 showMapButton={false}
4846 searchLimit={8}
4847 __={__}
4848 className=""
4849 mapClassName="rounded-lg"
4850 />
4851 </div>
4852 </div>
4853
4854 {/* GPS Coordinates - Manual Override */}
4855 <div className="space-y-4">
4856 <div className="flex items-center justify-between">
4857 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
4858 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
4859 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
4860 2
4861 </span>
4862 </div>
4863 {__("GPS Coordinates", "yatra")}
4864 <span className="text-xs text-gray-500 dark:text-gray-400">
4865 ({__("Manual override", "yatra")})
4866 </span>
4867 </label>
4868 <div className="flex items-center gap-2">
4869 <button
4870 type="button"
4871 onClick={() => {
4872 if (navigator.geolocation) {
4873 navigator.geolocation.getCurrentPosition(
4874 (position) => {
4875 handleFieldChange(
4876 "starting_latitude",
4877 position.coords.latitude.toString(),
4878 );
4879 handleFieldChange(
4880 "starting_longitude",
4881 position.coords.longitude.toString(),
4882 );
4883 },
4884 (error) => {
4885 let message = __(
4886 "Unable to get your location",
4887 "yatra",
4888 );
4889 let showHttpsNotice = false;
4890
4891 switch (error.code) {
4892 case 1: // PERMISSION_DENIED
4893 if (
4894 error.message.includes(
4895 "secure origins",
4896 )
4897 ) {
4898 message = __(
4899 "Location access requires HTTPS. This feature will work on your live HTTPS site.",
4900 "yatra",
4901 );
4902 showHttpsNotice = true;
4903 } else {
4904 message = __(
4905 "Location access denied. Please allow location access in your browser.",
4906 "yatra",
4907 );
4908 }
4909 break;
4910 case 2: // POSITION_UNAVAILABLE
4911 message = __(
4912 "Location information is unavailable. Please try again.",
4913 "yatra",
4914 );
4915 break;
4916 case 3: // TIMEOUT
4917 message = __(
4918 "Location request timed out. Please try again.",
4919 "yatra",
4920 );
4921 break;
4922 }
4923
4924 if (showHttpsNotice) {
4925 alert(
4926 message +
4927 "\n\n" +
4928 __(
4929 "For local development, you can:\n1. Use a browser extension that allows geolocation on HTTP\n2. Set up a local HTTPS certificate\n3. Test on your live HTTPS site",
4930 "yatra",
4931 ),
4932 );
4933 } else {
4934 alert(message);
4935 }
4936 },
4937 );
4938 } else {
4939 alert(
4940 __(
4941 "Geolocation is not supported by your browser",
4942 "yatra",
4943 ),
4944 );
4945 }
4946 }}
4947 className="text-xs bg-green-100 text-green-700 px-3 py-1.5 rounded-lg hover:bg-green-200 transition-colors flex items-center gap-1"
4948 >
4949 <div className="w-3 h-3">
4950 <svg fill="currentColor" viewBox="0 0 20 20">
4951 <path
4952 fillRule="evenodd"
4953 d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
4954 clipRule="evenodd"
4955 />
4956 </svg>
4957 </div>
4958 {__("Use Current Location", "yatra")}
4959 </button>
4960 </div>
4961 </div>
4962
4963 <div className="grid grid-cols-2 gap-3">
4964 <div className="space-y-2">
4965 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
4966 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
4967 {__("Latitude", "yatra")}
4968 </label>
4969 <Input
4970 type="text"
4971 value={formData.starting_latitude}
4972 onChange={(e) =>
4973 handleFieldChange(
4974 "starting_latitude",
4975 e.target.value,
4976 )
4977 }
4978 className="w-full text-sm"
4979 />
4980 </div>
4981 <div className="space-y-2">
4982 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
4983 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
4984 {__("Longitude", "yatra")}
4985 </label>
4986 <Input
4987 type="text"
4988 value={formData.starting_longitude}
4989 onChange={(e) =>
4990 handleFieldChange(
4991 "starting_longitude",
4992 e.target.value,
4993 )
4994 }
4995 placeholder={__("e.g., 115.0920", "yatra")}
4996 className="w-full text-sm"
4997 />
4998 </div>
4999 </div>
5000 <p className="text-xs text-gray-500 dark:text-gray-400">
5001 {__(
5002 "Manual coordinate entry. These will be auto-filled when you select a location from the map above.",
5003 "yatra",
5004 )}
5005 </p>
5006 </div>
5007 </div>
5008
5009 {/* ENDING LOCATION */}
5010 <div className="space-y-6">
5011 {/* Location Header */}
5012 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-xl border border-green-200 dark:border-green-800">
5013 <div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
5014 <CheckCircle2 className="w-5 h-5 text-white" />
5015 </div>
5016 <div className="flex-1">
5017 <h4 className="text-base font-semibold text-green-900 dark:text-green-100">
5018 {__("Ending Point", "yatra")}
5019 </h4>
5020 <p className="text-xs text-green-700 dark:text-green-300">
5021 {__("Where the journey concludes", "yatra")}
5022 </p>
5023 </div>
5024 <div className="flex items-center gap-2">
5025 {formData.ending_location && (
5026 <div
5027 className="w-2 h-2 bg-green-500 rounded-full animate-pulse"
5028 title={__("Location set", "yatra")}
5029 ></div>
5030 )}
5031 </div>
5032 </div>
5033
5034 {/* Integrated Location Input with Map */}
5035 <div className="space-y-4">
5036 <div className="flex items-center justify-between">
5037 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5038 <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
5039 <span className="text-xs font-bold text-green-600 dark:text-green-400">
5040 1
5041 </span>
5042 </div>
5043 {__("Ending Location", "yatra")}
5044 </label>
5045 <div className="flex items-center gap-2">
5046 <span className="text-xs text-gray-500 dark:text-gray-400">
5047 {formData.ending_location
5048 ? "✓"
5049 : __("Required", "yatra")}
5050 </span>
5051 {formData.ending_latitude &&
5052 formData.ending_longitude && (
5053 <div className="flex items-center gap-1 text-xs text-green-600">
5054 <div className="w-2 h-2 bg-green-500 rounded-full"></div>
5055 {__("Coords Set", "yatra")}
5056 </div>
5057 )}
5058 </div>
5059 </div>
5060
5061 {/* LocationPicker - Primary Interface */}
5062 <div className="space-y-3">
5063 <LocationPicker
5064 value={{
5065 name: formData.ending_location,
5066 latitude: formData.ending_latitude,
5067 longitude: formData.ending_longitude,
5068 }}
5069 onChange={(locationData) => {
5070 handleFieldChange(
5071 "ending_location",
5072 locationData.name,
5073 );
5074 handleFieldChange(
5075 "ending_latitude",
5076 locationData.latitude,
5077 );
5078 handleFieldChange(
5079 "ending_longitude",
5080 locationData.longitude,
5081 );
5082 }}
5083 label=""
5084 placeholder={__(
5085 "Search for ending location...",
5086 "yatra",
5087 )}
5088 helpText=""
5089 required={false}
5090 defaultMapCenter={
5091 formData.ending_latitude &&
5092 formData.ending_longitude
5093 ? [
5094 parseFloat(formData.ending_latitude),
5095 parseFloat(formData.ending_longitude),
5096 ]
5097 : formData.starting_latitude &&
5098 formData.starting_longitude
5099 ? [
5100 parseFloat(formData.starting_latitude),
5101 parseFloat(formData.starting_longitude),
5102 ]
5103 : [20, 0]
5104 }
5105 defaultZoom={
5106 formData.ending_latitude &&
5107 formData.ending_longitude
5108 ? 13
5109 : formData.starting_latitude &&
5110 formData.starting_longitude
5111 ? 13
5112 : 2
5113 }
5114 mapHeight="300px"
5115 showMapButton={false}
5116 searchLimit={8}
5117 __={__}
5118 className=""
5119 mapClassName="rounded-lg"
5120 />
5121 </div>
5122 </div>
5123
5124 {/* GPS Coordinates - Manual Override */}
5125 <div className="space-y-4">
5126 <div className="flex items-center justify-between">
5127 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5128 <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
5129 <span className="text-xs font-bold text-green-600 dark:text-green-400">
5130 2
5131 </span>
5132 </div>
5133 {__("GPS Coordinates", "yatra")}
5134 <span className="text-xs text-gray-500 dark:text-gray-400">
5135 ({__("Optional", "yatra")})
5136 </span>
5137 </label>
5138 </div>
5139
5140 <div className="grid grid-cols-2 gap-3">
5141 <div className="space-y-2">
5142 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5143 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
5144 {__("Latitude", "yatra")}
5145 </label>
5146 <Input
5147 type="text"
5148 value={formData.ending_latitude || ""}
5149 onChange={(e) =>
5150 handleFieldChange(
5151 "ending_latitude",
5152 e.target.value,
5153 )
5154 }
5155 placeholder={__("e.g., -8.5069", "yatra")}
5156 className="w-full text-sm"
5157 />
5158 </div>
5159 <div className="space-y-2">
5160 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5161 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
5162 {__("Longitude", "yatra")}
5163 </label>
5164 <Input
5165 type="text"
5166 value={formData.ending_longitude || ""}
5167 onChange={(e) =>
5168 handleFieldChange(
5169 "ending_longitude",
5170 e.target.value,
5171 )
5172 }
5173 placeholder={__("e.g., 115.2625", "yatra")}
5174 className="w-full text-sm"
5175 />
5176 </div>
5177 </div>
5178 <p className="text-xs text-gray-500 dark:text-gray-400">
5179 {__(
5180 "Optional: Manual coordinate entry. Auto-filled when you select a location from the map above.",
5181 "yatra",
5182 )}
5183 </p>
5184 </div>
5185 </div>
5186 </div>
5187 </div>
5188
5189 {/* Landmarks */}
5190 <div>
5191 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
5192 {__("Key Landmarks / Points of Interest", "yatra")}
5193 </label>
5194 <HelpText
5195 text={__(
5196 "Add notable landmarks or points of interest visited during this trip",
5197 "yatra",
5198 )}
5199 className="mb-2"
5200 />
5201 {formData.landmarks.length > 0 ? (
5202 <div className="space-y-2 mb-2">
5203 {formData.landmarks.map((landmark, index) => (
5204 <div
5205 key={index}
5206 className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
5207 >
5208 <MapPin className="w-4 h-4 text-gray-400 flex-shrink-0" />
5209 <span className="flex-1 text-sm text-gray-900 dark:text-white">
5210 {landmark}
5211 </span>
5212 <Button
5213 type="button"
5214 variant="ghost"
5215 size="sm"
5216 onClick={() => {
5217 const newLandmarks = [...formData.landmarks];
5218 newLandmarks.splice(index, 1);
5219 handleFieldChange("landmarks", newLandmarks);
5220 }}
5221 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
5222 >
5223 <X className="w-4 h-4" />
5224 </Button>
5225 </div>
5226 ))}
5227 </div>
5228 ) : (
5229 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center mb-2">
5230 <MapPin className="w-6 h-6 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
5231 <p className="text-xs text-gray-500 dark:text-gray-400">
5232 {__("No landmarks added yet", "yatra")}
5233 </p>
5234 </div>
5235 )}
5236 <Button
5237 type="button"
5238 variant="outline"
5239 onClick={() => {
5240 setShowLandmarkDialog(true);
5241 setLandmarkInput("");
5242 }}
5243 className="w-full"
5244 >
5245 <Plus className="w-4 h-4 mr-2" />
5246 {__("Add Landmark", "yatra")}
5247 </Button>
5248 </div>
5249 </div>
5250 </div>
5251 );
5252
5253 case "duration":
5254 return (
5255 <div className="space-y-6">
5256 <div className="flex flex-wrap items-center gap-2 mb-2">
5257 <Calendar className="w-5 h-5 text-gray-500" />
5258 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
5259 {__("Availability & Booking", "yatra")}
5260 </h2>
5261 <Badge
5262 variant="outline"
5263 className="text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-800"
5264 >
5265 {__("Recommended", "yatra")}
5266 </Badge>
5267 </div>
5268 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
5269 {__("When is it available & who can book it?", "yatra")}
5270 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
5271 {__(
5272 "Set your trip's availability period, capacity limits, and booking requirements. This helps automate bookings and prevent overbooking.",
5273 "yatra",
5274 )}
5275 </span>
5276 </p>
5277
5278 <div className="space-y-6">
5279 {/* SECTION 1: Availability Period */}
5280 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5281 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5282 <Calendar className="w-4 h-4 text-blue-500" />
5283 {__("Availability Period", "yatra")}
5284 </h3>
5285 <div className="space-y-4">
5286 {/* Availability Dates */}
5287 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5288 <div>
5289 <label
5290 htmlFor="available_from"
5291 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5292 >
5293 {__("Available From", "yatra")}
5294 </label>
5295 <DatePicker
5296 value={formData.available_from}
5297 onChange={(val) =>
5298 handleFieldChange("available_from", val)
5299 }
5300 placeholder={__("Select date", "yatra")}
5301 />
5302 <HelpText
5303 text={__(
5304 "Earliest date this trip becomes available for booking",
5305 "yatra",
5306 )}
5307 className="mt-2"
5308 />
5309 </div>
5310 <div>
5311 <label
5312 htmlFor="available_to"
5313 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5314 >
5315 {__("Available To", "yatra")}
5316 </label>
5317 <DatePicker
5318 value={formData.available_to}
5319 onChange={(val) =>
5320 handleFieldChange("available_to", val)
5321 }
5322 placeholder={__("Select date", "yatra")}
5323 />
5324 <HelpText
5325 text={__(
5326 "Latest date this trip is available for booking",
5327 "yatra",
5328 )}
5329 className="mt-2"
5330 />
5331 </div>
5332 </div>
5333
5334 {/* Booking Window */}
5335 <div>
5336 <label
5337 htmlFor="booking_window_days"
5338 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5339 >
5340 {__("Booking Window (Days in Advance)", "yatra")}
5341 </label>
5342 <Input
5343 id="booking_window_days"
5344 type="number"
5345 min="0"
5346 value={formData.booking_window_days}
5347 onChange={(e) =>
5348 handleFieldChange("booking_window_days", e.target.value)
5349 }
5350 placeholder={__("e.g., 30", "yatra")}
5351 />
5352 <HelpText
5353 text={__(
5354 "Minimum days in advance customers can book this trip",
5355 "yatra",
5356 )}
5357 className="mt-2"
5358 />
5359 </div>
5360
5361 {/* Seasonal Availability */}
5362 <div>
5363 <label
5364 htmlFor="seasonal_availability"
5365 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5366 >
5367 {__("Seasonal Availability Notes", "yatra")}
5368 </label>
5369 <Input
5370 id="seasonal_availability"
5371 type="text"
5372 value={formData.seasonal_availability}
5373 onChange={(e) =>
5374 handleFieldChange(
5375 "seasonal_availability",
5376 e.target.value,
5377 )
5378 }
5379 placeholder={__(
5380 "e.g., Available year-round except monsoon season",
5381 "yatra",
5382 )}
5383 />
5384 <HelpText
5385 text={__(
5386 "General notes about when this trip is typically available",
5387 "yatra",
5388 )}
5389 className="mt-2"
5390 />
5391 </div>
5392 </div>
5393 </div>
5394
5395 {/* SECTION 2: Capacity & Travelers */}
5396 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5397 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5398 <Users className="w-4 h-4 text-blue-500" />
5399 {__("Capacity & Travelers", "yatra")}
5400 </h3>
5401 <div className="space-y-4">
5402 {/* Group Size */}
5403 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5404 <div>
5405 <label
5406 htmlFor="min_travelers"
5407 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5408 >
5409 {__("Minimum Travelers", "yatra")}
5410 </label>
5411 <Input
5412 id="min_travelers"
5413 type="number"
5414 min="1"
5415 value={formData.min_travelers}
5416 onChange={(e) =>
5417 handleFieldChange("min_travelers", e.target.value)
5418 }
5419 className={errors.min_travelers ? "border-red-500" : ""}
5420 />
5421 {errors.min_travelers && (
5422 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5423 <AlertCircle className="w-4 h-4" />
5424 {errors.min_travelers}
5425 </p>
5426 )}
5427 </div>
5428 <div>
5429 <label
5430 htmlFor="max_travelers"
5431 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5432 >
5433 {__("Maximum Travelers", "yatra")}
5434 </label>
5435 <Input
5436 id="max_travelers"
5437 type="number"
5438 min="1"
5439 value={formData.max_travelers}
5440 onChange={(e) =>
5441 handleFieldChange("max_travelers", e.target.value)
5442 }
5443 className={errors.max_travelers ? "border-red-500" : ""}
5444 />
5445 {errors.max_travelers && (
5446 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5447 <AlertCircle className="w-4 h-4" />
5448 {errors.max_travelers}
5449 </p>
5450 )}
5451 </div>
5452 </div>
5453
5454 {/* Fallback Settings */}
5455 <div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
5456 <div className="flex items-start gap-2 mb-3">
5457 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
5458 {__("Fallback Settings", "yatra")}
5459 </h4>
5460 <span
5461 title={__(
5462 "These settings are used when the trip has no availability dates or recurring rules set. They provide default values for flexible booking.",
5463 "yatra",
5464 )}
5465 >
5466 <HelpCircle className="w-4 h-4 text-gray-400 cursor-help" />
5467 </span>
5468 </div>
5469
5470 {/* Info Banner */}
5471 <div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
5472 <div className="flex items-start gap-2">
5473 <AlertCircle className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
5474 <div className="text-xs text-amber-800 dark:text-amber-200">
5475 <p className="font-medium mb-1">
5476 {__("When are these settings used?", "yatra")}
5477 </p>
5478 <p className="text-amber-700 dark:text-amber-300">
5479 {__(
5480 "These settings apply ONLY when your trip has ZERO availability dates AND ZERO recurring rules. They provide defaults for flexible booking scenarios.",
5481 "yatra",
5482 )}
5483 </p>
5484 </div>
5485 </div>
5486 </div>
5487
5488 {/* Settings Content - Grouped by Trip Type */}
5489 <div className="space-y-4">
5490 {/* Day Tour Settings */}
5491 {formData.trip_type === "single_day" && (
5492 <div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
5493 <h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5494 <Clock className="w-4 h-4" />
5495 {__("Day Tour Time Settings", "yatra")}
5496 </h5>
5497
5498 {/* Enable Multiple Time Slots Toggle */}
5499 <div className="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
5500 <div className="flex items-start gap-3">
5501 <input
5502 type="checkbox"
5503 id="has_default_time_slots"
5504 checked={formData.has_default_time_slots}
5505 onChange={(e) =>
5506 handleFieldChange(
5507 "has_default_time_slots",
5508 e.target.checked,
5509 )
5510 }
5511 className="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
5512 />
5513 <div className="flex-1">
5514 <label
5515 htmlFor="has_default_time_slots"
5516 className="text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
5517 >
5518 {__("Enable Multiple Time Slots", "yatra")}
5519 </label>
5520 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
5521 {__(
5522 "Allow customers to select from multiple departure times (e.g., Morning, Afternoon, Evening tours)",
5523 "yatra",
5524 )}
5525 </p>
5526 </div>
5527 </div>
5528 </div>
5529
5530 {/* Time Slots Configuration (if enabled) */}
5531 {formData.has_default_time_slots && (
5532 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
5533 <div className="flex items-center justify-between mb-3">
5534 <div>
5535 <label className="block text-sm font-medium text-gray-900 dark:text-white">
5536 {__("Time Slots Configuration", "yatra")}
5537 </label>
5538 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
5539 {__(
5540 "Add multiple departure times for customers to choose from.",
5541 "yatra",
5542 )}
5543 </p>
5544 </div>
5545 <Button
5546 type="button"
5547 variant="outline"
5548 size="sm"
5549 onClick={() => {
5550 const newSlot: TimeSlot = {
5551 id: `slot-${Date.now()}`,
5552 time: "09:00",
5553 label: "",
5554 };
5555 handleFieldChange("default_time_slots", [
5556 ...formData.default_time_slots,
5557 newSlot,
5558 ]);
5559 }}
5560 className="flex items-center gap-1"
5561 >
5562 <Plus className="w-4 h-4" />
5563 {__("Add Time Slot", "yatra")}
5564 </Button>
5565 </div>
5566
5567 {formData.default_time_slots.length === 0 ? (
5568 <div className="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
5569 <Clock className="w-8 h-8 mx-auto text-gray-400 mb-2" />
5570 <p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
5571 {__("No time slots added yet", "yatra")}
5572 </p>
5573 <Button
5574 type="button"
5575 variant="outline"
5576 size="sm"
5577 onClick={() => {
5578 const newSlot: TimeSlot = {
5579 id: `slot-${Date.now()}`,
5580 time: "09:00",
5581 label: __("Morning Tour", "yatra"),
5582 };
5583 handleFieldChange("default_time_slots", [
5584 newSlot,
5585 ]);
5586 }}
5587 >
5588 <Plus className="w-4 h-4 mr-1" />
5589 {__("Add First Time Slot", "yatra")}
5590 </Button>
5591 </div>
5592 ) : (
5593 <div className="space-y-3">
5594 {formData.default_time_slots.map(
5595 (slot, index) => (
5596 <div
5597 key={slot.id}
5598 className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
5599 >
5600 <div className="flex items-center justify-center w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex-shrink-0 mt-1">
5601 <span className="text-sm font-semibold text-blue-600 dark:text-blue-400">
5602 {index + 1}
5603 </span>
5604 </div>
5605 <div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
5606 <div>
5607 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
5608 {__("Time", "yatra")}
5609 </label>
5610 <Input
5611 type="time"
5612 value={slot.time}
5613 onChange={(e) => {
5614 const updated = [
5615 ...formData.default_time_slots,
5616 ];
5617 updated[index] = {
5618 ...slot,
5619 time: e.target.value,
5620 };
5621 handleFieldChange(
5622 "default_time_slots",
5623 updated,
5624 );
5625 }}
5626 className="w-full"
5627 />
5628 </div>
5629 <div>
5630 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
5631 {__("Label", "yatra")}
5632 </label>
5633 <Input
5634 type="text"
5635 value={slot.label}
5636 onChange={(e) => {
5637 const updated = [
5638 ...formData.default_time_slots,
5639 ];
5640 updated[index] = {
5641 ...slot,
5642 label: e.target.value,
5643 };
5644 handleFieldChange(
5645 "default_time_slots",
5646 updated,
5647 );
5648 }}
5649 placeholder={__(
5650 "e.g., Morning Tour, Afternoon Tour",
5651 "yatra",
5652 )}
5653 className="w-full"
5654 />
5655 </div>
5656 </div>
5657 <Button
5658 type="button"
5659 variant="ghost"
5660 size="sm"
5661 onClick={() => {
5662 const updated =
5663 formData.default_time_slots.filter(
5664 (_, i) => i !== index,
5665 );
5666 handleFieldChange(
5667 "default_time_slots",
5668 updated,
5669 );
5670 }}
5671 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 flex-shrink-0 mt-1"
5672 title={__(
5673 "Remove time slot",
5674 "yatra",
5675 )}
5676 >
5677 <X className="w-4 h-4" />
5678 </Button>
5679 </div>
5680 ),
5681 )}
5682 </div>
5683 )}
5684 </div>
5685 )}
5686
5687 {/* Single Departure Time (if multiple slots disabled) */}
5688 {!formData.has_default_time_slots && (
5689 <div>
5690 <label
5691 htmlFor="departure_time_single"
5692 className="block text-sm font-medium text-gray-900 dark:text-white mb-2"
5693 >
5694 {__("Default Departure Time", "yatra")}
5695 </label>
5696 <Input
5697 id="departure_time_single"
5698 type="time"
5699 value={formData.departure_time}
5700 onChange={(e) =>
5701 handleFieldChange(
5702 "departure_time",
5703 e.target.value,
5704 )
5705 }
5706 className="max-w-xs"
5707 />
5708 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
5709 {__(
5710 "Single departure time for all bookings when multiple time slots are not enabled.",
5711 "yatra",
5712 )}
5713 </p>
5714 </div>
5715 )}
5716 </div>
5717 )}
5718
5719 {/* Multi-Day Trip Settings */}
5720 {formData.trip_type === "multi_day" && (
5721 <div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
5722 <h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5723 <Calendar className="w-4 h-4" />
5724 {__("Multi-Day Trip Departure Settings", "yatra")}
5725 </h5>
5726
5727 <div>
5728 <label
5729 htmlFor="departure_time_multiday"
5730 className="block text-sm font-medium text-gray-900 dark:text-white mb-2"
5731 >
5732 {__("Default Departure Time", "yatra")}
5733 </label>
5734 <Input
5735 id="departure_time_multiday"
5736 type="time"
5737 value={formData.departure_time}
5738 onChange={(e) =>
5739 handleFieldChange(
5740 "departure_time",
5741 e.target.value,
5742 )
5743 }
5744 className="max-w-xs"
5745 />
5746 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
5747 {__(
5748 "Default departure time for trips without specific availability dates.",
5749 "yatra",
5750 )}
5751 </p>
5752 </div>
5753 </div>
5754 )}
5755
5756 {/* No Trip Type Selected */}
5757 {!formData.trip_type && (
5758 <div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700 text-center">
5759 <p className="text-sm text-gray-500 dark:text-gray-400">
5760 {__(
5761 "Please select a trip type (Day Tour or Multi-Day) to configure fallback settings.",
5762 "yatra",
5763 )}
5764 </p>
5765 </div>
5766 )}
5767 </div>
5768 </div>
5769 </div>
5770 </div>
5771
5772 {/* SECTION 3: Booking Policies */}
5773 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5774 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5775 <FileText className="w-4 h-4 text-blue-500" />
5776 {__("Booking Policies", "yatra")}
5777 </h3>
5778 <div className="space-y-4">
5779 {/* Age Restrictions */}
5780 <div>
5781 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5782 {__("Age Restrictions", "yatra")}
5783 </h4>
5784 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5785 <div>
5786 <label
5787 htmlFor="age_min"
5788 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5789 >
5790 {__("Minimum Age", "yatra")}
5791 </label>
5792 <Input
5793 id="age_min"
5794 type="number"
5795 min="0"
5796 value={formData.age_min}
5797 onChange={(e) =>
5798 handleFieldChange("age_min", e.target.value)
5799 }
5800 placeholder={__("e.g., 18", "yatra")}
5801 className={errors.age_min ? "border-red-500" : ""}
5802 />
5803 {errors.age_min && (
5804 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5805 <AlertCircle className="w-4 h-4" />
5806 {errors.age_min}
5807 </p>
5808 )}
5809 </div>
5810 <div>
5811 <label
5812 htmlFor="age_max"
5813 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5814 >
5815 {__("Maximum Age", "yatra")}
5816 </label>
5817 <Input
5818 id="age_max"
5819 type="number"
5820 min="0"
5821 value={formData.age_max}
5822 onChange={(e) =>
5823 handleFieldChange("age_max", e.target.value)
5824 }
5825 placeholder={__("e.g., 65", "yatra")}
5826 className={errors.age_max ? "border-red-500" : ""}
5827 />
5828 {errors.age_max && (
5829 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5830 <AlertCircle className="w-4 h-4" />
5831 {errors.age_max}
5832 </p>
5833 )}
5834 </div>
5835 </div>
5836 </div>
5837
5838 {/* Requirements */}
5839 <div>
5840 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5841 {__("Trip Requirements", "yatra")}
5842 </h4>
5843 <div className="space-y-4">
5844 <div>
5845 <label
5846 htmlFor="physical_requirements"
5847 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5848 >
5849 {__("Physical Requirements", "yatra")}
5850 </label>
5851 <textarea
5852 id="physical_requirements"
5853 value={formData.physical_requirements}
5854 onChange={(e) =>
5855 handleFieldChange(
5856 "physical_requirements",
5857 e.target.value,
5858 )
5859 }
5860 placeholder={__(
5861 "e.g., Moderate fitness level required. Some walking involved but no strenuous activities.",
5862 "yatra",
5863 )}
5864 rows={3}
5865 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
5866 />
5867 </div>
5868 <div>
5869 <label
5870 htmlFor="visa_requirements"
5871 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5872 >
5873 {__("Visa Requirements", "yatra")}
5874 </label>
5875 <textarea
5876 id="visa_requirements"
5877 value={formData.visa_requirements}
5878 onChange={(e) =>
5879 handleFieldChange(
5880 "visa_requirements",
5881 e.target.value,
5882 )
5883 }
5884 placeholder={__(
5885 "e.g., Tourist visa required. Can be obtained on arrival or in advance.",
5886 "yatra",
5887 )}
5888 rows={3}
5889 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
5890 />
5891 </div>
5892 <div>
5893 <label
5894 htmlFor="vaccination_requirements"
5895 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5896 >
5897 {__("Vaccination Requirements", "yatra")}
5898 </label>
5899 <textarea
5900 id="vaccination_requirements"
5901 value={formData.vaccination_requirements}
5902 onChange={(e) =>
5903 handleFieldChange(
5904 "vaccination_requirements",
5905 e.target.value,
5906 )
5907 }
5908 placeholder={__(
5909 "e.g., No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.",
5910 "yatra",
5911 )}
5912 rows={3}
5913 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
5914 />
5915 </div>
5916 </div>
5917 </div>
5918
5919 {/* Cancellation Policy */}
5920 <div>
5921 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5922 {__("Cancellation Policy", "yatra")}
5923 </h4>
5924 <textarea
5925 id="cancellation_policy"
5926 value={formData.cancellation_policy}
5927 onChange={(e) =>
5928 handleFieldChange("cancellation_policy", e.target.value)
5929 }
5930 placeholder={__(
5931 "e.g., Free cancellation up to 30 days before departure. 50% refund for cancellations 15-30 days before. No refund for cancellations less than 15 days before.",
5932 "yatra",
5933 )}
5934 rows={4}
5935 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
5936 />
5937 </div>
5938 </div>
5939 </div>
5940
5941 {/* SECTION 3b: Disable Booking (Pro) */}
5942 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5943 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5944 <Calendar className="w-4 h-4 text-blue-500" />
5945 {__("Booking Availability", "yatra")}
5946 <ProBadge isProActive={isPro} />
5947 </h3>
5948 <ProFeature
5949 description={__(
5950 "disable the booking calendar and make trips enquiry-only",
5951 "yatra",
5952 )}
5953 moduleName={__("Enquiry Only Mode", "yatra")}
5954 pricingUrl="https://wpyatra.com/pricing"
5955 isProActive={isPro}
5956 isModuleEnabled={isPro}
5957 >
5958 <div className="flex items-start gap-3">
5959 <div className="flex items-center h-5 mt-0.5">
5960 <input
5961 id="disable_booking"
5962 type="checkbox"
5963 checked={formData.disable_booking}
5964 onChange={(e) =>
5965 handleFieldChange("disable_booking", e.target.checked)
5966 }
5967 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 cursor-pointer"
5968 />
5969 </div>
5970 <div>
5971 <label
5972 htmlFor="disable_booking"
5973 className="block text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
5974 >
5975 {__("Disable Booking Calendar", "yatra")}
5976 </label>
5977 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
5978 {__(
5979 "When enabled, the booking form and date picker will be hidden. Visitors can only send an enquiry for this trip.",
5980 "yatra",
5981 )}
5982 </p>
5983 </div>
5984 </div>
5985 </ProFeature>
5986 </div>
5987
5988 {/* SECTION 4: Accommodation */}
5989 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5990 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5991 <Home className="w-4 h-4 text-blue-500" />
5992 {__("Accommodation", "yatra")}
5993 </h3>
5994 <div className="space-y-4">
5995 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5996 <div>
5997 <label
5998 htmlFor="accommodation_type"
5999 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6000 >
6001 {__("Accommodation Type", "yatra")}
6002 </label>
6003 <Input
6004 id="accommodation_type"
6005 type="text"
6006 value={formData.accommodation_type}
6007 onChange={(e) =>
6008 handleFieldChange(
6009 "accommodation_type",
6010 e.target.value,
6011 )
6012 }
6013 placeholder={__(
6014 "e.g., Hotel, Resort, Teahouse, Camping",
6015 "yatra",
6016 )}
6017 />
6018 </div>
6019 <div>
6020 <label
6021 htmlFor="meal_plan"
6022 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6023 >
6024 {__("Meal Plan", "yatra")}
6025 </label>
6026 <Select
6027 id="meal_plan"
6028 value={formData.meal_plan}
6029 onChange={(e) =>
6030 handleFieldChange("meal_plan", e.target.value)
6031 }
6032 >
6033 {MEAL_PLAN_SELECT_OPTIONS.map(({ value, label }) => (
6034 <option
6035 key={value || "meal-plan-empty"}
6036 value={value}
6037 >
6038 {label}
6039 </option>
6040 ))}
6041 </Select>
6042 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
6043 {__(
6044 "Tells travelers which meals are included with lodging (e.g. breakfast only vs full board).",
6045 "yatra",
6046 )}
6047 </p>
6048 </div>
6049 </div>
6050 <div>
6051 <label
6052 htmlFor="accommodation_details"
6053 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6054 >
6055 {__("Accommodation Details", "yatra")}
6056 </label>
6057 <textarea
6058 id="accommodation_details"
6059 value={formData.accommodation_details}
6060 onChange={(e) =>
6061 handleFieldChange(
6062 "accommodation_details",
6063 e.target.value,
6064 )
6065 }
6066 placeholder={__(
6067 "e.g., 4-star beachfront resort with private balconies, infinity pool, and spa facilities",
6068 "yatra",
6069 )}
6070 rows={3}
6071 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
6072 />
6073 </div>
6074 </div>
6075 </div>
6076
6077 {/* SECTION 5: Transportation */}
6078 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6079 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6080 <Car className="w-4 h-4 text-blue-500" />
6081 {__("Transportation", "yatra")}
6082 </h3>
6083 <div className="space-y-4">
6084 <div className="flex items-center gap-2">
6085 <input
6086 type="checkbox"
6087 id="transportation_included"
6088 checked={formData.transportation_included}
6089 onChange={(e) =>
6090 handleFieldChange(
6091 "transportation_included",
6092 e.target.checked,
6093 )
6094 }
6095 className="w-4 h-4 text-blue-600 bg-white border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
6096 />
6097 <label
6098 htmlFor="transportation_included"
6099 className="text-sm font-medium text-gray-900 dark:text-white"
6100 >
6101 {__("Transportation Included", "yatra")}
6102 </label>
6103 </div>
6104 {formData.transportation_included && (
6105 <>
6106 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6107 <div>
6108 <label
6109 htmlFor="pickup_location"
6110 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6111 >
6112 {__("Pickup Location", "yatra")}
6113 </label>
6114 <Input
6115 id="pickup_location"
6116 type="text"
6117 value={formData.pickup_location}
6118 onChange={(e) =>
6119 handleFieldChange(
6120 "pickup_location",
6121 e.target.value,
6122 )
6123 }
6124 placeholder={__(
6125 "e.g., Airport, Hotel, City Center",
6126 "yatra",
6127 )}
6128 />
6129 </div>
6130 <div>
6131 <label
6132 htmlFor="dropoff_location"
6133 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6134 >
6135 {__("Dropoff Location", "yatra")}
6136 </label>
6137 <Input
6138 id="dropoff_location"
6139 type="text"
6140 value={formData.dropoff_location}
6141 onChange={(e) =>
6142 handleFieldChange(
6143 "dropoff_location",
6144 e.target.value,
6145 )
6146 }
6147 placeholder={__(
6148 "e.g., Airport, Hotel, City Center",
6149 "yatra",
6150 )}
6151 />
6152 </div>
6153 </div>
6154 <div>
6155 <label
6156 htmlFor="transportation_details"
6157 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6158 >
6159 {__("Transportation Details", "yatra")}
6160 </label>
6161 <textarea
6162 id="transportation_details"
6163 value={formData.transportation_details}
6164 onChange={(e) =>
6165 handleFieldChange(
6166 "transportation_details",
6167 e.target.value,
6168 )
6169 }
6170 placeholder={__(
6171 "e.g., Private air-conditioned vehicle with professional driver",
6172 "yatra",
6173 )}
6174 rows={3}
6175 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
6176 />
6177 </div>
6178 </>
6179 )}
6180 </div>
6181 </div>
6182 </div>
6183 </div>
6184 );
6185
6186 case "categorization":
6187 return (
6188 <div className="space-y-6">
6189 <div className="flex flex-wrap items-center gap-2 mb-2">
6190 <Tag className="w-5 h-5 text-gray-500" />
6191 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6192 {__("Categories & Attributes", "yatra")}
6193 </h2>
6194 <Badge
6195 variant="outline"
6196 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
6197 >
6198 {__("Recommended", "yatra")}
6199 </Badge>
6200 </div>
6201 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
6202 {__("How should travelers find this trip?", "yatra")}
6203 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
6204 {__(
6205 "Organize your trip with categories, activities, and custom attributes to help travelers discover it through search and filters.",
6206 "yatra",
6207 )}
6208 </span>
6209 </p>
6210
6211 <div className="space-y-6">
6212 {/* SECTION 1: Categories & Classification */}
6213 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6214 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6215 <Tag className="w-4 h-4 text-blue-500" />
6216 {__("Categories & Classification", "yatra")}
6217 </h3>
6218 <div className="space-y-4">
6219 {/* Trip Category */}
6220 <div>
6221 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6222 {__("Trip Categories", "yatra")}
6223 </label>
6224 {tripCategories && tripCategories.length > 0 ? (
6225 <MultiSelect
6226 value={formData.trip_category}
6227 onChange={(values) =>
6228 handleFieldChange(
6229 "trip_category",
6230 values.map((v) => Number(v)),
6231 )
6232 }
6233 options={(() => {
6234 // Flatten categories with hierarchy indication
6235 const flattenCategories = (
6236 cats: any[],
6237 prefix = "",
6238 ): { value: number; label: string }[] => {
6239 const result: { value: number; label: string }[] =
6240 [];
6241 cats.forEach((cat) => {
6242 result.push({
6243 value: cat.id,
6244 label: prefix + cat.name,
6245 });
6246 if (
6247 cat.subcategories &&
6248 cat.subcategories.length > 0
6249 ) {
6250 result.push(
6251 ...flattenCategories(
6252 cat.subcategories,
6253 prefix + "— ",
6254 ),
6255 );
6256 }
6257 });
6258 return result;
6259 };
6260 return flattenCategories(tripCategories);
6261 })()}
6262 placeholder={__("Select categories...", "yatra")}
6263 searchPlaceholder={__("Search categories...", "yatra")}
6264 />
6265 ) : (
6266 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6267 <p className="text-sm text-gray-500 dark:text-gray-400">
6268 {__("No categories available. ", "yatra")}
6269 <a
6270 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories"
6271 target="_blank"
6272 rel="noopener noreferrer"
6273 className="text-blue-600 hover:text-blue-700 underline"
6274 >
6275 {__("Create categories here", "yatra")}
6276 </a>
6277 </p>
6278 </div>
6279 )}
6280 <HelpText
6281 text={__(
6282 "Select one or more categories for this trip",
6283 "yatra",
6284 )}
6285 className="mt-2"
6286 />
6287 </div>
6288
6289 {/* Difficulty Level */}
6290 <div>
6291 <label
6292 htmlFor="difficulty_level"
6293 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6294 >
6295 {__("Difficulty Level", "yatra")}
6296 </label>
6297 <Select
6298 id="difficulty_level"
6299 value={formData.difficulty_level}
6300 onChange={(e) =>
6301 handleFieldChange("difficulty_level", e.target.value)
6302 }
6303 disabled={isLoadingDifficultyLevels}
6304 >
6305 <option value="">
6306 {__("Select difficulty", "yatra")}
6307 </option>
6308 {isLoadingDifficultyLevels && (
6309 <option value="" disabled>
6310 {__("Loading difficulty levels...", "yatra")}
6311 </option>
6312 )}
6313 {!isLoadingDifficultyLevels &&
6314 difficultyLevels.length === 0 && (
6315 <option value="" disabled>
6316 {__(
6317 "No difficulty levels available - Click below to create",
6318 "yatra",
6319 )}
6320 </option>
6321 )}
6322 {!isLoadingDifficultyLevels &&
6323 difficultyLevels.map((level) => (
6324 <option
6325 key={`difficulty-${level.id}`}
6326 value={level.id?.toString() || ""}
6327 >
6328 {level.name}
6329 </option>
6330 ))}
6331 </Select>
6332 {!isLoadingDifficultyLevels &&
6333 difficultyLevels.length === 0 && (
6334 <div className="mt-3 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md">
6335 <p className="text-sm text-amber-700 dark:text-amber-300">
6336 {__("No difficulty levels available. ", "yatra")}
6337 <a
6338 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=difficulty-levels"
6339 target="_blank"
6340 rel="noopener noreferrer"
6341 className="text-amber-800 hover:text-amber-900 underline font-medium"
6342 >
6343 {__("Create difficulty levels here", "yatra")}
6344 </a>
6345 </p>
6346 </div>
6347 )}
6348 <HelpText
6349 text={__(
6350 "Physical difficulty level required for this trip",
6351 "yatra",
6352 )}
6353 className="mt-2"
6354 />
6355 </div>
6356
6357 {/* Activity Types */}
6358 <div>
6359 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6360 {__("Activity Types", "yatra")}
6361 </label>
6362 {activitiesData && activitiesData.length > 0 ? (
6363 <MultiSelect
6364 value={formData.activity_types}
6365 onChange={(values) =>
6366 handleFieldChange(
6367 "activity_types",
6368 values.map((v) => Number(v)),
6369 )
6370 }
6371 options={activitiesData.map((activity: any) => ({
6372 value: activity.id,
6373 label: activity.name,
6374 }))}
6375 placeholder={__("Select activities...", "yatra")}
6376 searchPlaceholder={__("Search activities...", "yatra")}
6377 />
6378 ) : (
6379 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6380 <p className="text-sm text-gray-500 dark:text-gray-400">
6381 {__("No activities available. ", "yatra")}
6382 <a
6383 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=activities"
6384 target="_blank"
6385 rel="noopener noreferrer"
6386 className="text-blue-600 hover:text-blue-700 underline"
6387 >
6388 {__("Create activities here", "yatra")}
6389 </a>
6390 </p>
6391 </div>
6392 )}
6393 <HelpText
6394 text={__(
6395 "Select all activities included in this trip",
6396 "yatra",
6397 )}
6398 className="mt-2"
6399 />
6400 </div>
6401
6402 {/* Featured Priority */}
6403 <div>
6404 <label
6405 htmlFor="featured_priority"
6406 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6407 >
6408 {__("Featured Priority", "yatra")}
6409 </label>
6410 <Select
6411 id="featured_priority"
6412 value={formData.featured_priority}
6413 onChange={(e) =>
6414 handleFieldChange(
6415 "featured_priority",
6416 e.target.value as TripFormData["featured_priority"],
6417 )
6418 }
6419 >
6420 <option value="none">{__("None", "yatra")}</option>
6421 <option value="featured">
6422 {__("Featured", "yatra")}
6423 </option>
6424 <option value="new">{__("New", "yatra")}</option>
6425 <option value="limited">
6426 {__("Limited Time", "yatra")}
6427 </option>
6428 </Select>
6429 <HelpText
6430 text={__(
6431 "Special designation for frontend display and promotion",
6432 "yatra",
6433 )}
6434 className="mt-2"
6435 />
6436 </div>
6437 </div>
6438 </div>
6439
6440 {/* SECTION 2: Custom Attributes */}
6441 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6442 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6443 <Settings className="w-4 h-4 text-blue-500" />
6444 {__("Custom Attributes", "yatra")}
6445 </h3>
6446 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
6447 {__(
6448 "Add custom attributes for advanced filtering and search capabilities.",
6449 "yatra",
6450 )}
6451 </p>
6452 <TripAttributesSection
6453 formData={formData}
6454 handleFieldChange={handleFieldChange}
6455 tripId={tripId || undefined}
6456 isEditMode={isEditMode}
6457 tripAttributesData={tripAttributesData}
6458 tripAttributesReady={tripAttributesReady}
6459 />
6460 </div>
6461 </div>
6462 </div>
6463 );
6464
6465 case "media":
6466 return (
6467 <div className="space-y-6">
6468 <div className="flex flex-wrap items-center gap-2 mb-2">
6469 <Image className="w-5 h-5 text-gray-500" />
6470 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6471 {__("Media & Content", "yatra")}
6472 </h2>
6473 <Badge
6474 variant="outline"
6475 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
6476 >
6477 {__("Recommended", "yatra")}
6478 </Badge>
6479 </div>
6480 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
6481 {__(
6482 "Add visual content, videos, stories, and testimonials to showcase your trip",
6483 "yatra",
6484 )}
6485 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
6486 {__(
6487 "Optional, but adding rich media greatly increases engagement and conversions.",
6488 "yatra",
6489 )}
6490 </span>
6491 </p>
6492
6493 <div className="space-y-6">
6494 {/* Photo Gallery */}
6495 <Card>
6496 <CardHeader>
6497 <CardTitle>{__("Photo Gallery", "yatra")}</CardTitle>
6498 <CardDescription>
6499 {__(
6500 "Upload images to showcase your trip. These will be displayed on the trip page.",
6501 "yatra",
6502 )}
6503 </CardDescription>
6504 </CardHeader>
6505 <CardContent>
6506 <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
6507 {formData.gallery_images.map((image, index) => (
6508 <div key={index} className="relative group">
6509 <div className="aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden">
6510 <img
6511 src={image.thumbnail_url || image.url}
6512 alt={image.alt_text || `Gallery ${index + 1}`}
6513 className="w-full h-full object-cover"
6514 />
6515 </div>
6516 {/* Reorder buttons */}
6517 <div className="absolute top-2 left-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
6518 {index > 0 && (
6519 <button
6520 type="button"
6521 onClick={() =>
6522 handleGalleryReorder(index, index - 1)
6523 }
6524 className="bg-black/60 text-white rounded p-1 hover:bg-black/80"
6525 title={__("Move left", "yatra")}
6526 >
6527 <ChevronLeft className="w-4 h-4" />
6528 </button>
6529 )}
6530 {index < formData.gallery_images.length - 1 && (
6531 <button
6532 type="button"
6533 onClick={() =>
6534 handleGalleryReorder(index, index + 1)
6535 }
6536 className="bg-black/60 text-white rounded p-1 hover:bg-black/80"
6537 title={__("Move right", "yatra")}
6538 >
6539 <ChevronRight className="w-4 h-4" />
6540 </button>
6541 )}
6542 </div>
6543 {/* Order badge */}
6544 <span className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-1.5 py-0.5 rounded">
6545 {index + 1}
6546 </span>
6547 {/* Remove button */}
6548 <button
6549 type="button"
6550 onClick={() => handleGalleryRemove(index)}
6551 className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
6552 >
6553 <X className="w-4 h-4" />
6554 </button>
6555 </div>
6556 ))}
6557 <button
6558 type="button"
6559 onClick={handleGalleryAdd}
6560 className={`aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${
6561 errors.gallery_images
6562 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
6563 : "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400"
6564 }`}
6565 >
6566 <Upload
6567 className={`w-8 h-8 mb-2 ${errors.gallery_images ? "text-red-400" : "text-gray-400"}`}
6568 />
6569 <span
6570 className={`text-sm ${errors.gallery_images ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`}
6571 >
6572 {__("Add Image", "yatra")}
6573 </span>
6574 </button>
6575 </div>
6576 {errors.gallery_images && (
6577 <p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6578 <AlertCircle className="w-4 h-4" />
6579 {errors.gallery_images}
6580 </p>
6581 )}
6582 </CardContent>
6583 </Card>
6584 {/* Video & Virtual Tour */}
6585 <Card>
6586 <CardHeader>
6587 <CardTitle>{__("Video & Virtual Tour", "yatra")}</CardTitle>
6588 <CardDescription>
6589 {__(
6590 "Add video content to showcase your trip visually",
6591 "yatra",
6592 )}
6593 </CardDescription>
6594 </CardHeader>
6595 <CardContent className="space-y-4">
6596 <div>
6597 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6598 {__("Video URL", "yatra")} (YouTube/Vimeo)
6599 </label>
6600 <HelpText
6601 text={__(
6602 "Paste the full URL from YouTube or Vimeo. The video will be embedded on your trip page.",
6603 "yatra",
6604 )}
6605 className="mb-2"
6606 />
6607 <Input
6608 type="url"
6609 value={formData.video_url}
6610 onChange={(e) =>
6611 handleFieldChange("video_url", e.target.value)
6612 }
6613 placeholder="https://www.youtube.com/watch?v=..."
6614 className={`font-mono text-xs ${errors.video_url ? "border-red-500" : ""}`}
6615 />
6616 {errors.video_url && (
6617 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6618 <AlertCircle className="w-4 h-4" />
6619 {errors.video_url}
6620 </p>
6621 )}
6622 </div>
6623 <div>
6624 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6625 {__("360° Virtual Tour URL", "yatra")}
6626 </label>
6627 <HelpText
6628 text={__(
6629 "Add a link to an interactive 360° virtual tour if available",
6630 "yatra",
6631 )}
6632 className="mb-2"
6633 />
6634 <Input
6635 type="url"
6636 value={formData.virtual_tour_url}
6637 onChange={(e) =>
6638 handleFieldChange("virtual_tour_url", e.target.value)
6639 }
6640 placeholder="https://..."
6641 className={`font-mono text-xs ${errors.virtual_tour_url ? "border-red-500" : ""}`}
6642 />
6643 {errors.virtual_tour_url && (
6644 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6645 <AlertCircle className="w-4 h-4" />
6646 {errors.virtual_tour_url}
6647 </p>
6648 )}
6649 </div>
6650 </CardContent>
6651 </Card>
6652
6653 {/* Trip Story & What Makes Special */}
6654 <Card>
6655 <CardHeader>
6656 <CardTitle>
6657 {__("Trip Story & Special Features", "yatra")}
6658 </CardTitle>
6659 <CardDescription>
6660 {__(
6661 "Tell an engaging story and highlight what makes this trip unique",
6662 "yatra",
6663 )}
6664 </CardDescription>
6665 </CardHeader>
6666 <CardContent className="space-y-4">
6667 <div>
6668 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6669 {__("What Makes This Trip Special", "yatra")}
6670 </label>
6671 <HelpText
6672 text={__(
6673 "Highlight the unique selling points and what sets this trip apart from others",
6674 "yatra",
6675 )}
6676 className="mb-2"
6677 />
6678 <textarea
6679 value={formData.what_makes_special}
6680 onChange={(e) =>
6681 handleFieldChange("what_makes_special", e.target.value)
6682 }
6683 placeholder={__(
6684 "Describe what makes this trip unique and special...",
6685 "yatra",
6686 )}
6687 rows={5}
6688 className="flex w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 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:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 resize-none transition-colors"
6689 />
6690 </div>
6691 <div>
6692 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6693 {__("Trip Story / Narrative", "yatra")}
6694 </label>
6695 <HelpText
6696 text={__(
6697 "Tell an engaging story about this trip. Use storytelling to connect with potential travelers emotionally",
6698 "yatra",
6699 )}
6700 className="mb-2"
6701 />
6702 <textarea
6703 value={formData.trip_story}
6704 onChange={(e) =>
6705 handleFieldChange("trip_story", e.target.value)
6706 }
6707 placeholder={__(
6708 "Write an engaging narrative about this trip...",
6709 "yatra",
6710 )}
6711 rows={8}
6712 className="flex w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 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:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 resize-none transition-colors"
6713 />
6714 </div>
6715 </CardContent>
6716 </Card>
6717
6718 {/* Testimonials - Select from Reviews */}
6719 <TestimonialsSelector
6720 tripId={tripId}
6721 selectedReviewIds={formData.testimonial_review_ids}
6722 onChange={(reviewIds) =>
6723 handleFieldChange("testimonial_review_ids", reviewIds)
6724 }
6725 />
6726 </div>
6727 </div>
6728 );
6729
6730 case "downloads":
6731 if (!showDownloadsUI) {
6732 return (
6733 <div className="space-y-6">
6734 <Alert variant="info">
6735 {__("Downloads module is not enabled.", "yatra")}
6736 </Alert>
6737 </div>
6738 );
6739 }
6740
6741 return (
6742 <div className="space-y-6">
6743 <div className="flex flex-wrap items-center gap-2 mb-2">
6744 <Download className="w-5 h-5 text-gray-500" />
6745 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6746 {__("Downloads", "yatra")}
6747 </h2>
6748 </div>
6749 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
6750 {__(
6751 "Attach files to this trip and control who can access them.",
6752 "yatra",
6753 )}
6754 </p>
6755
6756 <Card>
6757 <CardHeader>
6758 <CardTitle>{__("Downloads", "yatra")}</CardTitle>
6759 <CardDescription>
6760 {__(
6761 "Attach files to this trip and control who can access them.",
6762 "yatra",
6763 )}
6764 </CardDescription>
6765 </CardHeader>
6766 <CardContent className="space-y-4">
6767 {(formData.downloadable_items || []).length > 0 ? (
6768 <div className="space-y-3">
6769 {(formData.downloadable_items || []).map((item, index) => (
6770 <div
6771 key={index}
6772 className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg space-y-3"
6773 >
6774 <div className="flex items-start justify-between gap-2">
6775 <div className="flex-1">
6776 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6777 {__("Title", "yatra")}
6778 </label>
6779 <Input
6780 value={item.title}
6781 onChange={(e) =>
6782 handleDownloadableItemChange(
6783 index,
6784 "title",
6785 e.target.value,
6786 )
6787 }
6788 placeholder={__(
6789 "E.g. Packing list, itinerary PDF, waiver form...",
6790 "yatra",
6791 )}
6792 />
6793 </div>
6794
6795 <div className="flex gap-1 pt-7">
6796 {index > 0 && (
6797 <Button
6798 type="button"
6799 variant="ghost"
6800 size="sm"
6801 onClick={() =>
6802 handleDownloadableItemMove(index, index - 1)
6803 }
6804 title={__("Move up", "yatra")}
6805 >
6806 <ChevronUp className="w-4 h-4" />
6807 </Button>
6808 )}
6809 {index <
6810 (formData.downloadable_items || []).length -
6811 1 && (
6812 <Button
6813 type="button"
6814 variant="ghost"
6815 size="sm"
6816 onClick={() =>
6817 handleDownloadableItemMove(index, index + 1)
6818 }
6819 title={__("Move down", "yatra")}
6820 >
6821 <ChevronDown className="w-4 h-4" />
6822 </Button>
6823 )}
6824 <Button
6825 type="button"
6826 variant="ghost"
6827 size="sm"
6828 onClick={() =>
6829 handleDownloadableItemRemove(index)
6830 }
6831 className="text-red-600 hover:text-red-700"
6832 title={__("Remove", "yatra")}
6833 >
6834 <X className="w-4 h-4" />
6835 </Button>
6836 </div>
6837 </div>
6838
6839 <div>
6840 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6841 {__("Description", "yatra")} (
6842 {__("Optional", "yatra")})
6843 </label>
6844 <textarea
6845 value={item.description}
6846 onChange={(e) =>
6847 handleDownloadableItemChange(
6848 index,
6849 "description",
6850 e.target.value,
6851 )
6852 }
6853 rows={3}
6854 className="flex w-full rounded-md border-2 border-gray-300 bg-white px-4 py-2.5 text-base font-normal text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 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:text-gray-100 dark:ring-offset-gray-900 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 resize-none transition-colors"
6855 />
6856 </div>
6857
6858 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6859 <div>
6860 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6861 {__("Visibility", "yatra")}
6862 </label>
6863 <Select
6864 value={item.visibility}
6865 onChange={(e) =>
6866 handleDownloadableItemChange(
6867 index,
6868 "visibility",
6869 e.target.value,
6870 )
6871 }
6872 >
6873 <option value="public">
6874 {__("Public", "yatra")}
6875 </option>
6876 <option value="logged_in">
6877 {__("Private (My Account only)", "yatra")}
6878 </option>
6879 <option value="booked_only">
6880 {__(
6881 "Private (Booking confirmation only)",
6882 "yatra",
6883 )}
6884 </option>
6885 </Select>
6886 </div>
6887 <div>
6888 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6889 {__("File", "yatra")}
6890 </label>
6891 <div className="flex items-center gap-2">
6892 {item.attachment_url ? (
6893 <a
6894 href={item.attachment_url}
6895 target="_blank"
6896 rel="noreferrer"
6897 className="flex-shrink-0"
6898 title={__("View file", "yatra")}
6899 >
6900 {/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(
6901 item.attachment_url,
6902 ) ? (
6903 <img
6904 src={item.attachment_url}
6905 alt={
6906 item.attachment_title ||
6907 __("Selected file", "yatra")
6908 }
6909 className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 object-cover bg-white dark:bg-gray-900"
6910 />
6911 ) : (
6912 <div className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 flex items-center justify-center bg-gray-50 dark:bg-gray-900">
6913 <FileText className="w-4 h-4 text-gray-500 dark:text-gray-400" />
6914 </div>
6915 )}
6916 </a>
6917 ) : (
6918 <div className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 flex items-center justify-center bg-gray-50 dark:bg-gray-900 flex-shrink-0">
6919 <FileText className="w-4 h-4 text-gray-400 dark:text-gray-600" />
6920 </div>
6921 )}
6922
6923 <div className="min-w-0 flex-1">
6924 <div className="flex items-center gap-2 min-w-0">
6925 <Button
6926 type="button"
6927 variant="outline"
6928 onClick={() =>
6929 handleDownloadableItemSelectFile(index)
6930 }
6931 className="flex items-center gap-2 flex-shrink-0"
6932 >
6933 <Upload className="w-4 h-4" />
6934 {item.attachment_id
6935 ? __("Change File", "yatra")
6936 : __("Select File", "yatra")}
6937 </Button>
6938 <span className="text-xs text-gray-600 dark:text-gray-400 truncate min-w-0">
6939 {item.attachment_title ||
6940 (item.attachment_id
6941 ? `#${item.attachment_id}`
6942 : __("No file selected", "yatra"))}
6943 </span>
6944 {item.attachment_url && (
6945 <a
6946 href={item.attachment_url}
6947 target="_blank"
6948 rel="noreferrer"
6949 className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 flex-shrink-0"
6950 >
6951 <Eye className="w-3 h-3" />
6952 {__("View", "yatra")}
6953 </a>
6954 )}
6955 </div>
6956 </div>
6957 </div>
6958 </div>
6959 </div>
6960
6961 <div className="flex items-start gap-3">
6962 <div className="flex items-center gap-2 pt-0.5">
6963 <input
6964 type="checkbox"
6965 checked={item.enabled}
6966 onChange={(e) =>
6967 handleDownloadableItemChange(
6968 index,
6969 "enabled",
6970 e.target.checked,
6971 )
6972 }
6973 className="h-4 w-4"
6974 />
6975 <span className="text-sm text-gray-700 dark:text-gray-300">
6976 {__("Enabled", "yatra")}
6977 </span>
6978 </div>
6979 <div className="flex-1">
6980 <p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed">
6981 {__(
6982 "Controls whether this file appears and is available for download on the trip page. When disabled, the file is completely hidden from users.",
6983 "yatra",
6984 )}
6985 </p>
6986 </div>
6987 </div>
6988 </div>
6989 ))}
6990 </div>
6991 ) : (
6992 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6993 <FileText className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
6994 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
6995 {__("No downloads added yet", "yatra")}
6996 </p>
6997 </div>
6998 )}
6999
7000 <Button
7001 type="button"
7002 variant="outline"
7003 onClick={handleDownloadableItemAdd}
7004 className="w-full"
7005 >
7006 <Plus className="w-4 h-4 mr-2" />
7007 {__("Add Download", "yatra")}
7008 </Button>
7009 </CardContent>
7010 </Card>
7011 </div>
7012 );
7013
7014 case "pricing":
7015 return (
7016 <div className="space-y-4">
7017 <div className="flex flex-wrap items-center gap-2 mb-2">
7018 <DollarSign className="w-5 h-5 text-gray-500" />
7019 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7020 {__("Pricing & Payment", "yatra")}
7021 </h2>
7022 <Badge
7023 variant="outline"
7024 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
7025 >
7026 {__("Recommended", "yatra")}
7027 </Badge>
7028 </div>
7029 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7030 {__("How much does it cost?", "yatra")}
7031 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7032 {__(
7033 "Set pricing for different traveler types and payment options. Optional for drafts, but adding pricing now makes publishing easier.",
7034 "yatra",
7035 )}
7036 </span>
7037 </p>
7038
7039 <div className="space-y-4">
7040 {/* Pricing Type Selection */}
7041 <div>
7042 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3">
7043 {__("Pricing Type", "yatra")}
7044 </label>
7045 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7046 <label
7047 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
7048 formData.pricing_type === "regular"
7049 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
7050 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
7051 }`}
7052 >
7053 <input
7054 type="radio"
7055 name="pricing_type"
7056 value="regular"
7057 checked={formData.pricing_type === "regular"}
7058 onChange={(e) =>
7059 handleFieldChange("pricing_type", e.target.value)
7060 }
7061 className="sr-only"
7062 />
7063 <div className="flex flex-1">
7064 <div className="flex flex-col">
7065 <span
7066 className={`block text-sm font-semibold ${
7067 formData.pricing_type === "regular"
7068 ? "text-blue-900 dark:text-blue-300"
7069 : "text-gray-900 dark:text-white"
7070 }`}
7071 >
7072 {__("Regular Pricing", "yatra")}
7073 </span>
7074 <span
7075 className={`mt-1 flex items-center text-sm ${
7076 formData.pricing_type === "regular"
7077 ? "text-blue-700 dark:text-blue-400"
7078 : "text-gray-500 dark:text-gray-400"
7079 }`}
7080 >
7081 {__("Set a single price for all travelers", "yatra")}
7082 </span>
7083 </div>
7084 </div>
7085 {formData.pricing_type === "regular" && (
7086 <div className="absolute top-4 right-4">
7087 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
7088 </div>
7089 )}
7090 </label>
7091
7092 <label
7093 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
7094 formData.pricing_type === "traveler_based"
7095 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
7096 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
7097 }`}
7098 >
7099 <input
7100 type="radio"
7101 name="pricing_type"
7102 value="traveler_based"
7103 checked={formData.pricing_type === "traveler_based"}
7104 onChange={(e) =>
7105 handleFieldChange("pricing_type", e.target.value)
7106 }
7107 className="sr-only"
7108 />
7109 <div className="flex flex-1">
7110 <div className="flex flex-col">
7111 <span
7112 className={`block text-sm font-semibold ${
7113 formData.pricing_type === "traveler_based"
7114 ? "text-blue-900 dark:text-blue-300"
7115 : "text-gray-900 dark:text-white"
7116 }`}
7117 >
7118 {__("Traveler-Based Pricing", "yatra")}
7119 </span>
7120 <span
7121 className={`mt-1 flex items-center text-sm ${
7122 formData.pricing_type === "traveler_based"
7123 ? "text-blue-700 dark:text-blue-400"
7124 : "text-gray-500 dark:text-gray-400"
7125 }`}
7126 >
7127 {__(
7128 "Set different prices for each traveler category",
7129 "yatra",
7130 )}
7131 </span>
7132 </div>
7133 </div>
7134 {formData.pricing_type === "traveler_based" && (
7135 <div className="absolute top-4 right-4">
7136 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
7137 </div>
7138 )}
7139 </label>
7140 </div>
7141 </div>
7142
7143 {/* Regular Pricing */}
7144 {formData.pricing_type === "regular" && (
7145 <div className="space-y-4">
7146 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7147 <div>
7148 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7149 {__("Original Price", "yatra")}
7150 </label>
7151 <div className="relative">
7152 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7153 {getCurrencySymbol(globalCurrency)}
7154 </span>
7155 <Input
7156 type="number"
7157 step="0.01"
7158 min="0"
7159 value={formData.original_price}
7160 onChange={(e) =>
7161 handleFieldChange("original_price", e.target.value)
7162 }
7163 placeholder="0.00"
7164 className={`pl-7 ${errors.original_price ? "border-red-500" : ""}`}
7165 />
7166 </div>
7167 {errors.original_price && (
7168 <p className="mt-1 text-xs text-red-600">
7169 {errors.original_price}
7170 </p>
7171 )}
7172 </div>
7173
7174 <div>
7175 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7176 {__("Discounted Price", "yatra")} (
7177 {__("Optional", "yatra")})
7178 </label>
7179 <div className="relative">
7180 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7181 {getCurrencySymbol(globalCurrency)}
7182 </span>
7183 <Input
7184 type="number"
7185 step="0.01"
7186 min="0"
7187 value={formData.discounted_price}
7188 onChange={(e) =>
7189 handleFieldChange(
7190 "discounted_price",
7191 e.target.value,
7192 )
7193 }
7194 placeholder="0.00"
7195 className={`pl-7 ${errors.discounted_price ? "border-red-500" : ""}`}
7196 />
7197 </div>
7198 {errors.discounted_price && (
7199 <p className="mt-1 text-xs text-red-600">
7200 {errors.discounted_price}
7201 </p>
7202 )}
7203 </div>
7204 </div>
7205 </div>
7206 )}
7207
7208 {/* Traveler Category Pricing */}
7209 {formData.pricing_type === "traveler_based" && (
7210 <div>
7211 <div className="flex items-center justify-between mb-3">
7212 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400">
7213 {__("Traveler Category Pricing", "yatra")}
7214 </label>
7215 </div>
7216 <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
7217 {__(
7218 "Add pricing for traveler categories. Categories are managed in Traveler Categories page.",
7219 "yatra",
7220 )}
7221 </p>
7222 {isLoadingCategories ? (
7223 <div className="flex items-center justify-center py-8">
7224 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
7225 </div>
7226 ) : activeCategories.length === 0 ? (
7227 <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
7228 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
7229 {__("No active traveler categories found.", "yatra")}
7230 </p>
7231 <Button
7232 type="button"
7233 variant="outline"
7234 onClick={() =>
7235 (window.location.href =
7236 "?page=yatra&subpage=traveler-categories&action=create")
7237 }
7238 className="flex items-center gap-2 mx-auto"
7239 >
7240 <Plus className="w-4 h-4" />
7241 {__("Create Category", "yatra")}
7242 </Button>
7243 </div>
7244 ) : (
7245 <div className="space-y-4">
7246 {/* Add Pricing Button */}
7247 <div className="relative">
7248 <Button
7249 type="button"
7250 variant="outline"
7251 onClick={() =>
7252 setShowCategorySelector(!showCategorySelector)
7253 }
7254 className="flex items-center gap-2"
7255 disabled={
7256 activeCategories.filter(
7257 (cat) =>
7258 !formData.price_types.some(
7259 (pt) =>
7260 Number(pt.category_id) === Number(cat.id),
7261 ),
7262 ).length === 0
7263 }
7264 >
7265 <Plus className="w-4 h-4" />
7266 {__("Add Pricing", "yatra")}
7267 </Button>
7268
7269 {/* Category Selection Dropdown */}
7270 {showCategorySelector && (
7271 <>
7272 <div
7273 className="fixed inset-0 z-10"
7274 onClick={() => setShowCategorySelector(false)}
7275 />
7276 <div className="absolute top-full left-0 mt-2 w-full max-w-md bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-20 max-h-96 overflow-y-auto">
7277 <div className="p-2">
7278 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
7279 {__(
7280 "Select a category to add pricing",
7281 "yatra",
7282 )}
7283 </div>
7284 {activeCategories.filter(
7285 (cat) =>
7286 !formData.price_types.some(
7287 (pt) =>
7288 Number(pt.category_id) ===
7289 Number(cat.id),
7290 ),
7291 ).length === 0 ? (
7292 <div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
7293 {__(
7294 "All categories have pricing added",
7295 "yatra",
7296 )}
7297 </div>
7298 ) : (
7299 <div className="space-y-1">
7300 {activeCategories
7301 .filter(
7302 (cat) =>
7303 !formData.price_types.some(
7304 (pt) =>
7305 Number(pt.category_id) ===
7306 Number(cat.id),
7307 ),
7308 )
7309 .map((category) => {
7310 const ageRange =
7311 category.age_min !== undefined ||
7312 category.age_max !== undefined
7313 ? category.age_min !== undefined &&
7314 category.age_max !== undefined
7315 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
7316 : category.age_min !== undefined
7317 ? `${category.age_min}+ ${__("years", "yatra")}`
7318 : category.age_max !== undefined
7319 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
7320 : ""
7321 : null;
7322
7323 const pricingMode =
7324 category.pricing_mode || "per_person";
7325 const hasMin =
7326 category.min_pax !== null &&
7327 category.min_pax !== undefined;
7328 const hasMax =
7329 category.max_pax !== null &&
7330 category.max_pax !== undefined;
7331
7332 let pricingLabel = "";
7333 if (pricingMode === "per_group") {
7334 if (hasMin && hasMax) {
7335 pricingLabel = `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`;
7336 } else if (hasMin) {
7337 pricingLabel = `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`;
7338 } else if (hasMax) {
7339 pricingLabel = `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`;
7340 } else {
7341 pricingLabel = __(
7342 "Per group",
7343 "yatra",
7344 );
7345 }
7346 } else {
7347 pricingLabel = __(
7348 "Per person",
7349 "yatra",
7350 );
7351 }
7352
7353 return (
7354 <button
7355 key={category.id}
7356 type="button"
7357 onClick={() => {
7358 handlePriceTypeAdd(category.id);
7359 setShowCategorySelector(false);
7360 }}
7361 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
7362 >
7363 <div className="font-medium text-sm text-gray-900 dark:text-white">
7364 {category.label}
7365 {ageRange && (
7366 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
7367 ({ageRange})
7368 </span>
7369 )}
7370 </div>
7371 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7372 {category.description}
7373 {pricingLabel && (
7374 <span className="ml-1 text-[11px] font-medium text-gray-600 dark:text-gray-300">
7375 • {pricingLabel}
7376 </span>
7377 )}
7378 </div>
7379 </button>
7380 );
7381 })}
7382 </div>
7383 )}
7384 </div>
7385 </div>
7386 </>
7387 )}
7388 </div>
7389
7390 {/* Added Pricing List */}
7391 {formData.price_types.length > 0 && (
7392 <div className="space-y-3">
7393 {formData.price_types.map((priceType, index) => {
7394 // Use Number() to ensure consistent comparison (handles string/number mismatch)
7395 const category = activeCategories.find(
7396 (cat) =>
7397 Number(cat.id) ===
7398 Number(priceType.category_id),
7399 );
7400 if (!category) {
7401 return null;
7402 }
7403
7404 return (
7405 <div
7406 key={priceType.category_id}
7407 className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
7408 >
7409 <div className="flex items-start justify-between mb-3">
7410 <div className="flex-1">
7411 <div className="flex items-center gap-2 mb-1">
7412 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
7413 {category.label}
7414 {(category.age_min !== undefined ||
7415 category.age_max !== undefined) && (
7416 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
7417 (
7418 {category.age_min !== undefined &&
7419 category.age_max !== undefined
7420 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
7421 : category.age_min !== undefined
7422 ? `${category.age_min}+ ${__("years", "yatra")}`
7423 : category.age_max !== undefined
7424 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
7425 : ""}
7426 )
7427 </span>
7428 )}
7429
7430 {/* Pricing mode & group size */}
7431 <span className="ml-2 inline-flex items-center gap-1 text-[11px] font-medium text-gray-600 dark:text-gray-300">
7432 {(() => {
7433 const pricingMode =
7434 category.pricing_mode ||
7435 "per_person";
7436 const hasMin =
7437 category.min_pax !== null &&
7438 category.min_pax !== undefined;
7439 const hasMax =
7440 category.max_pax !== null &&
7441 category.max_pax !== undefined;
7442
7443 if (pricingMode === "per_group") {
7444 if (hasMin && hasMax) {
7445 return `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`;
7446 }
7447 if (hasMin) {
7448 return `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`;
7449 }
7450 if (hasMax) {
7451 return `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`;
7452 }
7453 return __("Per group", "yatra");
7454 }
7455
7456 return __("Per person", "yatra");
7457 })()}
7458 </span>
7459 </h4>
7460 </div>
7461 <p className="text-xs text-gray-600 dark:text-gray-400">
7462 {category.description}
7463 </p>
7464 </div>
7465 <button
7466 type="button"
7467 onClick={() =>
7468 handlePriceTypeRemove(category.id)
7469 }
7470 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
7471 title={__("Remove Pricing", "yatra")}
7472 >
7473 <X className="w-4 h-4" />
7474 </button>
7475 </div>
7476 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
7477 <div>
7478 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
7479 {__("Original Price", "yatra")}
7480 </label>
7481 <div className="relative">
7482 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7483 {getCurrencySymbol(globalCurrency)}
7484 </span>
7485 <Input
7486 type="number"
7487 step="0.01"
7488 min="0"
7489 value={priceType.original_price}
7490 onChange={(e) =>
7491 handlePriceTypeChange(
7492 category.id,
7493 "original_price",
7494 e.target.value,
7495 )
7496 }
7497 placeholder="0.00"
7498 className={`pl-7 ${errors[`price_type_${index}_original`] ? "border-red-500" : ""}`}
7499 />
7500 </div>
7501 {errors[`price_type_${index}_original`] && (
7502 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
7503 {errors[`price_type_${index}_original`]}
7504 </p>
7505 )}
7506 </div>
7507 <div>
7508 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
7509 {__("Discounted Price", "yatra")} (
7510 {__("Optional", "yatra")})
7511 </label>
7512 <div className="relative">
7513 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7514 {getCurrencySymbol(globalCurrency)}
7515 </span>
7516 <Input
7517 type="number"
7518 step="0.01"
7519 min="0"
7520 value={priceType.discounted_price}
7521 onChange={(e) =>
7522 handlePriceTypeChange(
7523 category.id,
7524 "discounted_price",
7525 e.target.value,
7526 )
7527 }
7528 placeholder="0.00"
7529 className={`pl-7 ${errors[`price_type_${index}_discounted`] ? "border-red-500" : ""}`}
7530 />
7531 </div>
7532 {errors[
7533 `price_type_${index}_discounted`
7534 ] && (
7535 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
7536 {
7537 errors[
7538 `price_type_${index}_discounted`
7539 ]
7540 }
7541 </p>
7542 )}
7543 </div>
7544 </div>
7545
7546 <div className="mt-3 flex items-center justify-between">
7547 <label className="inline-flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-300">
7548 <input
7549 type="checkbox"
7550 checked={Boolean(priceType.is_default)}
7551 onChange={(e) =>
7552 handlePriceTypeDefaultChange(
7553 category.id,
7554 e.target.checked,
7555 )
7556 }
7557 className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
7558 />
7559 {__(
7560 "Default price (used on page load)",
7561 "yatra",
7562 )}
7563 </label>
7564 <span className="text-[11px] text-gray-500 dark:text-gray-400">
7565 {__("Traveler-based pricing only", "yatra")}
7566 </span>
7567 </div>
7568 </div>
7569 );
7570 })}
7571 </div>
7572 )}
7573
7574 {/* Empty State */}
7575 {formData.price_types.length === 0 && (
7576 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
7577 <Tag className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
7578 <p className="text-sm text-gray-600 dark:text-gray-400">
7579 {__(
7580 "No pricing added yet. Select a category above to add pricing.",
7581 "yatra",
7582 )}
7583 </p>
7584 </div>
7585 )}
7586 </div>
7587 )}
7588 {errors.price_types && (
7589 <p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
7590 <AlertCircle className="w-4 h-4" />
7591 {errors.price_types}
7592 </p>
7593 )}
7594 </div>
7595 )}
7596
7597 {/*
7598 Deposit & Payment Terms — Pro feature (FlexiblePayments module).
7599 The DB columns live in the free TripsTable schema so existing data
7600 survives toggling Pro on/off. Editing is gated on
7601 window.yatraAdmin.flexiblePaymentsEnabled (set true by Pro's
7602 FlexiblePaymentsModule::addAdminData() on yatra_admin_localized_data).
7603
7604 We deliberately render the section in all three states (free / Pro
7605 installed-but-module-off / Pro+module-on) so the feature is
7606 discoverable in the free plugin. When unlocked → editable. When
7607 locked → fields shown disabled inside a <fieldset disabled>, with a
7608 PRO badge on the heading and a context-aware info banner above:
7609 - free: "Upgrade to Pro" link
7610 - Pro installed but module disabled: "Activate Module" link
7611 */}
7612 {(() => {
7613 const flexiblePaymentsEnabled = Boolean(
7614 (window as any)?.yatraAdmin?.flexiblePaymentsEnabled,
7615 );
7616 const isLocked = !flexiblePaymentsEnabled;
7617 const siteUrl =
7618 (window as any)?.yatraAdmin?.siteUrl ||
7619 "/wp-admin/admin.php?page=yatra";
7620 const modulesPageUrl = `${siteUrl}/wp-admin/admin.php?page=yatra&subpage=modules`;
7621 const pricingUrl = "https://mantrabrain.com/yatra-pricing/";
7622
7623 return (
7624 <div className="border-t border-gray-200 dark:border-gray-700 pt-4">
7625 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
7626 {__("Deposit & Payment Terms", "yatra")}
7627 {isLocked && <ProBadge isProActive={isPro} />}
7628 </h3>
7629
7630 {isLocked && (
7631 <div className="mb-3 p-3 bg-gradient-to-r from-purple-50 to-blue-50 dark:from-purple-900/20 dark:to-blue-900/20 border border-purple-200 dark:border-purple-800 rounded-md text-xs text-purple-900 dark:text-purple-100 flex items-start gap-2">
7632 <svg
7633 className="w-4 h-4 mt-0.5 text-purple-500 flex-shrink-0"
7634 fill="none"
7635 stroke="currentColor"
7636 viewBox="0 0 24 24"
7637 aria-hidden="true"
7638 >
7639 <path
7640 strokeLinecap="round"
7641 strokeLinejoin="round"
7642 strokeWidth={2}
7643 d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
7644 />
7645 </svg>
7646 <div className="flex-1">
7647 {isPro ? (
7648 <>
7649 {__(
7650 "Per-trip deposits need the Flexible Payments module.",
7651 "yatra",
7652 )}{" "}
7653 <a
7654 href={modulesPageUrl}
7655 target="_blank"
7656 rel="noopener noreferrer"
7657 className="underline font-medium"
7658 >
7659 {__("Activate module", "yatra")}
7660 </a>
7661 </>
7662 ) : (
7663 <>
7664 {__(
7665 "Per-trip deposit amount, percentage and custom payment terms are a Yatra Pro feature.",
7666 "yatra",
7667 )}{" "}
7668 <a
7669 href={pricingUrl}
7670 target="_blank"
7671 rel="noopener noreferrer"
7672 className="underline font-medium"
7673 >
7674 {__("Upgrade to Pro", "yatra")}
7675 </a>
7676 </>
7677 )}
7678 </div>
7679 </div>
7680 )}
7681
7682 {/*
7683 <fieldset disabled> is the HTML-standard way to make every
7684 form control inside it non-interactive in one go — works
7685 for our <Input> and <textarea> without per-field disabled props.
7686 */}
7687 <fieldset
7688 disabled={isLocked}
7689 className={
7690 isLocked
7691 ? "opacity-60 cursor-not-allowed select-none"
7692 : ""
7693 }
7694 aria-label={
7695 isLocked
7696 ? __(
7697 "Deposit & Payment Terms (Pro feature, disabled)",
7698 "yatra",
7699 )
7700 : undefined
7701 }
7702 >
7703 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7704 <div>
7705 <label
7706 htmlFor="deposit_amount"
7707 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7708 >
7709 {__("Deposit Amount", "yatra")}
7710 </label>
7711 <div className="relative">
7712 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
7713 {getCurrencySymbol(globalCurrency)}
7714 </span>
7715 <Input
7716 id="deposit_amount"
7717 type="number"
7718 step="0.01"
7719 min="0"
7720 value={formData.deposit_amount}
7721 onChange={(e) =>
7722 handleFieldChange(
7723 "deposit_amount",
7724 e.target.value,
7725 )
7726 }
7727 placeholder="0.00"
7728 className="pl-7"
7729 />
7730 </div>
7731 </div>
7732 <div>
7733 <label
7734 htmlFor="deposit_percentage"
7735 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7736 >
7737 {__("Deposit Percentage", "yatra")}
7738 </label>
7739 <div className="relative">
7740 <Input
7741 id="deposit_percentage"
7742 type="number"
7743 step="0.01"
7744 min="0"
7745 max="100"
7746 value={formData.deposit_percentage}
7747 onChange={(e) =>
7748 handleFieldChange(
7749 "deposit_percentage",
7750 e.target.value,
7751 )
7752 }
7753 placeholder="0"
7754 className="pr-7"
7755 />
7756 <span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500">
7757 %
7758 </span>
7759 </div>
7760 </div>
7761 </div>
7762 <div className="mt-4">
7763 <label
7764 htmlFor="payment_terms"
7765 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7766 >
7767 {__("Payment Terms", "yatra")}
7768 </label>
7769 <textarea
7770 id="payment_terms"
7771 value={formData.payment_terms}
7772 onChange={(e) =>
7773 handleFieldChange("payment_terms", e.target.value)
7774 }
7775 placeholder={__(
7776 "e.g., 50% deposit required at booking, balance due 30 days before departure",
7777 "yatra",
7778 )}
7779 rows={2}
7780 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
7781 />
7782 </div>
7783 </fieldset>
7784 </div>
7785 );
7786 })()}
7787
7788 {/* Group Pricing */}
7789 {/* Removed */}
7790 </div>
7791 </div>
7792 );
7793
7794 case "attributes":
7795 // Redirect to categorization section (attributes now merged there)
7796 setCurrentSection("categorization");
7797 return null;
7798
7799 case "itinerary":
7800 return (
7801 <div className="space-y-6">
7802 {/* Header */}
7803 <div className="flex flex-wrap items-center gap-2 mb-2">
7804 <BookOpen className="w-5 h-5 text-gray-500" />
7805 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7806 {__("Trip Details", "yatra")}
7807 </h2>
7808 <Badge
7809 variant="outline"
7810 className="text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-800"
7811 >
7812 {__("Recommended", "yatra")}
7813 </Badge>
7814 </div>
7815 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7816 {__("What will travelers experience?", "yatra")}
7817 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7818 {__(
7819 "Build your day-by-day itinerary and list what's included/excluded to set clear expectations.",
7820 "yatra",
7821 )}
7822 </span>
7823 </p>
7824
7825 {/* Sub-tabs */}
7826 <div className="border-b border-gray-200 dark:border-gray-700">
7827 <nav className="flex gap-6" aria-label="Trip Details Tabs">
7828 <button
7829 onClick={() => setTripDetailsTab("itinerary")}
7830 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
7831 tripDetailsTab === "itinerary"
7832 ? "border-blue-500 text-blue-600 dark:text-blue-400"
7833 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
7834 }`}
7835 >
7836 <div className="flex items-center gap-2">
7837 <Calendar className="w-4 h-4" />
7838 {__("Itinerary", "yatra")}
7839 </div>
7840 </button>
7841 <button
7842 onClick={() => setTripDetailsTab("included")}
7843 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
7844 tripDetailsTab === "included"
7845 ? "border-blue-500 text-blue-600 dark:text-blue-400"
7846 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
7847 }`}
7848 >
7849 <div className="flex items-center gap-2">
7850 <CheckSquare className="w-4 h-4" />
7851 {__("Included/Excluded", "yatra")}
7852 </div>
7853 </button>
7854 </nav>
7855 </div>
7856
7857 {/* Tab Content */}
7858 <div className="mt-6">
7859 {tripDetailsTab === "itinerary" && (
7860 <ItinerarySection
7861 formData={formData as any}
7862 isEditMode={isEditMode}
7863 tripId={tripId}
7864 />
7865 )}
7866 {tripDetailsTab === "included" && (
7867 <IncludedSection
7868 formData={formData}
7869 errors={errors}
7870 handleFieldChange={handleFieldChange as any}
7871 />
7872 )}
7873 </div>
7874 </div>
7875 );
7876
7877 case "included":
7878 // Redirect to itinerary with included tab selected
7879 setCurrentSection("itinerary");
7880 setTripDetailsTab("included");
7881 return null;
7882
7883 case "faqs":
7884 // Redirect to seo section (FAQs now merged there)
7885 setCurrentSection("seo");
7886 return null;
7887
7888 // Removed: case 'frontend-tabs' - now part of 'advanced' case
7889
7890 case "seo":
7891 return (
7892 <div className="space-y-6">
7893 <div className="flex flex-wrap items-center gap-2 mb-2">
7894 <Search className="w-5 h-5 text-gray-500" />
7895 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7896 {__("SEO & Marketing", "yatra")}
7897 </h2>
7898 <Badge
7899 variant="outline"
7900 className="text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-300 border-amber-200 dark:border-amber-800"
7901 >
7902 {__("Recommended", "yatra")}
7903 </Badge>
7904 </div>
7905 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7906 {__("How will people find & trust this trip?", "yatra")}
7907 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7908 {__(
7909 "Optimize for search engines and answer common questions to build trust and improve discoverability.",
7910 "yatra",
7911 )}
7912 </span>
7913 </p>
7914
7915 <div className="space-y-6">
7916 {/* SECTION 1: SEO Settings */}
7917 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
7918 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
7919 <Search className="w-4 h-4 text-blue-500" />
7920 {__("Search Engine Optimization", "yatra")}
7921 </h3>
7922 <div className="space-y-4">
7923 <div>
7924 <label
7925 htmlFor="meta_title"
7926 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7927 >
7928 {__("Meta Title", "yatra")}
7929 </label>
7930 <Input
7931 id="meta_title"
7932 type="text"
7933 value={formData.meta_title}
7934 onChange={(e) =>
7935 handleFieldChange("meta_title", e.target.value)
7936 }
7937 placeholder={formData.title || __("Trip Title", "yatra")}
7938 maxLength={60}
7939 />
7940 <HelpText
7941 text={__(
7942 "Best for search engines: 50-60 characters. Include main keywords.",
7943 "yatra",
7944 )}
7945 className="mt-1"
7946 />
7947 </div>
7948 <div>
7949 <label
7950 htmlFor="meta_description"
7951 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7952 >
7953 {__("Meta Description", "yatra")}
7954 </label>
7955 <textarea
7956 id="meta_description"
7957 value={formData.meta_description}
7958 onChange={(e) =>
7959 handleFieldChange("meta_description", e.target.value)
7960 }
7961 rows={3}
7962 maxLength={160}
7963 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
7964 placeholder={__(
7965 "Compelling description that includes key travel terms...",
7966 "yatra",
7967 )}
7968 />
7969 <HelpText
7970 text={__(
7971 "160 characters max. Include location, duration, and key features.",
7972 "yatra",
7973 )}
7974 className="mt-1"
7975 />
7976 </div>
7977 <div>
7978 <label
7979 htmlFor="meta_keywords"
7980 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7981 >
7982 {__("Meta Keywords", "yatra")}
7983 </label>
7984 <Input
7985 id="meta_keywords"
7986 type="text"
7987 value={formData.meta_keywords}
7988 onChange={(e) =>
7989 handleFieldChange("meta_keywords", e.target.value)
7990 }
7991 placeholder={__(
7992 "adventure, travel, tour, guide, experience",
7993 "yatra",
7994 )}
7995 />
7996 <HelpText
7997 text={__(
7998 "Comma-separated keywords. Include location, activities, and travel terms.",
7999 "yatra",
8000 )}
8001 className="mt-1"
8002 />
8003 </div>
8004 </div>
8005
8006 {/* SEO Preview */}
8007 <Card>
8008 <CardHeader>
8009 <CardTitle>{__("SEO Preview", "yatra")}</CardTitle>
8010 <CardDescription>
8011 {__(
8012 "Preview how your trip appears in search results",
8013 "yatra",
8014 )}
8015 </CardDescription>
8016 </CardHeader>
8017 <CardContent className="space-y-4">
8018 <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
8019 <div className="text-xs text-gray-500 dark:text-gray-400 mb-2">
8020 {__("Google Search Result Preview", "yatra")}
8021 </div>
8022 <div className="space-y-2">
8023 <div className="text-blue-800 dark:text-blue-400 text-sm font-medium">
8024 {sanitizeTextForSEO(
8025 formData.meta_title ||
8026 formData.title ||
8027 "Trip Title",
8028 60,
8029 )}
8030 </div>
8031 <div className="text-green-700 dark:text-green-400 text-sm">
8032 {formData.meta_description
8033 ? sanitizeTextForSEO(formData.meta_description, 150)
8034 : formData.description
8035 ? sanitizeTextForSEO(formData.description, 150)
8036 : "Trip description..."}
8037 </div>
8038 <div className="text-gray-600 dark:text-gray-400 text-xs">
8039 {window.location.hostname}/trip/
8040 {formData.slug || "trip-slug"}
8041 </div>
8042 </div>
8043 </div>
8044 </CardContent>
8045 </Card>
8046 </div>
8047
8048 {/* SECTION 2: FAQs */}
8049 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
8050 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
8051 <HelpCircle className="w-4 h-4 text-blue-500" />
8052 {__("Frequently Asked Questions", "yatra")}
8053 </h3>
8054 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8055 {__(
8056 "Answer common questions to build trust and reduce support inquiries. FAQs also help with SEO.",
8057 "yatra",
8058 )}
8059 </p>
8060 <div className="space-y-4">
8061 {formData.faqs.map((faq, index) => (
8062 <div
8063 key={index}
8064 className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-900"
8065 >
8066 <div className="flex items-start justify-between mb-3">
8067 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
8068 {__("FAQ", "yatra")} {index + 1}
8069 </h4>
8070 <button
8071 type="button"
8072 onClick={() => handleFAQRemove(index)}
8073 className="text-red-600 hover:text-red-700"
8074 >
8075 <X className="w-4 h-4" />
8076 </button>
8077 </div>
8078 <div className="space-y-3">
8079 <div>
8080 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8081 {__("Question", "yatra")}
8082 </label>
8083 <Input
8084 type="text"
8085 value={faq.question}
8086 onChange={(e) =>
8087 handleFAQChange(index, "question", e.target.value)
8088 }
8089 placeholder={__("Enter question...", "yatra")}
8090 />
8091 </div>
8092 <div>
8093 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8094 {__("Answer", "yatra")}
8095 </label>
8096 <textarea
8097 value={faq.answer}
8098 onChange={(e) =>
8099 handleFAQChange(index, "answer", e.target.value)
8100 }
8101 placeholder={__("Enter answer...", "yatra")}
8102 rows={3}
8103 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
8104 />
8105 </div>
8106 </div>
8107 </div>
8108 ))}
8109 <Button
8110 type="button"
8111 variant="outline"
8112 onClick={handleFAQAdd}
8113 className="w-full"
8114 >
8115 <Plus className="w-4 h-4 mr-2" />
8116 {__("Add FAQ", "yatra")}
8117 </Button>
8118 </div>
8119 </div>
8120 </div>
8121 </div>
8122 );
8123
8124 case "advanced":
8125 return (
8126 <div className="space-y-6">
8127 <div className="flex flex-wrap items-center gap-2 mb-2">
8128 <Settings className="w-5 h-5 text-gray-500" />
8129 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
8130 {__("Lifecycle Management", "yatra")}
8131 </h2>
8132 <Badge
8133 variant="outline"
8134 className="text-xs bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-300 border-purple-200 dark:border-purple-800"
8135 >
8136 {__("Power Users", "yatra")}
8137 </Badge>
8138 </div>
8139 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
8140 {__("When should this trip be published or archived?", "yatra")}
8141 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
8142 {__(
8143 "Schedule automatic publishing and unpublishing for seasonal trips. Optional but useful for managing trip lifecycles.",
8144 "yatra",
8145 )}
8146 </span>
8147 </p>
8148
8149 <Card>
8150 <CardHeader>
8151 <CardTitle>{__("Version Control", "yatra")}</CardTitle>
8152 <CardDescription>
8153 {__("Track changes and version history", "yatra")}
8154 </CardDescription>
8155 </CardHeader>
8156 <CardContent className="space-y-4">
8157 <div>
8158 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8159 {__("Version", "yatra")}
8160 </label>
8161 <Input
8162 type="number"
8163 value={formData.version}
8164 onChange={(e) =>
8165 handleFieldChange(
8166 "version",
8167 parseInt(e.target.value) || 1,
8168 )
8169 }
8170 min="1"
8171 readOnly
8172 className="bg-gray-50 dark:bg-gray-800"
8173 />
8174 <HelpText
8175 text={__(
8176 "Version number is automatically incremented when changes are saved",
8177 "yatra",
8178 )}
8179 className="mt-2"
8180 />
8181 </div>
8182 </CardContent>
8183 </Card>
8184
8185 <Card>
8186 <CardHeader>
8187 <CardTitle>{__("Scheduled Publishing", "yatra")}</CardTitle>
8188 <CardDescription>
8189 {__(
8190 "Automatically publish or unpublish your trip on specific dates",
8191 "yatra",
8192 )}
8193 </CardDescription>
8194 </CardHeader>
8195 <CardContent className="space-y-4">
8196 <div>
8197 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8198 {__("Schedule Publish Date", "yatra")}
8199 </label>
8200 <DatePicker
8201 value={formData.scheduled_publish_date}
8202 onChange={(val) =>
8203 handleFieldChange("scheduled_publish_date", val)
8204 }
8205 placeholder={__("Select date", "yatra")}
8206 />
8207 <HelpText
8208 text={__(
8209 "Trip will be automatically published on this date and time",
8210 "yatra",
8211 )}
8212 className="mt-2"
8213 />
8214 </div>
8215 <div>
8216 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8217 {__("Schedule Unpublish Date", "yatra")}
8218 </label>
8219 <DatePicker
8220 value={formData.scheduled_unpublish_date}
8221 onChange={(val) =>
8222 handleFieldChange("scheduled_unpublish_date", val)
8223 }
8224 placeholder={__("Select date", "yatra")}
8225 />
8226 <HelpText
8227 text={__(
8228 "Trip will be automatically unpublished (archived) on this date and time",
8229 "yatra",
8230 )}
8231 className="mt-2"
8232 />
8233 </div>
8234 </CardContent>
8235 </Card>
8236
8237 <Card>
8238 <CardHeader>
8239 <CardTitle>
8240 {__("Seasonal Auto-Enable/Disable", "yatra")}
8241 </CardTitle>
8242 <CardDescription>
8243 {__(
8244 "Automatically enable or disable trip availability based on seasonal dates",
8245 "yatra",
8246 )}
8247 </CardDescription>
8248 </CardHeader>
8249 <CardContent className="space-y-4">
8250 <div className="flex items-center gap-2">
8251 <input
8252 type="checkbox"
8253 id="seasonal_auto_enable"
8254 checked={formData.seasonal_auto_enable}
8255 onChange={(e) =>
8256 handleFieldChange(
8257 "seasonal_auto_enable",
8258 e.target.checked,
8259 )
8260 }
8261 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
8262 />
8263 <label
8264 htmlFor="seasonal_auto_enable"
8265 className="text-sm font-medium text-gray-700 dark:text-gray-300"
8266 >
8267 {__("Enable seasonal auto-management", "yatra")}
8268 </label>
8269 </div>
8270 {formData.seasonal_auto_enable && (
8271 <>
8272 <div>
8273 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8274 {__("Auto-Enable Date", "yatra")}
8275 </label>
8276 <DatePicker
8277 value={formData.seasonal_enable_date}
8278 onChange={(val) =>
8279 handleFieldChange("seasonal_enable_date", val)
8280 }
8281 placeholder={__("Select date", "yatra")}
8282 />
8283 <HelpText
8284 text={__(
8285 "Trip will become available for booking on this date",
8286 "yatra",
8287 )}
8288 className="mt-2"
8289 />
8290 </div>
8291 <div>
8292 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8293 {__("Auto-Disable Date", "yatra")}
8294 </label>
8295 <DatePicker
8296 value={formData.seasonal_disable_date}
8297 onChange={(val) =>
8298 handleFieldChange("seasonal_disable_date", val)
8299 }
8300 placeholder={__("Select date", "yatra")}
8301 />
8302 <HelpText
8303 text={__(
8304 "Trip will become unavailable for booking on this date",
8305 "yatra",
8306 )}
8307 className="mt-2"
8308 />
8309 </div>
8310 </>
8311 )}
8312 </CardContent>
8313 </Card>
8314
8315 {/* Frontend Tabs Management */}
8316 <Card>
8317 <CardHeader>
8318 <CardTitle>{__("Frontend Tabs Management", "yatra")}</CardTitle>
8319 <CardDescription>
8320 {__(
8321 "Manage which tabs appear on the trip single page and in what order. Enable or disable tabs, customize labels, and reorder them.",
8322 "yatra",
8323 )}
8324 </CardDescription>
8325 </CardHeader>
8326 <CardContent>
8327 <div className="space-y-3">
8328 {[...formData.frontend_tabs]
8329 .sort((a, b) => a.order - b.order)
8330 .map((tab, index) => (
8331 <div
8332 key={tab.id}
8333 draggable
8334 onDragStart={(e) => handleDragStart(e, tab.id)}
8335 onDragOver={handleDragOver}
8336 onDragEnter={() => handleDragEnter(tab.id)}
8337 onDragLeave={handleDragLeave}
8338 onDrop={(e) => handleDrop(e, tab.id)}
8339 onDragEnd={handleDragEnd}
8340 className={`p-4 rounded-lg border cursor-move relative ${
8341 tab.enabled
8342 ? "bg-gray-50 dark:bg-gray-850 border-gray-200 dark:border-gray-700 shadow-sm"
8343 : "bg-gray-100 dark:bg-gray-900 border-gray-200 dark:border-gray-700 opacity-60 shadow-sm"
8344 } ${
8345 draggedTab === tab.id
8346 ? "opacity-50 scale-95 shadow-lg"
8347 : ""
8348 } ${
8349 dragOverTab === tab.id && draggedTab !== tab.id
8350 ? "border-blue-400 bg-blue-50 dark:bg-blue-900/20"
8351 : ""
8352 }`}
8353 >
8354 {tab.content_type === "custom" && (
8355 <Button
8356 type="button"
8357 variant="ghost"
8358 size="sm"
8359 onClick={() => handleTabRemove(tab.id)}
8360 className="absolute top-2 right-2 h-10 w-10 p-0 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
8361 title={__("Delete tab", "yatra")}
8362 >
8363 <Trash2 className="w-5 h-5" />
8364 </Button>
8365 )}
8366 <div className="flex items-center gap-3 mb-3">
8367 <GripVertical className="w-6 h-6 text-gray-400 cursor-grab active:cursor-grabbing" />
8368 <div className="flex-1">
8369 <div className="flex items-center gap-2 mb-2">
8370 <Input
8371 type="text"
8372 value={tab.label}
8373 onChange={(e) =>
8374 handleTabLabelChange(tab.id, e.target.value)
8375 }
8376 className="text-sm font-medium flex-1"
8377 disabled={!tab.enabled}
8378 placeholder={__("Tab Label", "yatra")}
8379 />
8380 <Badge variant="outline" className="text-xs">
8381 {tab.content_type}
8382 </Badge>
8383 </div>
8384 <div className="mb-2">
8385 {tab.enabled ? (
8386 <IconPicker
8387 value={tab.icon || null}
8388 onChange={(value) =>
8389 handleTabIconChange(tab.id, value)
8390 }
8391 label={__("Tab Icon", "yatra")}
8392 helpText={__(
8393 "Select an icon or upload an image for this tab",
8394 "yatra",
8395 )}
8396 size="sm"
8397 />
8398 ) : (
8399 <div className="opacity-50">
8400 <IconPicker
8401 value={tab.icon || null}
8402 onChange={() => {}} // No-op when disabled
8403 label={__("Tab Icon", "yatra")}
8404 helpText={__(
8405 "Enable this tab to select an icon",
8406 "yatra",
8407 )}
8408 size="sm"
8409 />
8410 </div>
8411 )}
8412 </div>
8413 {tab.content_type === "custom" && (
8414 <div className="mt-2">
8415 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
8416 {__("Custom Content", "yatra")}
8417 </label>
8418 <textarea
8419 value={tab.custom_content || ""}
8420 onChange={(e) =>
8421 handleTabCustomContentChange(
8422 tab.id,
8423 e.target.value,
8424 )
8425 }
8426 rows={3}
8427 disabled={!tab.enabled}
8428 placeholder={__(
8429 "Enter custom content for this tab...",
8430 "yatra",
8431 )}
8432 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white 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"
8433 />
8434 </div>
8435 )}
8436 {tab.content_type !== "custom" && (
8437 <div className="mt-2">
8438 <Select
8439 value={tab.content_type}
8440 onChange={(e) =>
8441 handleTabContentTypeChange(
8442 tab.id,
8443 e.target
8444 .value as FrontendTab["content_type"],
8445 )
8446 }
8447 disabled={!tab.enabled}
8448 className="text-xs"
8449 >
8450 <option value="overview">
8451 {__("Overview", "yatra")}
8452 </option>
8453 <option value="itinerary">
8454 {__("Itinerary", "yatra")}
8455 </option>
8456 <option value="included_excluded">
8457 {__("Included", "yatra")}
8458 </option>
8459 <option value="location">
8460 {__("Location", "yatra")}
8461 </option>
8462 <option value="important_info">
8463 {__("Important Info", "yatra")}
8464 </option>
8465 <option value="downloads">
8466 {__("Downloads", "yatra")}
8467 </option>
8468 <option value="faq">
8469 {__("FAQ", "yatra")}
8470 </option>
8471 <option value="trip_story">
8472 {__("Story", "yatra")}
8473 </option>
8474 <option value="what_makes_special">
8475 {__("Special", "yatra")}
8476 </option>
8477 <option value="testimonials">
8478 {__("Testimonials", "yatra")}
8479 </option>
8480 <option value="custom">
8481 {__("Custom Content", "yatra")}
8482 </option>
8483 </Select>
8484 </div>
8485 )}
8486 </div>
8487 <div className="flex items-center gap-2">
8488 <Button
8489 type="button"
8490 variant="ghost"
8491 size="sm"
8492 onClick={() => handleTabMove(tab.id, "up")}
8493 disabled={index === 0}
8494 className="h-10 w-10 p-0"
8495 title={__("Move up", "yatra")}
8496 >
8497 <ChevronUp className="w-5 h-5" />
8498 </Button>
8499 <Button
8500 type="button"
8501 variant="ghost"
8502 size="sm"
8503 onClick={() => handleTabMove(tab.id, "down")}
8504 disabled={
8505 index === formData.frontend_tabs.length - 1
8506 }
8507 className="h-10 w-10 p-0"
8508 title={__("Move down", "yatra")}
8509 >
8510 <ChevronDown className="w-5 h-5" />
8511 </Button>
8512 <label className="flex items-center gap-2 cursor-pointer">
8513 <input
8514 type="checkbox"
8515 checked={tab.enabled}
8516 onChange={() => handleTabToggle(tab.id)}
8517 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
8518 />
8519 <span className="text-sm text-gray-700 dark:text-gray-300">
8520 {tab.enabled
8521 ? __("Enabled", "yatra")
8522 : __("Disabled", "yatra")}
8523 </span>
8524 </label>
8525 </div>
8526 </div>
8527 </div>
8528 ))}
8529 </div>
8530 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
8531 <Button
8532 type="button"
8533 variant="outline"
8534 onClick={handleTabAdd}
8535 className="w-full"
8536 >
8537 <Plus className="w-4 h-4 mr-2" />
8538 {__("Add Custom Tab", "yatra")}
8539 </Button>
8540 </div>
8541 </CardContent>
8542 </Card>
8543 </div>
8544 );
8545
8546 default:
8547 return (
8548 <div className="text-center py-8 text-gray-500">
8549 {__("Section content coming soon...", "yatra")}
8550 </div>
8551 );
8552 }
8553 };
8554
8555 return (
8556 <div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden">
8557 {/* Header */}
8558 <div className="bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 flex items-center justify-between">
8559 {/* Left Side - Title, Status, Saved Time */}
8560 <div className="flex items-center gap-4 min-w-0 flex-1">
8561 <div className="min-w-0">
8562 <h1 className="text-base font-semibold text-gray-900 dark:text-white truncate">
8563 {formData.title || __("New Trip", "yatra")}
8564 </h1>
8565 <div className="flex items-center gap-2 mt-1">
8566 <Badge
8567 variant="outline"
8568 className={`bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 ${
8569 formData.status === "publish"
8570 ? "text-green-700 dark:text-green-400 border-green-300 dark:border-green-800"
8571 : formData.status === "review"
8572 ? "text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-800"
8573 : formData.status === "approved"
8574 ? "text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-800"
8575 : formData.status === "suspended"
8576 ? "text-orange-700 dark:text-orange-400 border-orange-300 dark:border-orange-800"
8577 : formData.status === "archived"
8578 ? "text-gray-700 dark:text-gray-400 border-gray-300 dark:border-gray-600"
8579 : "text-gray-700 dark:text-gray-300"
8580 }`}
8581 >
8582 {formData.status === "draft"
8583 ? __("Draft", "yatra")
8584 : formData.status === "review"
8585 ? __("Review", "yatra")
8586 : formData.status === "approved"
8587 ? __("Approved", "yatra")
8588 : formData.status === "publish"
8589 ? __("Published", "yatra")
8590 : formData.status === "suspended"
8591 ? __("Suspended", "yatra")
8592 : __("Archived", "yatra")}
8593 </Badge>
8594 </div>
8595 </div>
8596
8597 {/* Quick Start Mode Toggle */}
8598 {!isEditMode && (
8599 <div className="flex items-center gap-2 px-2 py-1 rounded-md bg-gray-50 dark:bg-gray-700/50 flex-shrink-0">
8600 <button
8601 onClick={() => setSimpleMode(!simpleMode)}
8602 className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
8603 title={
8604 simpleMode
8605 ? __("Switch to Advanced Mode", "yatra")
8606 : __("Switch to Simple Mode", "yatra")
8607 }
8608 >
8609 <Lightbulb
8610 className={`w-3.5 h-3.5 ${simpleMode ? "text-yellow-500" : ""}`}
8611 />
8612 <span>
8613 {simpleMode ? __("Simple", "yatra") : __("Advanced", "yatra")}
8614 </span>
8615 </button>
8616 </div>
8617 )}
8618
8619 {/* Guided Tour Button (First Time Users) */}
8620 {!isEditMode && (
8621 <Button
8622 variant="ghost"
8623 size="sm"
8624 onClick={() => {
8625 showToast(
8626 __("Guided tour feature coming soon!", "yatra"),
8627 "info",
8628 );
8629 }}
8630 className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0"
8631 >
8632 <BookOpen className="w-3.5 h-3.5 mr-1" />
8633 {__("Take a Tour", "yatra")}
8634 </Button>
8635 )}
8636
8637 {/* Dummy Data Button */}
8638 {!isEditMode && (
8639 <Button
8640 variant="ghost"
8641 size="sm"
8642 onClick={handleFillDummyData}
8643 className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0"
8644 title={__("Fill form with dummy data", "yatra")}
8645 >
8646 <Database className="w-3.5 h-3.5 mr-1" />
8647 {__("Fill Dummy Data", "yatra")} ({dummyDataIndex + 1}/
8648 {dummyTripsData.length})
8649 </Button>
8650 )}
8651 </div>
8652
8653 {/* Right Side - Action Buttons */}
8654 <div className="flex items-center gap-2 flex-shrink-0">
8655 {isEditMode && (
8656 <Button
8657 variant="outline"
8658 onClick={() => setShowRevisionsDialog(true)}
8659 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8660 >
8661 <History className="w-4 h-4 mr-2" />
8662 {__("Revisions", "yatra")}
8663 </Button>
8664 )}
8665 <Button
8666 variant="outline"
8667 onClick={handlePreview}
8668 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8669 disabled={!formData.slug}
8670 >
8671 <Eye className="w-4 h-4 mr-2" />
8672 {__("Preview", "yatra")}
8673 </Button>
8674 <Button
8675 variant="outline"
8676 onClick={handleSaveDraft}
8677 disabled={isSubmitting}
8678 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8679 >
8680 <Save className="w-4 h-4 mr-2" />
8681 {isEditMode
8682 ? __("Update Draft", "yatra")
8683 : __("Save Draft", "yatra")}
8684 </Button>
8685 <div className="relative group">
8686 <Button
8687 onClick={handlePublish}
8688 disabled={isSubmitting}
8689 className="bg-blue-600 hover:bg-blue-700 text-white border-0 relative"
8690 >
8691 {isSubmitting ? (
8692 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
8693 ) : (
8694 <>
8695 <Sparkles className="w-4 h-4 mr-2" />
8696 {isEditMode
8697 ? __("Update Trip", "yatra")
8698 : __("Publish Trip", "yatra")}
8699 <ChevronDown className="w-4 h-4 ml-2" />
8700 </>
8701 )}
8702 </Button>
8703 {!isSubmitting && (
8704 <div className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
8705 <div className="py-1">
8706 <button
8707 type="button"
8708 onClick={() => {
8709 handleFieldChange("status", "draft");
8710 handleSaveDraft();
8711 }}
8712 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8713 >
8714 <span>{__("Save as Draft", "yatra")}</span>
8715 </button>
8716 <button
8717 type="button"
8718 onClick={() => {
8719 handleFieldChange("status", "review");
8720 handlePublish();
8721 }}
8722 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8723 >
8724 <span>{__("Save for Review", "yatra")}</span>
8725 </button>
8726 <button
8727 type="button"
8728 onClick={() => {
8729 handleFieldChange("status", "approved");
8730 handlePublish();
8731 }}
8732 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8733 >
8734 <span>{__("Mark as Approved", "yatra")}</span>
8735 </button>
8736 <button
8737 type="button"
8738 onClick={handlePublish}
8739 className="w-full text-left px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 font-medium flex items-center gap-2"
8740 >
8741 <Sparkles className="w-4 h-4" />
8742 <span>
8743 {isEditMode
8744 ? __("Update & Publish", "yatra")
8745 : __("Publish", "yatra")}
8746 </span>
8747 </button>
8748 <button
8749 type="button"
8750 onClick={() => {
8751 handleFieldChange("status", "suspended");
8752 handlePublish();
8753 }}
8754 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8755 >
8756 <span>{__("Suspend", "yatra")}</span>
8757 </button>
8758 <button
8759 type="button"
8760 onClick={() => {
8761 handleFieldChange("status", "archived");
8762 handlePublish();
8763 }}
8764 className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
8765 >
8766 <span>{__("Archive", "yatra")}</span>
8767 </button>
8768 </div>
8769 </div>
8770 )}
8771 </div>
8772 </div>
8773 </div>
8774
8775 {/* Navigation Bar */}
8776 <div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
8777 <div className="flex items-center justify-between gap-4">
8778 {/* Previous Button */}
8779 <Button
8780 variant="outline"
8781 size="default"
8782 onClick={goToPreviousSection}
8783 disabled={currentStepIndex === 0}
8784 className="flex items-center gap-2 text-sm font-semibold border-2 border-blue-400 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:border-blue-500 dark:hover:border-blue-500 text-blue-700 dark:text-blue-300 shadow-sm disabled:opacity-40 disabled:cursor-not-allowed flex-shrink-0 px-4 py-2"
8785 >
8786 <ChevronLeft className="w-4 h-4" />
8787 {__("Previous", "yatra")}
8788 </Button>
8789
8790 {/* Next Button */}
8791 <Button
8792 variant="outline"
8793 size="default"
8794 onClick={goToNextSection}
8795 disabled={currentStepIndex >= allSections.length - 1}
8796 className="flex items-center gap-2 text-sm font-semibold border-2 border-blue-400 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:border-blue-500 dark:hover:border-blue-500 text-blue-700 dark:text-blue-300 shadow-sm disabled:opacity-40 disabled:cursor-not-allowed flex-shrink-0 px-4 py-2"
8797 >
8798 {__("Next", "yatra")}
8799 <ChevronRight className="w-4 h-4" />
8800 </Button>
8801 </div>
8802 </div>
8803
8804 <div className="flex flex-1 min-h-0 p-0">
8805 {/* Sidebar */}
8806 <div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 overflow-y-auto overflow-x-hidden flex-shrink-0 min-h-0">
8807 <div className="p-5 pb-8 space-y-6">
8808 {/* PHASE 1: ESSENTIALS */}
8809 <div>
8810 <div className="flex items-center gap-2 mb-3 px-1">
8811 <div className="flex items-center gap-2 flex-1">
8812 <Box className="w-3.5 h-3.5 text-red-500 dark:text-red-400 flex-shrink-0" />
8813 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8814 {__("PHASE 1: ESSENTIALS", "yatra")}
8815 </h3>
8816 </div>
8817 <Badge
8818 variant="outline"
8819 className="text-[9px] px-1.5 py-0 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800"
8820 >
8821 {__("Must Complete", "yatra")}
8822 </Badge>
8823 </div>
8824 <div className="space-y-0.5">
8825 {essentialsSections.map((section, index) => {
8826 const Icon = section.icon;
8827 const isActive = currentSection === section.id;
8828 const isNext =
8829 !section.completed &&
8830 essentialsSections
8831 .slice(0, index)
8832 .every((s) => s.completed);
8833 return (
8834 <button
8835 key={section.id}
8836 type="button"
8837 onClick={() => setCurrentSection(section.id)}
8838 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border relative ${
8839 isActive
8840 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
8841 : isNext
8842 ? "border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-900/10 text-gray-700 dark:text-gray-300 hover:bg-amber-50 dark:hover:bg-amber-900/20"
8843 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
8844 }`}
8845 >
8846 <div className="flex items-center gap-2 flex-shrink-0">
8847 <span
8848 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
8849 isActive
8850 ? "bg-blue-600 text-white dark:bg-blue-500"
8851 : isNext
8852 ? "bg-amber-500 text-white"
8853 : section.completed
8854 ? "bg-green-500 text-white"
8855 : section.hasErrors
8856 ? "bg-red-500 text-white"
8857 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
8858 }`}
8859 >
8860 {index + 1}
8861 </span>
8862 <Icon
8863 className={`w-4 h-4 transition-colors ${
8864 isActive
8865 ? "text-blue-600 dark:text-blue-400"
8866 : isNext
8867 ? "text-amber-600 dark:text-amber-400"
8868 : section.hasErrors
8869 ? "text-red-600 dark:text-red-400"
8870 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
8871 }`}
8872 />
8873 </div>
8874 <span className="flex-1 min-w-0 break-words leading-snug">
8875 {section.label}
8876 {section.required && (
8877 <span className="ml-1 text-[9px] text-red-500">
8878 *
8879 </span>
8880 )}
8881 </span>
8882 {section.hasErrors && (
8883 <span
8884 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
8885 title={__("This section has errors", "yatra")}
8886 />
8887 )}
8888 </button>
8889 );
8890 })}
8891 </div>
8892 </div>
8893
8894 {/* PHASE 2: DETAILS */}
8895 <div>
8896 <div className="flex items-center gap-2 mb-3 px-1">
8897 <div className="flex items-center gap-2 flex-1">
8898 <BookOpen className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400 flex-shrink-0" />
8899 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8900 {__("PHASE 2: DETAILS", "yatra")}
8901 </h3>
8902 </div>
8903 <Badge
8904 variant="outline"
8905 className="text-[9px] px-1.5 py-0 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800"
8906 >
8907 {__("Recommended", "yatra")}
8908 </Badge>
8909 </div>
8910 <div className="space-y-0.5">
8911 {detailsSections.map((section, index) => {
8912 const Icon = section.icon;
8913 const isActive = currentSection === section.id;
8914 const sectionNumber = essentialsSections.length + index + 1;
8915 return (
8916 <button
8917 key={section.id}
8918 type="button"
8919 onClick={() => setCurrentSection(section.id)}
8920 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border ${
8921 isActive
8922 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
8923 : section.hasErrors
8924 ? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10"
8925 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
8926 }`}
8927 >
8928 <div className="flex items-center gap-2 flex-shrink-0">
8929 <span
8930 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
8931 isActive
8932 ? "bg-blue-600 text-white dark:bg-blue-500"
8933 : section.completed
8934 ? "bg-green-500 text-white"
8935 : section.hasErrors
8936 ? "bg-red-500 text-white"
8937 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
8938 }`}
8939 >
8940 {sectionNumber}
8941 </span>
8942 <Icon
8943 className={`w-4 h-4 transition-colors ${
8944 isActive
8945 ? "text-blue-600 dark:text-blue-400"
8946 : section.hasErrors
8947 ? "text-red-600 dark:text-red-400"
8948 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
8949 }`}
8950 />
8951 </div>
8952 <span className="flex-1 min-w-0 break-words leading-snug">
8953 {section.label}
8954 </span>
8955 {section.hasErrors && (
8956 <span
8957 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
8958 title={__("This section has errors", "yatra")}
8959 />
8960 )}
8961 </button>
8962 );
8963 })}
8964 </div>
8965 </div>
8966
8967 {/* PHASE 3: OPTIMIZATION */}
8968 <div>
8969 <div className="flex items-center gap-2 mb-3 px-1">
8970 <div className="flex items-center gap-2 flex-1">
8971 <BarChart3 className="w-3.5 h-3.5 text-purple-500 dark:text-purple-400 flex-shrink-0" />
8972 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
8973 {__("PHASE 3: OPTIMIZATION", "yatra")}
8974 </h3>
8975 </div>
8976 <Badge
8977 variant="outline"
8978 className="text-[9px] px-1.5 py-0 bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800"
8979 >
8980 {__("Optional", "yatra")}
8981 </Badge>
8982 </div>
8983 <div className="space-y-0.5">
8984 {optimizationSections.map((section, index) => {
8985 const Icon = section.icon;
8986 const isActive = currentSection === section.id;
8987 const sectionNumber =
8988 essentialsSections.length +
8989 detailsSections.length +
8990 index +
8991 1;
8992 return (
8993 <button
8994 key={section.id}
8995 type="button"
8996 onClick={() => setCurrentSection(section.id)}
8997 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border ${
8998 isActive
8999 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
9000 : section.hasErrors
9001 ? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10"
9002 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
9003 }`}
9004 >
9005 <div className="flex items-center gap-2 flex-shrink-0">
9006 <span
9007 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
9008 isActive
9009 ? "bg-blue-600 text-white dark:bg-blue-500"
9010 : section.completed
9011 ? "bg-green-500 text-white"
9012 : section.hasErrors
9013 ? "bg-red-500 text-white"
9014 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
9015 }`}
9016 >
9017 {sectionNumber}
9018 </span>
9019 <Icon
9020 className={`w-4 h-4 transition-colors ${
9021 isActive
9022 ? "text-blue-600 dark:text-blue-400"
9023 : section.hasErrors
9024 ? "text-red-600 dark:text-red-400"
9025 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
9026 }`}
9027 />
9028 </div>
9029 <span className="flex-1 min-w-0 break-words leading-snug">
9030 {section.label}
9031 </span>
9032 {section.hasErrors && (
9033 <span
9034 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
9035 title={__("This section has errors", "yatra")}
9036 />
9037 )}
9038 </button>
9039 );
9040 })}
9041 </div>
9042 </div>
9043
9044 {/* PHASE 4: ADVANCED */}
9045 <div className="pb-12">
9046 <div className="flex items-center gap-2 mb-3 px-1">
9047 <div className="flex items-center gap-2 flex-1">
9048 <Settings className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
9049 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
9050 {__("PHASE 4: ADVANCED", "yatra")}
9051 </h3>
9052 </div>
9053 <Badge
9054 variant="outline"
9055 className="text-[9px] px-1.5 py-0 bg-gray-50 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800"
9056 >
9057 {__("Power Users", "yatra")}
9058 </Badge>
9059 </div>
9060 <div className="space-y-0.5">
9061 {advancedSections.map((section, index) => {
9062 const Icon = section.icon;
9063 const isActive = currentSection === section.id;
9064 const sectionNumber =
9065 essentialsSections.length +
9066 detailsSections.length +
9067 optimizationSections.length +
9068 index +
9069 1;
9070 return (
9071 <button
9072 key={section.id}
9073 type="button"
9074 onClick={() => setCurrentSection(section.id)}
9075 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-normal transition-all duration-200 text-left group border ${
9076 isActive
9077 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
9078 : "border-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-gray-100"
9079 }`}
9080 >
9081 <div className="flex items-center gap-2 flex-shrink-0">
9082 <span
9083 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
9084 isActive
9085 ? "bg-blue-600 text-white dark:bg-blue-500"
9086 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
9087 }`}
9088 >
9089 {sectionNumber}
9090 </span>
9091 <Icon
9092 className={`w-4 h-4 transition-colors ${
9093 isActive
9094 ? "text-blue-600 dark:text-blue-400"
9095 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
9096 }`}
9097 />
9098 </div>
9099 <span className="flex-1 min-w-0 break-words leading-snug">
9100 {section.label}
9101 </span>
9102 </button>
9103 );
9104 })}
9105 </div>
9106 </div>
9107 </div>
9108 </div>
9109
9110 {/* Main Content */}
9111 <div className="flex-1 overflow-y-auto bg-white dark:bg-gray-900">
9112 <div className="p-6 pb-24 max-w-4xl">
9113 {errors.submit && (
9114 <Alert variant="error" className="mb-4">
9115 {errors.submit}
9116 </Alert>
9117 )}
9118 {renderSectionContent()}
9119 </div>
9120 </div>
9121 </div>
9122
9123 {/* Modals */}
9124 {/* Highlight Modal */}
9125 {showHighlightModal && (
9126 <div
9127 className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
9128 onClick={() => setShowHighlightModal(false)}
9129 >
9130 <div
9131 className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"
9132 onClick={(e) => e.stopPropagation()}
9133 >
9134 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
9135 {__("Add Highlight", "yatra")}
9136 </h3>
9137 <div className="space-y-4">
9138 <div>
9139 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
9140 {__("Highlight Text", "yatra")}
9141 </label>
9142 <Input
9143 type="text"
9144 value={modalInput.text}
9145 onChange={(e) =>
9146 setModalInput({ ...modalInput, text: e.target.value })
9147 }
9148 placeholder={__("e.g., Private guide included", "yatra")}
9149 autoFocus
9150 onKeyDown={(e) => {
9151 if (e.key === "Enter") {
9152 e.preventDefault();
9153 handleHighlightSave();
9154 }
9155 }}
9156 />
9157 <HelpText
9158 text={__(
9159 'Keep it short and impactful. Examples: "All meals included", "Skip-the-line tickets", "Free airport transfer"',
9160 "yatra",
9161 )}
9162 className="mt-2"
9163 />
9164 </div>
9165 <div className="flex items-center justify-end gap-2">
9166 <Button
9167 variant="outline"
9168 onClick={() => setShowHighlightModal(false)}
9169 >
9170 {__("Cancel", "yatra")}
9171 </Button>
9172 <Button
9173 onClick={handleHighlightSave}
9174 disabled={!modalInput.text.trim()}
9175 >
9176 {__("Add", "yatra")}
9177 </Button>
9178 </div>
9179 </div>
9180 </div>
9181 </div>
9182 )}
9183
9184 {/* Revisions Dialog */}
9185 {showRevisionsDialog && (
9186 <div
9187 className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/50 backdrop-blur-sm"
9188 onClick={() => setShowRevisionsDialog(false)}
9189 >
9190 <Card
9191 className="w-full max-w-2xl max-h-[80vh] mx-4 shadow-xl overflow-hidden flex flex-col"
9192 onClick={(e) => e.stopPropagation()}
9193 >
9194 <CardHeader className="pb-3 flex-shrink-0">
9195 <div className="flex items-center justify-between">
9196 <CardTitle className="text-lg flex items-center gap-2">
9197 <History className="w-5 h-5" />
9198 {__("Trip Revisions", "yatra")}
9199 </CardTitle>
9200 <button
9201 onClick={() => setShowRevisionsDialog(false)}
9202 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
9203 aria-label={__("Close", "yatra")}
9204 >
9205 <X className="w-5 h-5" />
9206 </button>
9207 </div>
9208 </CardHeader>
9209 <CardContent className="flex-1 overflow-y-auto pb-4">
9210 {isLoadingRevisions ? (
9211 <div className="flex items-center justify-center py-8">
9212 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
9213 <span className="ml-2 text-gray-600 dark:text-gray-400">
9214 {__("Loading revisions...", "yatra")}
9215 </span>
9216 </div>
9217 ) : revisions.length === 0 ? (
9218 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
9219 {__("No revisions found", "yatra")}
9220 </div>
9221 ) : (
9222 <div className="space-y-2">
9223 {revisions.map((revision) => (
9224 <div
9225 key={revision.id}
9226 className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors"
9227 onClick={() => handleRevisionClick(revision.id)}
9228 >
9229 <div className="flex items-center justify-between">
9230 <div className="flex-1">
9231 <div className="flex items-center gap-2 mb-1">
9232 <Badge variant="outline">v{revision.version}</Badge>
9233 <span className="text-sm font-medium text-gray-900 dark:text-white">
9234 {__("Revision", "yatra")} {revision.version}
9235 </span>
9236 </div>
9237 <div className="text-xs text-gray-500 dark:text-gray-400">
9238 {__("Created by", "yatra")}{" "}
9239 {revision.created_by_name} •{" "}
9240 {new Date(revision.created_at).toLocaleString()}
9241 </div>
9242 </div>
9243 <Button
9244 variant="outline"
9245 size="sm"
9246 onClick={(e) => {
9247 e.stopPropagation();
9248 handleRevisionClick(revision.id);
9249 }}
9250 >
9251 {__("Use This Revision", "yatra")}
9252 </Button>
9253 </div>
9254 </div>
9255 ))}
9256 </div>
9257 )}
9258 </CardContent>
9259 </Card>
9260 </div>
9261 )}
9262
9263 {/* Revision Confirmation Dialog */}
9264 <ConfirmationDialog
9265 isOpen={showRevisionConfirm}
9266 onClose={() => {
9267 setShowRevisionConfirm(false);
9268 setSelectedRevisionId(null);
9269 }}
9270 onConfirm={handleRevisionConfirm}
9271 title={__("Use This Revision?", "yatra")}
9272 message={__(
9273 "Do you want to use this revision? This will replace all current form data with the revision data.",
9274 "yatra",
9275 )}
9276 confirmText={__("Yes, Use This Revision", "yatra")}
9277 cancelText={__("Cancel", "yatra")}
9278 variant="info"
9279 isLoading={false}
9280 />
9281
9282 {/* Landmark Input Modal */}
9283 <Modal
9284 isOpen={showLandmarkDialog}
9285 onClose={() => {
9286 setShowLandmarkDialog(false);
9287 setLandmarkInput("");
9288 }}
9289 title={__("Add Landmark", "yatra")}
9290 description={__(
9291 "Enter the name of the landmark or point of interest",
9292 "yatra",
9293 )}
9294 size="sm"
9295 hideFooter={false}
9296 footer={
9297 <div className="flex gap-2 justify-end">
9298 <Button
9299 variant="outline"
9300 onClick={() => {
9301 setShowLandmarkDialog(false);
9302 setLandmarkInput("");
9303 }}
9304 >
9305 {__("Cancel", "yatra")}
9306 </Button>
9307 <Button
9308 onClick={() => {
9309 if (landmarkInput.trim()) {
9310 handleFieldChange("landmarks", [
9311 ...formData.landmarks,
9312 landmarkInput.trim(),
9313 ]);
9314 setLandmarkInput("");
9315 setShowLandmarkDialog(false);
9316 }
9317 }}
9318 disabled={!landmarkInput.trim()}
9319 >
9320 <Plus className="w-4 h-4 mr-2" />
9321 {__("Add Landmark", "yatra")}
9322 </Button>
9323 </div>
9324 }
9325 >
9326 <div className="space-y-4">
9327 <div>
9328 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
9329 {__("Landmark Name", "yatra")}
9330 </label>
9331 <Input
9332 type="text"
9333 value={landmarkInput}
9334 onChange={(e) => setLandmarkInput(e.target.value)}
9335 placeholder={__(
9336 "e.g., Eiffel Tower, Central Park, Grand Canyon",
9337 "yatra",
9338 )}
9339 className="w-full"
9340 autoFocus
9341 onKeyDown={(e) => {
9342 if (e.key === "Enter" && landmarkInput.trim()) {
9343 handleFieldChange("landmarks", [
9344 ...formData.landmarks,
9345 landmarkInput.trim(),
9346 ]);
9347 setLandmarkInput("");
9348 setShowLandmarkDialog(false);
9349 }
9350 }}
9351 />
9352 </div>
9353 <div className="text-xs text-gray-500 dark:text-gray-400">
9354 {__(
9355 "Examples: Famous monuments, natural landmarks, historical sites, viewpoints, or points of interest that travelers will visit during this trip.",
9356 "yatra",
9357 )}
9358 </div>
9359 </div>
9360 </Modal>
9361 </div>
9362 );
9363 };
9364
9365 export default TripForm;
9366