PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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.16, at resources/js/pages/TripForm.tsx

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