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

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

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