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

9,707 lines 412.5 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 duration_hours: tripData.duration_hours?.toString() || "",
2227 available_from: tripData.available_from || "",
2228 available_to: tripData.available_to || "",
2229 booking_window_days: tripData.booking_window_days?.toString() || "",
2230 seasonal_availability: tripData.seasonal_availability || "",
2231 best_season: tripData.best_season || "",
2232 peak_season: tripData.peak_season || "",
2233 off_season: tripData.off_season || "",
2234 activity_types: extractIds(tripData.activity_types || []),
2235 difficulty_level: tripData.difficulty_level?.toString() || "",
2236 trip_category: extractIds(tripData.trip_category || []),
2237 tags: Array.isArray(tripData.tags) ? tripData.tags : [],
2238 featured_priority: tripData.featured_priority || "none",
2239 accommodation_type: tripData.accommodation_type || "",
2240 meal_plan: tripData.meal_plan || "",
2241 accommodation_details: tripData.accommodation_details || "",
2242 transportation_included: tripData.transportation_included || false,
2243 pickup_location: tripData.pickup_location || "",
2244 dropoff_location: tripData.dropoff_location || "",
2245 transportation_details: tripData.transportation_details || "",
2246 pricing_type: (tripData.pricing_type ||
2247 (tripData.price_types &&
2248 Array.isArray(tripData.price_types) &&
2249 tripData.price_types.length > 0
2250 ? "traveler_based"
2251 : "regular")) as "regular" | "traveler_based",
2252 original_price: tripData.original_price?.toString() || "",
2253 discounted_price: tripData.discounted_price?.toString() || "",
2254 price_types: Array.isArray(tripData.price_types)
2255 ? tripData.price_types.map((pt: any) => ({
2256 category_id: Number(pt.category_id) || 0,
2257 original_price: pt.original_price?.toString() || "",
2258 discounted_price: pt.discounted_price?.toString() || "",
2259 is_default: Boolean(pt.is_default),
2260 }))
2261 : [],
2262 deposit_amount: tripData.deposit_amount?.toString() || "",
2263 deposit_percentage: tripData.deposit_percentage?.toString() || "",
2264 payment_terms: tripData.payment_terms || "",
2265 max_travelers: tripData.max_travelers?.toString() || "",
2266 min_travelers: tripData.min_travelers?.toString() || "",
2267 booking_deadline_hours: tripData.booking_deadline_hours || "",
2268 cancellation_policy: tripData.cancellation_policy || "",
2269 age_min: tripData.age_min?.toString() || "",
2270 age_max: tripData.age_max?.toString() || "",
2271 physical_requirements: tripData.physical_requirements || "",
2272 visa_requirements: tripData.visa_requirements || "",
2273 vaccination_requirements: tripData.vaccination_requirements || "",
2274 disable_booking: Boolean(tripData.custom_fields?.disable_booking),
2275 // tinyint(1) columns can serialize from PHP/wpdb as the string "0"/"1".
2276 // JS treats "0" as truthy, so a plain `value || false` would leave the
2277 // checkbox stuck on after the user un-checked + saved. Coerce explicitly.
2278 has_default_time_slots:
2279 tripData.has_default_time_slots === true ||
2280 tripData.has_default_time_slots === 1 ||
2281 tripData.has_default_time_slots === "1",
2282 default_time_slots: Array.isArray(tripData.default_time_slots)
2283 ? tripData.default_time_slots
2284 : tripData.default_time_slots
2285 ? JSON.parse(tripData.default_time_slots)
2286 : [],
2287 departure_time: tripData.departure_time || "09:00",
2288 included_items: normalizeAmenityItems(tripData.included_items),
2289 excluded_items: normalizeAmenityItems(tripData.excluded_items),
2290 // Preserve current itinerary data if it exists, otherwise use database data
2291 itinerary_days: shouldPreserveItinerary
2292 ? currentItineraryDays
2293 : normalizeItineraryDays(tripData.itinerary_days),
2294 // Preserve current gallery data if it exists, otherwise use database data
2295 gallery_images: shouldPreserveGallery
2296 ? currentGalleryImages
2297 : normalizeGalleryImages(tripData.gallery_images),
2298 featured_image: tripData.featured_image
2299 ? Number(tripData.featured_image)
2300 : null,
2301 downloadable_items: normalizeDownloadableItems(
2302 tripData.downloadable_items,
2303 ),
2304 faqs: normalizeFaqs(tripData.faqs),
2305 frontend_tabs: Array.isArray(tripData.frontend_tabs)
2306 ? tripData.frontend_tabs
2307 : [
2308 // Core sections (always present) - in logical order
2309 {
2310 id: "overview",
2311 label: "Overview",
2312 enabled: true,
2313 order: 1,
2314 content_type: "overview",
2315 icon: { type: "icon", value: "book" },
2316 },
2317 {
2318 id: "itinerary",
2319 label: "Itinerary",
2320 enabled: true,
2321 order: 2,
2322 content_type: "itinerary",
2323 icon: { type: "icon", value: "calendar" },
2324 },
2325 {
2326 id: "included",
2327 label: "Included",
2328 enabled: true,
2329 order: 3,
2330 content_type: "included_excluded",
2331 icon: { type: "icon", value: "check" },
2332 },
2333 {
2334 id: "location",
2335 label: "Location",
2336 enabled: true,
2337 order: 4,
2338 content_type: "location",
2339 icon: { type: "icon", value: "map-pin" },
2340 },
2341 {
2342 id: "important_info",
2343 label: "Important Info",
2344 enabled: true,
2345 order: 5,
2346 content_type: "important_info",
2347 icon: { type: "icon", value: "info" },
2348 },
2349
2350 // Conditional sections (enabled by default, shown conditionally on frontend)
2351 {
2352 id: "downloads",
2353 label: "Downloads",
2354 enabled: true,
2355 order: 6,
2356 content_type: "downloads",
2357 icon: { type: "icon", value: "download" },
2358 },
2359 {
2360 id: "faq",
2361 label: "FAQ",
2362 enabled: true,
2363 order: 7,
2364 content_type: "faq",
2365 icon: { type: "icon", value: "help-circle" },
2366 },
2367 {
2368 id: "trip_story",
2369 label: "Story",
2370 enabled: true,
2371 order: 8,
2372 content_type: "trip_story",
2373 custom_content: "",
2374 icon: { type: "icon", value: "book" },
2375 },
2376 {
2377 id: "what_makes_special",
2378 label: "Special",
2379 enabled: true,
2380 order: 9,
2381 content_type: "what_makes_special",
2382 custom_content: "",
2383 icon: { type: "icon", value: "star" },
2384 },
2385 {
2386 id: "testimonials",
2387 label: "Testimonials",
2388 enabled: true,
2389 order: 10,
2390 content_type: "testimonials",
2391 icon: { type: "icon", value: "message-circle" },
2392 },
2393 ],
2394 availability_dates: normalizeAvailabilityDates(
2395 tripData.availability_dates,
2396 ),
2397 status: (tripData.status || "draft") as
2398 | "draft"
2399 | "review"
2400 | "approved"
2401 | "publish"
2402 | "archived"
2403 | "suspended",
2404 scheduled_publish_date: tripData.scheduled_publish_date || "",
2405 scheduled_unpublish_date: tripData.scheduled_unpublish_date || "",
2406 version: tripData.version || 1,
2407 seasonal_auto_enable: tripData.seasonal_auto_enable || false,
2408 seasonal_enable_date: tripData.seasonal_enable_date || "",
2409 seasonal_disable_date: tripData.seasonal_disable_date || "",
2410 meta_title: tripData.meta_title || "",
2411 meta_description: tripData.meta_description || "",
2412 meta_keywords: tripData.meta_keywords || "",
2413 attributes: tripAttributesData || {},
2414 });
2415
2416 if (tripData.featured_image_url && tripData.featured_image) {
2417 const numericId = Number(tripData.featured_image) || 0;
2418 setFeaturedImagePreview(tripData.featured_image_url);
2419 if (numericId > 0) {
2420 featuredImageCache.current[numericId] = tripData.featured_image_url;
2421 }
2422 } else if (!tripData.featured_image) {
2423 setFeaturedImagePreview("");
2424 }
2425 // eslint-disable-next-line react-hooks/exhaustive-deps
2426 }, [tripData, tripAttributesData, destinationsData, isEditMode, tripId]);
2427
2428 useEffect(() => {
2429 let isMounted = true;
2430
2431 const resolveFeaturedImage = async () => {
2432 const attachmentId = formData.featured_image;
2433
2434 if (!attachmentId) {
2435 setFeaturedImagePreview("");
2436 return;
2437 }
2438
2439 const cachedUrl = featuredImageCache.current[attachmentId];
2440 if (cachedUrl) {
2441 setFeaturedImagePreview(cachedUrl);
2442 return;
2443 }
2444
2445 if (!mediaBaseUrl) {
2446 setFeaturedImagePreview("");
2447 return;
2448 }
2449
2450 setIsResolvingFeaturedImage(true);
2451 try {
2452 const data = await wpService.getMedia(attachmentId);
2453 const url = data?.source_url || "";
2454 if (url && isMounted) {
2455 featuredImageCache.current[attachmentId] = url;
2456 setFeaturedImagePreview(url);
2457 } else if (isMounted) {
2458 setFeaturedImagePreview("");
2459 }
2460 } catch (error) {
2461 console.error("Failed to resolve featured image URL:", error);
2462 if (isMounted) {
2463 setFeaturedImagePreview("");
2464 }
2465 } finally {
2466 if (isMounted) {
2467 setIsResolvingFeaturedImage(false);
2468 }
2469 }
2470 };
2471
2472 resolveFeaturedImage();
2473
2474 return () => {
2475 isMounted = false;
2476 };
2477 }, [formData.featured_image, mediaBaseUrl]);
2478
2479 // Map errors to sections - also check for price_type errors
2480 const getSectionErrors = (sectionId: SectionId): string[] => {
2481 const errorMap: Record<SectionId, string[]> = {
2482 basic: [
2483 "title",
2484 "slug",
2485 "description",
2486 "featured_image",
2487 "trip_type",
2488 "duration_days",
2489 "duration_nights",
2490 "duration_hours",
2491 ],
2492 location: ["destinations", "starting_location", "ending_location"],
2493 duration: ["available_from", "available_to", "booking_window_days"],
2494 pricing: ["original_price", "discounted_price", "price_types"],
2495 booking: ["min_travelers", "max_travelers", "age_min", "age_max"],
2496 attributes: ["attributes"],
2497 itinerary: ["itinerary_days"],
2498 included: ["included_items", "excluded_items"],
2499 media: ["gallery_images", "video_url", "virtual_tour_url"], // Removed featured_image - it's in basic section
2500 downloads: ["downloadable_items"],
2501 categorization: ["trip_category", "activity_types"],
2502 faqs: ["faqs"],
2503 seo: ["meta_title", "meta_description"],
2504 advanced: [],
2505 };
2506
2507 const sectionFields = errorMap[sectionId] || [];
2508 const fieldErrors = sectionFields.filter((field) => errors[field]);
2509
2510 // Also check for price_type errors (they have dynamic keys like price_type_0_original)
2511 if (sectionId === "pricing") {
2512 const priceTypeErrors = Object.keys(errors).filter((key) =>
2513 key.startsWith("price_type_"),
2514 );
2515 return [...fieldErrors, ...priceTypeErrors];
2516 }
2517
2518 return fieldErrors;
2519 };
2520
2521 // Define sections - Reorganized for beginner-friendly UX
2522 // PHASE 1: ESSENTIALS (Must complete for publishable trip)
2523 const essentialsSections: Section[] = [
2524 // 1. Trip Basics - What you're offering
2525 {
2526 id: "basic",
2527 label: __("Trip Basics", "yatra"),
2528 icon: FileText,
2529 required: true,
2530 completed: !!(formData.title?.trim() && formData.slug?.trim()),
2531 hasErrors: getSectionErrors("basic").length > 0,
2532 },
2533
2534 // 2. Location & Route - Where it happens
2535 {
2536 id: "location",
2537 label: __("Location & Route", "yatra"),
2538 icon: MapPin,
2539 required: false,
2540 completed: !!(formData.destinations.length > 0),
2541 hasErrors: getSectionErrors("location").length > 0,
2542 },
2543
2544 // 3. Pricing - How much it costs
2545 {
2546 id: "pricing",
2547 label: __("Pricing", "yatra"),
2548 icon: DollarSign,
2549 required: true,
2550 completed:
2551 formData.pricing_type === "regular"
2552 ? !!(
2553 formData.original_price && parseFloat(formData.original_price) > 0
2554 )
2555 : formData.price_types.some(
2556 (pt) => pt.original_price && parseFloat(pt.original_price) > 0,
2557 ),
2558 hasErrors: getSectionErrors("pricing").length > 0,
2559 },
2560
2561 // 4. Availability & Booking - When available + booking rules (merged duration + booking)
2562 {
2563 id: "duration",
2564 label: __("Availability & Booking", "yatra"),
2565 icon: Calendar,
2566 required: false,
2567 completed: !!(
2568 formData.available_from ||
2569 formData.available_to ||
2570 (formData.min_travelers && formData.max_travelers)
2571 ),
2572 hasErrors:
2573 getSectionErrors("duration").length > 0 ||
2574 getSectionErrors("booking").length > 0,
2575 },
2576 ];
2577
2578 // PHASE 2: DETAILS (Enhance trip quality)
2579 const detailsSections: Section[] = [
2580 // 5. Trip Details - Description + Itinerary + Included/Excluded (merged 3 sections)
2581 {
2582 id: "itinerary",
2583 label: __("Trip Details", "yatra"),
2584 icon: BookOpen,
2585 required: false,
2586 completed:
2587 formData.itinerary_days.length > 0 ||
2588 formData.included_items.length > 0 ||
2589 formData.excluded_items.length > 0,
2590 hasErrors:
2591 getSectionErrors("itinerary").length > 0 ||
2592 getSectionErrors("included").length > 0,
2593 },
2594 ];
2595
2596 // PHASE 3: OPTIMIZATION (Improve discoverability)
2597 const optimizationSections: Section[] = [
2598 // 6. Media & Gallery - Photos, videos, testimonials
2599 {
2600 id: "media",
2601 label: __("Media & Gallery", "yatra"),
2602 icon: Image,
2603 required: false,
2604 completed: formData.gallery_images.length > 0 || !!formData.video_url,
2605 hasErrors: getSectionErrors("media").length > 0,
2606 },
2607 ...(showDownloadsUI
2608 ? ([
2609 {
2610 id: "downloads",
2611 label: __("Downloads", "yatra"),
2612 icon: Download,
2613 required: false,
2614 completed: (formData.downloadable_items || []).length > 0,
2615 hasErrors: getSectionErrors("downloads").length > 0,
2616 },
2617 ] as Section[])
2618 : []),
2619
2620 // 7. Categories & Attributes - Classification + Custom Attributes (merged 2 sections)
2621 {
2622 id: "categorization",
2623 label: __("Categories & Attributes", "yatra"),
2624 icon: Tag,
2625 required: false,
2626 completed: !!(
2627 formData.trip_category ||
2628 formData.activity_types.length > 0 ||
2629 formData.tags.length > 0 ||
2630 (formData.attributes && Object.keys(formData.attributes).length > 0)
2631 ),
2632 hasErrors:
2633 getSectionErrors("categorization").length > 0 ||
2634 getSectionErrors("attributes").length > 0,
2635 },
2636
2637 // 8. SEO & Marketing - SEO + FAQs (merged, fixed duplicate)
2638 {
2639 id: "seo",
2640 label: __("SEO & Marketing", "yatra"),
2641 icon: Search,
2642 required: false,
2643 completed: !!(
2644 formData.meta_title ||
2645 formData.meta_description ||
2646 formData.faqs.length > 0
2647 ),
2648 hasErrors:
2649 getSectionErrors("seo").length > 0 ||
2650 getSectionErrors("faqs").length > 0,
2651 },
2652 ];
2653
2654 // PHASE 4: ADVANCED (Power users only)
2655 const advancedSections: Section[] = [
2656 // 9. Advanced Settings - Publishing, scheduling, technical
2657 {
2658 id: "advanced",
2659 label: __("Advanced Settings", "yatra"),
2660 icon: Settings,
2661 required: false,
2662 completed: false,
2663 hasErrors: false,
2664 },
2665 ];
2666
2667 const allSections = [
2668 ...essentialsSections,
2669 ...detailsSections,
2670 ...optimizationSections,
2671 ...advancedSections,
2672 ];
2673 const currentStepIndex = allSections.findIndex(
2674 (s) => s.id === currentSection,
2675 );
2676
2677 // Navigation helpers
2678 const goToNextSection = () => {
2679 if (currentStepIndex < allSections.length - 1) {
2680 setCurrentSection(allSections[currentStepIndex + 1].id);
2681 }
2682 };
2683
2684 const goToPreviousSection = () => {
2685 if (currentStepIndex > 0) {
2686 setCurrentSection(allSections[currentStepIndex - 1].id);
2687 }
2688 };
2689
2690 // Smart defaults - Auto-calculate nights from days (only if nights is empty)
2691 useEffect(() => {
2692 if (
2693 formData.duration_days &&
2694 formData.trip_type === "multi_day" &&
2695 (!formData.duration_nights || formData.duration_nights === "")
2696 ) {
2697 const days = parseInt(formData.duration_days);
2698 if (days > 0 && !isNaN(days)) {
2699 setFormData((prev) => ({
2700 ...prev,
2701 duration_nights: String(Math.max(0, days - 1)),
2702 }));
2703 }
2704 }
2705 // eslint-disable-next-line react-hooks/exhaustive-deps
2706 }, [formData.duration_days, formData.trip_type]);
2707
2708 // Auto-calculate days from nights (only if days is empty)
2709 useEffect(() => {
2710 if (
2711 formData.duration_nights &&
2712 formData.trip_type === "multi_day" &&
2713 (!formData.duration_days || formData.duration_days === "")
2714 ) {
2715 const nights = parseInt(formData.duration_nights);
2716 if (nights > 0 && !isNaN(nights)) {
2717 setFormData((prev) => ({ ...prev, duration_days: String(nights + 1) }));
2718 }
2719 }
2720 // eslint-disable-next-line react-hooks/exhaustive-deps
2721 }, [formData.duration_nights, formData.trip_type]);
2722
2723 // Auto-generate slug from title
2724 const generateSlug = (title: string): string => {
2725 return title
2726 .toLowerCase()
2727 .trim()
2728 .replace(/[^\w\s-]/g, "")
2729 .replace(/[\s_-]+/g, "-")
2730 .replace(/^-+|-+$/g, "");
2731 };
2732
2733 const handleTitleChange = (value: string) => {
2734 setFormData((prev) => ({
2735 ...prev,
2736 title: value,
2737 slug: prev.slug || generateSlug(value),
2738 }));
2739 if (errors.title) {
2740 setErrors((prev) => ({ ...prev, title: "" }));
2741 }
2742 };
2743
2744 const handleFieldChange = (field: keyof TripFormData, value: any) => {
2745 setFormData((prev) => ({ ...prev, [field]: value }));
2746 if (errors[field]) {
2747 setErrors((prev) => ({ ...prev, [field]: "" }));
2748 }
2749
2750 if (field === "featured_image") {
2751 // Handle explicit null (when removing image)
2752 if (value === null || value === "" || value === undefined) {
2753 setFeaturedImagePreview("");
2754 } else {
2755 const numericValue = typeof value === "number" ? value : Number(value);
2756 if (!numericValue) {
2757 setFeaturedImagePreview("");
2758 } else if (featuredImageCache.current[numericValue]) {
2759 setFeaturedImagePreview(featuredImageCache.current[numericValue]);
2760 }
2761 }
2762 }
2763 };
2764
2765 const handleHighlightAdd = () => {
2766 setShowHighlightModal(true);
2767 setModalInput({ text: "", question: "", answer: "" });
2768 };
2769
2770 const handleHighlightSave = () => {
2771 if (modalInput.text && modalInput.text.trim()) {
2772 setFormData((prev) => ({
2773 ...prev,
2774 highlights: [...prev.highlights, modalInput.text.trim()],
2775 }));
2776 setShowHighlightModal(false);
2777 setModalInput({ text: "", question: "", answer: "" });
2778 }
2779 };
2780
2781 const handleHighlightRemove = (index: number) => {
2782 setFormData((prev) => ({
2783 ...prev,
2784 highlights: prev.highlights.filter((_, i) => i !== index),
2785 }));
2786 };
2787
2788 // Included/Excluded handlers - will be integrated into ItinerarySection component
2789 // Keeping for future integration when included/excluded are merged into itinerary section
2790 // const handleIncludedAdd = () => { ... };
2791 // const handleIncludedRemove = (index: number) => { ... };
2792 // const handleExcludedAdd = () => { ... };
2793 // const handleExcludedRemove = (index: number) => { ... };
2794
2795 const handleFAQAdd = () => {
2796 setFormData((prev) => ({
2797 ...prev,
2798 faqs: [...prev.faqs, { question: "", answer: "" }],
2799 }));
2800 };
2801
2802 const handleFAQRemove = (index: number) => {
2803 setFormData((prev) => ({
2804 ...prev,
2805 faqs: prev.faqs.filter((_, i) => i !== index),
2806 }));
2807 };
2808
2809 const handleFAQChange = (
2810 index: number,
2811 field: "question" | "answer",
2812 value: string,
2813 ) => {
2814 setFormData((prev) => ({
2815 ...prev,
2816 faqs: prev.faqs.map((faq, i) =>
2817 i === index ? { ...faq, [field]: value } : faq,
2818 ),
2819 }));
2820 };
2821
2822 const handleGalleryAdd = () => {
2823 // Use WordPress media library with multiple selection
2824 if (window.wp && window.wp.media) {
2825 const mediaUploader = window.wp.media({
2826 title: __("Select Gallery Images", "yatra"),
2827 button: { text: __("Add to Gallery", "yatra") },
2828 multiple: true, // Allow multiple image selection
2829 library: { type: "image" },
2830 });
2831
2832 mediaUploader.on("select", () => {
2833 const selection = mediaUploader.state().get("selection");
2834 const newImages: Array<{
2835 id: number;
2836 url: string;
2837 thumbnail_url?: string;
2838 alt_text?: string;
2839 caption?: string;
2840 }> = [];
2841
2842 selection.each((attachment: any) => {
2843 const image = attachment.toJSON();
2844 if (image.url) {
2845 newImages.push({
2846 id: image.id || 0,
2847 url: image.url,
2848 thumbnail_url:
2849 image.sizes?.thumbnail?.url ||
2850 image.sizes?.medium?.url ||
2851 image.url,
2852 alt_text: image.alt || "",
2853 caption: image.caption || "",
2854 });
2855 }
2856 });
2857
2858 if (newImages.length > 0) {
2859 setFormData((prev) => ({
2860 ...prev,
2861 gallery_images: [...prev.gallery_images, ...newImages],
2862 }));
2863 }
2864 });
2865
2866 prepareWordPressMediaFrameOpen();
2867 mediaUploader.open();
2868 } else {
2869 // Fallback for when wp.media is not available
2870 showToast(
2871 __(
2872 "Media library not available. Please ensure you are logged in as admin.",
2873 "yatra",
2874 ),
2875 "error",
2876 );
2877 }
2878 };
2879
2880 const handleGalleryRemove = (index: number) => {
2881 setFormData((prev) => ({
2882 ...prev,
2883 gallery_images: prev.gallery_images.filter((_, i) => i !== index),
2884 }));
2885 };
2886
2887 const handleGalleryReorder = (fromIndex: number, toIndex: number) => {
2888 setFormData((prev) => {
2889 const newImages = [...prev.gallery_images];
2890 const [movedImage] = newImages.splice(fromIndex, 1);
2891 newImages.splice(toIndex, 0, movedImage);
2892 return { ...prev, gallery_images: newImages };
2893 });
2894 };
2895
2896 const handleDownloadableItemAdd = () => {
2897 setFormData((prev) => {
2898 const nextOrder = (prev.downloadable_items?.length || 0) + 1;
2899 return {
2900 ...prev,
2901 downloadable_items: [
2902 ...(prev.downloadable_items || []),
2903 {
2904 id: null,
2905 title: "",
2906 description: "",
2907 attachment_id: null,
2908 attachment_url: "",
2909 attachment_title: "",
2910 visibility: "booked_only",
2911 enabled: true,
2912 sort_order: nextOrder,
2913 },
2914 ],
2915 };
2916 });
2917 };
2918
2919 const handleDownloadableItemRemove = (index: number) => {
2920 setFormData((prev) => ({
2921 ...prev,
2922 downloadable_items: (prev.downloadable_items || []).filter(
2923 (_, i) => i !== index,
2924 ),
2925 }));
2926 };
2927
2928 const handleDownloadableItemMove = (fromIndex: number, toIndex: number) => {
2929 setFormData((prev) => {
2930 const items = [...(prev.downloadable_items || [])];
2931 const [moved] = items.splice(fromIndex, 1);
2932 items.splice(toIndex, 0, moved);
2933
2934 const normalized = items.map((item, idx) => ({
2935 ...item,
2936 sort_order: idx + 1,
2937 }));
2938
2939 return { ...prev, downloadable_items: normalized };
2940 });
2941 };
2942
2943 const handleDownloadableItemChange = (
2944 index: number,
2945 field: keyof DownloadableItem,
2946 value: any,
2947 ) => {
2948 setFormData((prev) => ({
2949 ...prev,
2950 downloadable_items: (prev.downloadable_items || []).map((item, i) =>
2951 i === index ? { ...item, [field]: value } : item,
2952 ),
2953 }));
2954 };
2955
2956 const handleDownloadableItemSelectFile = (index: number) => {
2957 if (window.wp && window.wp.media) {
2958 const mediaUploader = window.wp.media({
2959 title: __("Select File", "yatra"),
2960 button: { text: __("Use this file", "yatra") },
2961 multiple: false,
2962 });
2963
2964 mediaUploader.on("select", () => {
2965 const selection = mediaUploader.state().get("selection");
2966 const attachment = selection.first();
2967 if (!attachment) return;
2968 const file = attachment.toJSON();
2969
2970 handleDownloadableItemChange(index, "attachment_id", file.id || null);
2971 handleDownloadableItemChange(index, "attachment_url", file.url || "");
2972 handleDownloadableItemChange(
2973 index,
2974 "attachment_title",
2975 file.title || file.filename || "",
2976 );
2977 });
2978
2979 prepareWordPressMediaFrameOpen();
2980 mediaUploader.open();
2981 } else {
2982 // Fallback for when wp.media is not available
2983 showToast(
2984 __(
2985 "Media library not available. Please ensure you are logged in as admin.",
2986 "yatra",
2987 ),
2988 "error",
2989 );
2990 }
2991 };
2992
2993 const handlePriceTypeAdd = (categoryId: number) => {
2994 // Check if category already exists (compare as numbers to handle string/number mismatch)
2995 if (
2996 formData.price_types.some(
2997 (pt) => Number(pt.category_id) === Number(categoryId),
2998 )
2999 ) {
3000 showToast(
3001 __("This category already has pricing set", "yatra"),
3002 "warning",
3003 );
3004 return;
3005 }
3006 setFormData((prev) => ({
3007 ...prev,
3008 price_types: [
3009 ...prev.price_types,
3010 {
3011 category_id: categoryId,
3012 original_price: "",
3013 discounted_price: "",
3014 is_default: false,
3015 },
3016 ],
3017 }));
3018 };
3019
3020 const handlePriceTypeRemove = (categoryId: number) => {
3021 setFormData((prev) => ({
3022 ...prev,
3023 price_types: prev.price_types.filter(
3024 (pt) => Number(pt.category_id) !== Number(categoryId),
3025 ),
3026 }));
3027 };
3028
3029 const handlePriceTypeChange = (
3030 categoryId: number,
3031 field: "original_price" | "discounted_price",
3032 value: string,
3033 ) => {
3034 setFormData((prev) => ({
3035 ...prev,
3036 price_types: prev.price_types.map((pt) =>
3037 Number(pt.category_id) === Number(categoryId)
3038 ? { ...pt, [field]: value }
3039 : pt,
3040 ),
3041 }));
3042 };
3043
3044 const handlePriceTypeDefaultChange = (
3045 categoryId: number,
3046 isDefault: boolean,
3047 ) => {
3048 setFormData((prev) => ({
3049 ...prev,
3050 price_types: prev.price_types.map((pt) => {
3051 if (Number(pt.category_id) === Number(categoryId)) {
3052 return { ...pt, is_default: isDefault };
3053 }
3054 // Only allow one default at a time.
3055 return isDefault ? { ...pt, is_default: false } : pt;
3056 }),
3057 }));
3058 };
3059
3060 // Frontend Tabs Handlers
3061 const handleTabToggle = (tabId: string) => {
3062 setFormData((prev) => ({
3063 ...prev,
3064 frontend_tabs: prev.frontend_tabs.map((tab) =>
3065 tab.id === tabId ? { ...tab, enabled: !tab.enabled } : tab,
3066 ),
3067 }));
3068 };
3069
3070 // Handle dummy data fill
3071 const handleFillDummyData = () => {
3072 // Cycle through dummy data sets
3073 const nextIndex = (dummyDataIndex + 1) % dummyTripsData.length;
3074 setDummyDataIndex(nextIndex);
3075
3076 const dummyData = dummyTripsData[nextIndex];
3077
3078 // Populate form with dummy data
3079 setFormData({
3080 ...dummyData,
3081 // Ensure arrays are properly set
3082 destinations: dummyData.destinations || [],
3083 activity_types: dummyData.activity_types || [],
3084 price_types: dummyData.price_types || [],
3085 included_items: dummyData.included_items || [],
3086 excluded_items: dummyData.excluded_items || [],
3087 highlights: dummyData.highlights || [],
3088 faqs: dummyData.faqs || [],
3089 gallery_images: dummyData.gallery_images || [],
3090 itinerary_days: dummyData.itinerary_days || [],
3091 tags: dummyData.tags || [],
3092 testimonial_review_ids: dummyData.testimonial_review_ids || [],
3093 countries: dummyData.countries || [],
3094 regions: dummyData.regions || [],
3095 landmarks: dummyData.landmarks || [],
3096 availability_dates: dummyData.availability_dates || [],
3097 frontend_tabs: dummyData.frontend_tabs || [],
3098 has_default_time_slots: dummyData.has_default_time_slots || false,
3099 default_time_slots: dummyData.default_time_slots || [],
3100 departure_time: dummyData.departure_time || "09:00",
3101 downloadable_items: dummyData.downloadable_items || [],
3102 });
3103
3104 // Show toast notification
3105 showToast(
3106 __("Dummy data filled", "yatra") +
3107 ` (${nextIndex + 1}/${dummyTripsData.length})`,
3108 "success",
3109 );
3110 };
3111
3112 const handleTabLabelChange = (tabId: string, label: string) => {
3113 setFormData((prev) => ({
3114 ...prev,
3115 frontend_tabs: prev.frontend_tabs.map((tab) =>
3116 tab.id === tabId ? { ...tab, label } : tab,
3117 ),
3118 }));
3119 };
3120
3121 const handleTabMove = (tabId: string, direction: "up" | "down") => {
3122 setFormData((prev) => {
3123 const tabs = [...prev.frontend_tabs];
3124 const index = tabs.findIndex((t) => t.id === tabId);
3125 if (index === -1) return prev;
3126
3127 const newIndex = direction === "up" ? index - 1 : index + 1;
3128 if (newIndex < 0 || newIndex >= tabs.length) return prev;
3129
3130 [tabs[index], tabs[newIndex]] = [tabs[newIndex], tabs[index]];
3131 // Update order
3132 tabs.forEach((tab, i) => {
3133 tab.order = i + 1;
3134 });
3135
3136 return { ...prev, frontend_tabs: tabs };
3137 });
3138 };
3139
3140 const handleTabRemove = (tabId: string) => {
3141 setFormData((prev) => ({
3142 ...prev,
3143 frontend_tabs: prev.frontend_tabs
3144 .filter((tab) => tab.id !== tabId)
3145 .map((tab, i) => ({ ...tab, order: i + 1 })),
3146 }));
3147 };
3148
3149 const handleTabAdd = () => {
3150 const newTabId = `custom_${Date.now()}`;
3151 const maxOrder = Math.max(...formData.frontend_tabs.map((t) => t.order), 0);
3152 setFormData((prev) => ({
3153 ...prev,
3154 frontend_tabs: [
3155 ...prev.frontend_tabs,
3156 {
3157 id: newTabId,
3158 label: __("New Tab", "yatra"),
3159 enabled: true,
3160 order: maxOrder + 1,
3161 content_type: "custom",
3162 custom_content: "",
3163 },
3164 ],
3165 }));
3166 };
3167
3168 const handleTabContentTypeChange = (
3169 tabId: string,
3170 contentType: FrontendTab["content_type"],
3171 ) => {
3172 setFormData((prev) => ({
3173 ...prev,
3174 frontend_tabs: prev.frontend_tabs.map((tab) =>
3175 tab.id === tabId ? { ...tab, content_type: contentType } : tab,
3176 ),
3177 }));
3178 };
3179
3180 const handleTabCustomContentChange = (tabId: string, content: string) => {
3181 setFormData((prev) => ({
3182 ...prev,
3183 frontend_tabs: prev.frontend_tabs.map((tab) =>
3184 tab.id === tabId ? { ...tab, custom_content: content } : tab,
3185 ),
3186 }));
3187 };
3188
3189 const handleTabIconChange = (tabId: string, icon: IconPickerValue | null) => {
3190 setFormData((prev) => ({
3191 ...prev,
3192 frontend_tabs: prev.frontend_tabs.map((tab) =>
3193 tab.id === tabId ? { ...tab, icon } : tab,
3194 ),
3195 }));
3196 };
3197
3198 // Drag and drop handlers
3199 const [draggedTab, setDraggedTab] = useState<string | null>(null);
3200 const [dragOverTab, setDragOverTab] = useState<string | null>(null);
3201
3202 const handleDragStart = (e: React.DragEvent, tabId: string) => {
3203 setDraggedTab(tabId);
3204 e.dataTransfer.effectAllowed = "move";
3205 };
3206
3207 const handleDragOver = (e: React.DragEvent) => {
3208 e.preventDefault();
3209 e.dataTransfer.dropEffect = "move";
3210 };
3211
3212 const handleDragEnter = (tabId: string) => {
3213 setDragOverTab(tabId);
3214 };
3215
3216 const handleDragLeave = () => {
3217 setDragOverTab(null);
3218 };
3219
3220 const handleDrop = (e: React.DragEvent, targetTabId: string) => {
3221 e.preventDefault();
3222 setDragOverTab(null);
3223
3224 if (!draggedTab || draggedTab === targetTabId) {
3225 setDraggedTab(null);
3226 return;
3227 }
3228
3229 setFormData((prev) => {
3230 const tabs = [...prev.frontend_tabs];
3231 const draggedIndex = tabs.findIndex((t) => t.id === draggedTab);
3232 const targetIndex = tabs.findIndex((t) => t.id === targetTabId);
3233
3234 if (draggedIndex === -1 || targetIndex === -1) {
3235 return prev;
3236 }
3237
3238 // Remove dragged tab and insert at new position
3239 const [draggedTabObj] = tabs.splice(draggedIndex, 1);
3240 tabs.splice(targetIndex, 0, draggedTabObj);
3241
3242 // Update order values
3243 tabs.forEach((tab, i) => {
3244 tab.order = i + 1;
3245 });
3246
3247 return { ...prev, frontend_tabs: tabs };
3248 });
3249
3250 setDraggedTab(null);
3251 };
3252
3253 const handleDragEnd = () => {
3254 setDraggedTab(null);
3255 setDragOverTab(null);
3256 };
3257
3258 const buildEssentialFieldErrors = (): Record<string, string> => {
3259 const newErrors: Record<string, string> = {};
3260
3261 if (!formData.title.trim()) {
3262 newErrors.title = __("Title is required", "yatra");
3263 }
3264
3265 if (!formData.slug.trim()) {
3266 newErrors.slug = __("Slug is required", "yatra");
3267 } else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) {
3268 newErrors.slug = __(
3269 "Slug can only contain letters, numbers, and hyphens",
3270 "yatra",
3271 );
3272 }
3273
3274 return newErrors;
3275 };
3276
3277 const validateForm = (): boolean => {
3278 const newErrors = buildEssentialFieldErrors();
3279 setErrors(newErrors);
3280 return Object.keys(newErrors).length === 0;
3281 };
3282
3283 // Save mutation
3284 const saveMutation = useMutation({
3285 mutationFn: async (
3286 data: TripFormData & { status?: "draft" | "publish" },
3287 ) => {
3288 const payload = {
3289 title: data.title.trim(),
3290 slug: data.slug.trim(),
3291 description: data.description.trim(),
3292 short_description: data.short_description.trim(),
3293 highlights: data.highlights,
3294 trip_details: data.trip_details.trim(),
3295 what_makes_special: data.what_makes_special.trim(),
3296 trip_story: data.trip_story.trim(),
3297 video_url: data.video_url.trim(),
3298 virtual_tour_url: data.virtual_tour_url.trim(),
3299 testimonial_review_ids: Array.isArray(data.testimonial_review_ids)
3300 ? data.testimonial_review_ids.filter(
3301 (id): id is number => id !== null && id !== undefined && id > 0,
3302 )
3303 : [],
3304 destinations: data.destinations || [], // Array of destination IDs
3305 starting_location: data.starting_location.trim(),
3306 ending_location: data.ending_location.trim(),
3307 countries: data.countries || [],
3308 regions: data.regions || [],
3309 starting_latitude: data.starting_latitude
3310 ? parseFloat(data.starting_latitude)
3311 : null,
3312 starting_longitude: data.starting_longitude
3313 ? parseFloat(data.starting_longitude)
3314 : null,
3315 ending_latitude: data.ending_latitude
3316 ? parseFloat(data.ending_latitude)
3317 : null,
3318 ending_longitude: data.ending_longitude
3319 ? parseFloat(data.ending_longitude)
3320 : null,
3321 landmarks: data.landmarks || [],
3322 trip_type: data.trip_type,
3323 duration_days: data.duration_days ? parseInt(data.duration_days) : null,
3324 duration_nights: data.duration_nights
3325 ? parseInt(data.duration_nights)
3326 : null,
3327 available_from: data.available_from || null,
3328 available_to: data.available_to || null,
3329 booking_window_days: data.booking_window_days
3330 ? parseInt(data.booking_window_days)
3331 : null,
3332 seasonal_availability: data.seasonal_availability || "",
3333 best_season: data.best_season.trim(),
3334 peak_season: data.peak_season.trim(),
3335 off_season: data.off_season.trim(),
3336 activity_types: data.activity_types || [], // Array of activity IDs
3337 difficulty_level: parseInt(data.difficulty_level) || null,
3338 trip_category: (() => {
3339 const rawCategories = data.trip_category || [];
3340
3341 // If no trip categories available yet, don't filter - send raw data
3342 if (tripCategories.length === 0) {
3343 return rawCategories;
3344 }
3345
3346 // Filter valid categories - save exactly what user selects
3347 const validCategories = rawCategories.filter((_catId) => {
3348 // Always return true - save all selected categories regardless of availability
3349 return true;
3350 });
3351
3352 return validCategories;
3353 })(),
3354 tags: data.tags || [],
3355 accommodation_type: data.accommodation_type || "",
3356 meal_plan: data.meal_plan || "",
3357 accommodation_details: data.accommodation_details.trim(),
3358 transportation_included: data.transportation_included || false,
3359 pickup_location: data.pickup_location.trim(),
3360 dropoff_location: data.dropoff_location.trim(),
3361 transportation_details: data.transportation_details.trim(),
3362 pricing_type: data.pricing_type,
3363 original_price:
3364 data.pricing_type === "regular"
3365 ? data.original_price
3366 ? parseFloat(data.original_price)
3367 : null
3368 : null,
3369 discounted_price:
3370 data.pricing_type === "regular"
3371 ? data.discounted_price
3372 ? parseFloat(data.discounted_price)
3373 : null
3374 : null,
3375 price_types:
3376 data.pricing_type === "traveler_based"
3377 ? data.price_types.map((pt) => ({
3378 category_id: pt.category_id,
3379 is_default: Boolean((pt as any).is_default),
3380 original_price: pt.original_price
3381 ? parseFloat(pt.original_price)
3382 : null,
3383 discounted_price: pt.discounted_price
3384 ? parseFloat(pt.discounted_price)
3385 : null,
3386 }))
3387 : [],
3388 deposit_amount: data.deposit_amount
3389 ? parseFloat(data.deposit_amount)
3390 : null,
3391 deposit_percentage: data.deposit_percentage
3392 ? parseFloat(data.deposit_percentage)
3393 : null,
3394 payment_terms: data.payment_terms.trim(),
3395 max_travelers: data.max_travelers ? parseInt(data.max_travelers) : null,
3396 min_travelers: data.min_travelers ? parseInt(data.min_travelers) : null,
3397 booking_deadline_hours: data.booking_deadline_hours || null,
3398 cancellation_policy: data.cancellation_policy || "",
3399 custom_fields: { disable_booking: data.disable_booking },
3400 age_min: data.age_min ? parseInt(data.age_min) : null,
3401 age_max: data.age_max ? parseInt(data.age_max) : null,
3402 physical_requirements: data.physical_requirements.trim(),
3403 visa_requirements: data.visa_requirements.trim(),
3404 vaccination_requirements: data.vaccination_requirements.trim(),
3405 has_default_time_slots: data.has_default_time_slots || false,
3406 default_time_slots: JSON.stringify(data.default_time_slots || []),
3407 departure_time: data.departure_time || "09:00",
3408 included_items: (data.included_items || [])
3409 .map((item) => ({
3410 title: item.title?.trim() || "",
3411 description: item.description?.trim() || "",
3412 }))
3413 .filter((item) => item.title),
3414 excluded_items: (data.excluded_items || [])
3415 .map((item) => ({
3416 title: item.title?.trim() || "",
3417 description: item.description?.trim() || "",
3418 }))
3419 .filter((item) => item.title),
3420 itinerary_days: data.itinerary_days || [],
3421 gallery_images: data.gallery_images || [],
3422 featured_image: data.featured_image ?? null,
3423 faqs: data.faqs || [],
3424 frontend_tabs: data.frontend_tabs.map((tab) => ({
3425 id: tab.id,
3426 label: tab.label,
3427 enabled: tab.enabled,
3428 order: tab.order,
3429 content_type: tab.content_type,
3430 custom_content: tab.custom_content || null,
3431 icon: tab.icon || null,
3432 })),
3433 availability_dates: data.availability_dates.map((avail) => ({
3434 id: avail.id,
3435 departure_date: avail.departure_date || null,
3436 arrival_date: avail.arrival_date || null,
3437 seats_remaining: avail.seats_remaining || null,
3438 original_price: avail.original_price
3439 ? parseFloat(avail.original_price)
3440 : null,
3441 discounted_price: avail.discounted_price
3442 ? parseFloat(avail.discounted_price)
3443 : null,
3444 discount_percentage: avail.discount_percentage
3445 ? parseFloat(avail.discount_percentage)
3446 : null,
3447 status: avail.status || "available",
3448 from_location: avail.from_location || null,
3449 to_location: avail.to_location || null,
3450 from_latitude: avail.from_latitude || null,
3451 from_longitude: avail.from_longitude || null,
3452 to_latitude: avail.to_latitude || null,
3453 to_longitude: avail.to_longitude || null,
3454 })),
3455 status: data.status === "publish" ? "publish" : "draft",
3456 scheduled_publish_date: data.scheduled_publish_date || null,
3457 scheduled_unpublish_date: data.scheduled_unpublish_date || null,
3458 version: data.version || 1,
3459 seasonal_auto_enable: data.seasonal_auto_enable || false,
3460 seasonal_enable_date: data.seasonal_enable_date || null,
3461 seasonal_disable_date: data.seasonal_disable_date || null,
3462 meta_title: data.meta_title || "",
3463 meta_description: data.meta_description || "",
3464 meta_keywords: data.meta_keywords || "",
3465 attributes: data.attributes || {},
3466 featured_priority: data.featured_priority,
3467 };
3468
3469 if (showDownloadsUI) {
3470 (payload as any).downloadable_items = (data.downloadable_items || [])
3471 .map((item, idx) => ({
3472 id: item.id ?? null,
3473 title: (item.title || "").trim(),
3474 description: item.description || "",
3475 attachment_id: item.attachment_id ?? null,
3476 visibility: item.visibility || "booked_only",
3477 enabled: item.enabled !== false,
3478 sort_order: item.sort_order != null ? item.sort_order : idx + 1,
3479 attachment_url: item.attachment_url || "",
3480 attachment_title: item.attachment_title || "",
3481
3482 // Prefixed keys (requested)
3483 download_title: (item.title || "").trim(),
3484 download_description: item.description || "",
3485 download_visibility: item.visibility || "booked_only",
3486 download_enabled: item.enabled !== false,
3487 download_file: item.attachment_id ?? null,
3488 }))
3489 .filter((item) => item.title);
3490 }
3491
3492 if (isEditMode && tripId) {
3493 const response = await apiClient.put(`/trips/${tripId}`, payload);
3494 return response.data || response;
3495 } else {
3496 const response = await apiClient.post("/trips", payload);
3497 return response.data || response;
3498 }
3499 },
3500 onSuccess: (data, variables) => {
3501 queryClient.invalidateQueries({ queryKey: ["trips"] });
3502 queryClient.invalidateQueries({ queryKey: ["trip", tripId] });
3503 // Force refetch the trip data immediately
3504 queryClient.refetchQueries({ queryKey: ["trip", tripId] });
3505 setIsSubmitting(false);
3506
3507 // Show success message - different for edit vs create mode
3508 let successMessage: string;
3509 if (isEditMode) {
3510 // Edit mode: updating existing trip
3511 successMessage =
3512 variables.status === "publish"
3513 ? __("Trip updated and published successfully", "yatra")
3514 : __("Trip updated successfully", "yatra");
3515 } else {
3516 // Create mode: creating new trip
3517 successMessage =
3518 variables.status === "publish"
3519 ? __("Trip created and published successfully", "yatra")
3520 : __("Trip saved as draft successfully", "yatra");
3521 }
3522 showToast(successMessage, "success");
3523
3524 // Only redirect when creating a NEW trip - stay on page when editing
3525 if (!isEditMode && data?.id) {
3526 // If creating new trip, redirect to edit mode so user can continue editing
3527 setTimeout(() => {
3528 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${data.id}`;
3529 }, 1000);
3530 }
3531 // When editing (isEditMode), always stay on the same page - no redirect
3532 },
3533 onError: (error: any) => {
3534 const errorMessage =
3535 error?.response?.data?.message ||
3536 error?.message ||
3537 __("An error occurred while saving", "yatra");
3538 showToast(errorMessage, "error");
3539 setErrors({ submit: errorMessage });
3540 setIsSubmitting(false);
3541 },
3542 });
3543
3544 // Light validation for draft saves (only essential fields)
3545 const validateDraft = (): boolean => {
3546 const newErrors = buildEssentialFieldErrors();
3547 setErrors(newErrors);
3548 return Object.keys(newErrors).length === 0;
3549 };
3550
3551 const handleSaveDraft = async () => {
3552 if (!validateDraft()) {
3553 showToast(
3554 __("Please add a trip title and URL before saving.", "yatra"),
3555 "error",
3556 );
3557 return;
3558 }
3559 setIsSubmitting(true);
3560 saveMutation.mutate({ ...formData, status: "draft" });
3561 };
3562
3563 // eslint-disable-next-line react-hooks/exhaustive-deps
3564 const handlePublish = async () => {
3565 if (!validateForm()) {
3566 const firstError = Object.keys(errors)[0];
3567 if (firstError) {
3568 showToast(
3569 __("Trip title and slug are required before publishing.", "yatra"),
3570 "error",
3571 );
3572 const errorElement = document.querySelector(
3573 `[name="${firstError}"], #${firstError}`,
3574 );
3575 if (errorElement) {
3576 errorElement.scrollIntoView({ behavior: "smooth", block: "center" });
3577 }
3578 }
3579 return;
3580 }
3581 setIsSubmitting(true);
3582 saveMutation.mutate({ ...formData, status: "publish" });
3583 };
3584
3585 // Enter should trigger publish/update (skip multiline and interactive controls)
3586 useEffect(() => {
3587 const onKeyDown = (e: KeyboardEvent) => {
3588 if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
3589 return;
3590
3591 const target = e.target as HTMLElement | null;
3592 if (!target) return;
3593
3594 const tag = target.tagName.toLowerCase();
3595 const isTextarea = tag === "textarea";
3596 const isButton = tag === "button";
3597 const isLink = tag === "a";
3598 const isContentEditable =
3599 target.getAttribute("contenteditable") === "true";
3600
3601 // Ignore Enter in multiline or interactive elements
3602 if (isTextarea || isButton || isLink || isContentEditable) return;
3603
3604 const inputType = (target as HTMLInputElement).type;
3605 if (["submit", "button", "file"].includes(inputType)) return;
3606
3607 e.preventDefault();
3608 if (!isSubmitting) {
3609 handlePublish();
3610 }
3611 };
3612
3613 window.addEventListener("keydown", onKeyDown);
3614 return () => window.removeEventListener("keydown", onKeyDown);
3615 }, [isSubmitting, handlePublish]);
3616
3617 // Using static revisions data for UI only
3618 const revisions = dummyRevisions;
3619 const isLoadingRevisions = false;
3620
3621 const handlePreview = async () => {
3622 const slug = (formData.slug || "").trim();
3623 if (!slug) {
3624 showToast(
3625 __(
3626 "Trip slug is missing. Please add a slug before previewing.",
3627 "yatra",
3628 ),
3629 "error",
3630 );
3631 return;
3632 }
3633
3634 const siteUrl = (window as any)?.yatraAdmin?.siteUrl || "";
3635 const { plainUrl, prettyUrl } = buildYatraSinglePublicUrls({
3636 entity: "trip",
3637 slug,
3638 siteUrl,
3639 bases: settingsData as Record<string, unknown> | null,
3640 });
3641
3642 if (isWordPressPlainPermalink()) {
3643 window.open(plainUrl, "_blank", "noopener,noreferrer");
3644 return;
3645 }
3646
3647 let apiPermalink = (tripData as any)?.permalink || (tripData as any)?.url;
3648
3649 if (!apiPermalink && tripId) {
3650 try {
3651 const detail = await apiClient.get(`/trips/${tripId}`);
3652 apiPermalink =
3653 (detail as any)?.permalink || (detail as any)?.url || apiPermalink;
3654 } catch (error) {
3655 // fall through to prettyUrl
3656 }
3657 }
3658
3659 if (apiPermalink) {
3660 window.open(apiPermalink, "_blank", "noopener,noreferrer");
3661 return;
3662 }
3663
3664 window.open(prettyUrl, "_blank", "noopener,noreferrer");
3665 };
3666
3667 const handleRevisionClick = (revisionId: number) => {
3668 setSelectedRevisionId(revisionId);
3669 setShowRevisionConfirm(true);
3670 };
3671
3672 const handleRevisionConfirm = () => {
3673 // UI only - no actual functionality
3674 if (selectedRevisionId) {
3675 showToast(__("Revision feature is coming soon", "yatra"), "info");
3676 }
3677 setShowRevisionConfirm(false);
3678 setShowRevisionsDialog(false);
3679 setSelectedRevisionId(null);
3680 };
3681
3682 // Skeleton loader for edit mode
3683 if (isEditMode && isLoadingTrip) {
3684 return (
3685 <div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden">
3686 {/* Header Skeleton */}
3687 <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">
3688 <div className="flex items-center gap-4 min-w-0 flex-1">
3689 <div className="min-w-0">
3690 <div className="h-6 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" />
3691 <div className="flex items-center gap-3">
3692 <div className="h-5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3693 <div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
3694 </div>
3695 </div>
3696 </div>
3697 <div className="flex items-center gap-2">
3698 <div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3699 <div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3700 <div className="h-9 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3701 <div className="h-9 w-36 bg-blue-200 dark:bg-blue-800 rounded animate-pulse" />
3702 </div>
3703 </div>
3704
3705 {/* Navigation Bar Skeleton */}
3706 <div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
3707 <div className="flex items-center justify-between gap-4">
3708 <div className="h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3709 <div className="flex-1 flex flex-col items-center gap-1 max-w-md mx-4">
3710 <div className="h-3 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3711 <div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
3712 <div className="h-2.5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3713 </div>
3714 <div className="h-9 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3715 </div>
3716 </div>
3717
3718 {/* Main Content Skeleton */}
3719 <div className="flex-1 flex overflow-hidden">
3720 {/* Sidebar Skeleton */}
3721 <div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 overflow-y-auto">
3722 <div className="p-5 pb-8 space-y-6">
3723 {/* Essentials Section Skeleton */}
3724 <div>
3725 <div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" />
3726 <div className="space-y-0.5">
3727 {[...Array(8)].map((_, i) => (
3728 <div
3729 key={`essentials-${i}`}
3730 className="flex items-center gap-2 px-3 py-2.5 rounded-md"
3731 >
3732 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3733 <div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3734 </div>
3735 ))}
3736 </div>
3737 </div>
3738
3739 {/* Marketing Section Skeleton */}
3740 <div>
3741 <div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3 px-1" />
3742 <div className="space-y-0.5">
3743 {[...Array(9)].map((_, i) => (
3744 <div
3745 key={`marketing-${i}`}
3746 className="flex items-center gap-2 px-3 py-2.5 rounded-md"
3747 >
3748 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3749 <div className="h-4 flex-1 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3750 </div>
3751 ))}
3752 </div>
3753 </div>
3754
3755 {/* Lifecycle Section Skeleton */}
3756 <div>
3757 <div className="flex items-center gap-2 px-3 py-2.5 rounded-md">
3758 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse flex-shrink-0" />
3759 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3760 </div>
3761 </div>
3762 </div>
3763 </div>
3764
3765 {/* Main Form Area Skeleton */}
3766 <div className="flex-1 overflow-y-auto p-6">
3767 <div className="max-w-4xl mx-auto space-y-6">
3768 {/* Section Header Skeleton */}
3769 <div className="flex items-center gap-2 mb-4">
3770 <div className="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3771 <div className="h-6 w-56 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3772 </div>
3773 <div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-6" />
3774
3775 {/* Form Fields Skeleton */}
3776 <Card>
3777 <CardContent className="p-6 space-y-6">
3778 {/* Title Field */}
3779 <div className="space-y-2">
3780 <div className="h-3.5 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3781 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3782 </div>
3783
3784 {/* Slug Field */}
3785 <div className="space-y-2">
3786 <div className="h-3.5 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3787 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3788 </div>
3789
3790 {/* Description Field */}
3791 <div className="space-y-2">
3792 <div className="h-3.5 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3793 <div className="h-32 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3794 </div>
3795
3796 {/* Featured Image Skeleton */}
3797 <div className="space-y-2">
3798 <div className="h-3.5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3799 <div className="h-48 w-full bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
3800 </div>
3801
3802 {/* Grid Fields Skeleton */}
3803 <div className="grid grid-cols-2 gap-4">
3804 {[...Array(4)].map((_, i) => (
3805 <div key={`grid-${i}`} className="space-y-3">
3806 <div className="h-3.5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3807 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
3808 </div>
3809 ))}
3810 </div>
3811 </CardContent>
3812 </Card>
3813 </div>
3814 </div>
3815 </div>
3816 </div>
3817 );
3818 }
3819
3820 if (isEditMode && tripError) {
3821 const requestInfo =
3822 tripErrorContext.requestInfo || (tripError as any)?.config || {};
3823 const method = (requestInfo.method || "GET").toUpperCase?.() || "GET";
3824 const url = requestInfo.url || (tripError as any)?.config?.url || "";
3825 const payload =
3826 requestInfo.payload || (tripError as any)?.config?.data || "";
3827 const composedErrorText = `Method: ${method}\nURL: ${url}\nPayload: ${payload || "N/A"}\n\n${tripErrorContext.details || ""}`;
3828
3829 return (
3830 <div className="min-h-screen flex items-center justify-center bg-white">
3831 <div className="w-full max-w-5xl px-6 py-12">
3832 <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">
3833 <div className="w-16 h-16 rounded-2xl bg-white border border-[#ffdede] flex items-center justify-center">
3834 <svg
3835 className="w-10 h-10 text-red-500"
3836 fill="none"
3837 stroke="currentColor"
3838 viewBox="0 0 24 24"
3839 strokeWidth="1.8"
3840 >
3841 <path
3842 strokeLinecap="round"
3843 strokeLinejoin="round"
3844 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"
3845 />
3846 </svg>
3847 </div>
3848 <div className="space-y-1">
3849 <h2 className="text-2xl font-semibold text-gray-900">
3850 {__("Error Loading Trips", "yatra")}
3851 </h2>
3852 <p className="text-sm text-gray-600">
3853 {__(
3854 "We couldn’t connect to the trips service. Please refresh or try again shortly.",
3855 "yatra",
3856 )}
3857 </p>
3858 </div>
3859 <div className="flex flex-wrap gap-2 justify-center">
3860 <Button
3861 onClick={() =>
3862 queryClient.invalidateQueries({ queryKey: ["trip", tripId] })
3863 }
3864 >
3865 {__("Try again", "yatra")}
3866 </Button>
3867 <Button
3868 variant="outline"
3869 onClick={() => {
3870 if (typeof window !== "undefined") {
3871 window.open(
3872 "https://wordpress.org/support/plugin/yatra",
3873 "_blank",
3874 );
3875 }
3876 }}
3877 >
3878 {__("Visit support center", "yatra")}
3879 </Button>
3880 </div>
3881
3882 <div className="w-full mt-10">
3883 <div className="relative w-full text-left rounded-2xl border border-gray-200 bg-white shadow-sm space-y-0">
3884 <div className="flex items-center justify-between px-5 py-3 border-b border-gray-200">
3885 <p className="text-sm font-medium text-gray-800">
3886 {__("Technical details", "yatra")}
3887 </p>
3888 <Button
3889 type="button"
3890 size="sm"
3891 variant="ghost"
3892 className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
3893 onClick={() =>
3894 copyErrorDetailsToClipboard(composedErrorText)
3895 }
3896 >
3897 {copiedErrorDetails ? (
3898 <>
3899 <Check className="w-4 h-4" />
3900 {__("Copied", "yatra")}
3901 </>
3902 ) : (
3903 <>
3904 <Copy className="w-4 h-4" />
3905 {__("Copy details", "yatra")}
3906 </>
3907 )}
3908 </Button>
3909 </div>
3910 <div className="px-5 py-3 border-b border-gray-200 space-y-2 text-sm text-left text-gray-800">
3911 <div>
3912 <span className="font-medium">
3913 {__("Method:", "yatra")}
3914 </span>{" "}
3915 <span className="font-mono">{method}</span>
3916 </div>
3917 <div className="break-all">
3918 <span className="font-medium">{__("URL:", "yatra")}</span>{" "}
3919 <span className="font-mono">
3920 {url || __("N/A", "yatra")}
3921 </span>
3922 </div>
3923 {payload && (
3924 <div>
3925 <span className="font-medium block mb-1">
3926 {__("Payload:", "yatra")}
3927 </span>
3928 <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">
3929 {payload}
3930 </pre>
3931 </div>
3932 )}
3933 </div>
3934 <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">
3935 {tripErrorContext.details ||
3936 JSON.stringify(
3937 {
3938 message:
3939 tripError instanceof Error
3940 ? tripError.message
3941 : __("Failed to load trip data", "yatra"),
3942 method,
3943 url,
3944 payload,
3945 },
3946 null,
3947 2,
3948 )}
3949 </pre>
3950 </div>
3951 </div>
3952 </div>
3953 </div>
3954 </div>
3955 );
3956 }
3957
3958 const renderSectionContent = () => {
3959 switch (currentSection) {
3960 case "basic":
3961 return (
3962 <div className="space-y-6">
3963 <div className="flex items-center gap-2 mb-3">
3964 <FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
3965 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
3966 {__("Basic Information", "yatra")}
3967 </h2>
3968 <Badge
3969 variant="outline"
3970 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"
3971 >
3972 {__("Start Here", "yatra")}
3973 </Badge>
3974 </div>
3975 <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">
3976 <p className="text-sm text-blue-900 dark:text-blue-100 font-medium mb-1">
3977 {__("💡 Getting Started", "yatra")}
3978 </p>
3979 <p className="text-xs text-blue-800 dark:text-blue-200">
3980 {__(
3981 "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.",
3982 "yatra",
3983 )}
3984 </p>
3985 </div>
3986
3987 {/* Essential Fields - Single View (No Tabs) */}
3988 <div className="space-y-6">
3989 {/* Tour Title */}
3990 <div className="mb-4">
3991 <div className="flex items-center justify-between mb-1.5">
3992 <label
3993 htmlFor="title"
3994 className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
3995 >
3996 {__("Trip Title", "yatra")}
3997 <Badge
3998 variant="outline"
3999 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"
4000 >
4001 {__("Required", "yatra")}
4002 </Badge>
4003 <button
4004 type="button"
4005 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4006 title={__(
4007 "A catchy title that describes your trip. Recommended: 50-60 characters for best SEO results.",
4008 "yatra",
4009 )}
4010 >
4011 <HelpCircle className="w-3.5 h-3.5" />
4012 </button>
4013 </label>
4014 <span
4015 className={`text-xs font-medium ${
4016 formData.title.length > 60
4017 ? "text-red-600 dark:text-red-400"
4018 : formData.title.length >= 50
4019 ? "text-green-600 dark:text-green-400"
4020 : "text-gray-500 dark:text-gray-400"
4021 }`}
4022 >
4023 {formData.title.length}/60
4024 </span>
4025 </div>
4026 <HelpText
4027 text={__(
4028 '💡 Tip: Include your destination and trip length. Example: "Bali Beach Retreat - 7 Days" or "Paris City Tour - Half Day"',
4029 "yatra",
4030 )}
4031 className="mb-2"
4032 />
4033 <Input
4034 id="title"
4035 type="text"
4036 value={formData.title}
4037 onChange={(e) => handleTitleChange(e.target.value)}
4038 placeholder={__("e.g., Bali Beach Retreat - 7 Days", "yatra")}
4039 maxLength={100}
4040 className={`${errors.title ? "border-red-500" : formData.title && formData.title.length <= 60 ? "border-green-500" : ""} transition-colors`}
4041 required
4042 />
4043 {errors.title ? (
4044 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4045 <AlertCircle className="w-4 h-4" />
4046 {errors.title}
4047 </p>
4048 ) : (
4049 formData.title &&
4050 formData.title.length > 60 && (
4051 <p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
4052 <AlertCircle className="w-3.5 h-3.5" />
4053 {__(
4054 "Title is longer than recommended for SEO (60 characters)",
4055 "yatra",
4056 )}
4057 </p>
4058 )
4059 )}
4060 </div>
4061
4062 {/* URL Slug */}
4063 <div className="mb-4">
4064 <div className="flex items-center justify-between mb-1.5">
4065 <label
4066 htmlFor="slug"
4067 className="block text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
4068 >
4069 {__("Trip URL", "yatra")}
4070 <Badge
4071 variant="outline"
4072 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"
4073 >
4074 {__("Required", "yatra")}
4075 </Badge>
4076 <button
4077 type="button"
4078 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4079 title={__(
4080 "The web address for your trip page. Auto-generated from your title, but you can customize it.",
4081 "yatra",
4082 )}
4083 >
4084 <HelpCircle className="w-3.5 h-3.5" />
4085 </button>
4086 </label>
4087 {showSlugPreview && formData.slug && (
4088 <button
4089 type="button"
4090 onClick={() => setShowSlugPreview(false)}
4091 className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
4092 >
4093 {__("Hide", "yatra")}
4094 </button>
4095 )}
4096 </div>
4097 <HelpText
4098 text={__(
4099 "💡 This is automatically created from your title. You usually don't need to change it unless you want a custom web address.",
4100 "yatra",
4101 )}
4102 className="mb-2"
4103 />
4104 <Input
4105 id="slug"
4106 type="text"
4107 value={formData.slug}
4108 onChange={(e) => handleFieldChange("slug", e.target.value)}
4109 placeholder={__("bali-beach-retreat-7-days", "yatra")}
4110 className={`font-mono text-sm ${errors.slug ? "border-red-500" : ""}`}
4111 required
4112 />
4113 {errors.slug && (
4114 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4115 <AlertCircle className="w-4 h-4" />
4116 {errors.slug}
4117 </p>
4118 )}
4119 {showSlugPreview && formData.slug && !errors.slug && (
4120 <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">
4121 <div>
4122 <span className="text-gray-500 dark:text-gray-400">
4123 {__("Preview URL:", "yatra")}{" "}
4124 </span>
4125 <span className="font-mono text-blue-600 dark:text-blue-400">
4126 {(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/
4127 {settingsData?.trip_base || "trip"}/{formData.slug}
4128 </span>
4129 </div>
4130 <button
4131 type="button"
4132 onClick={() => {
4133 const tripBase = settingsData?.trip_base || "trip";
4134 const url = `${(window as any).yatraAdmin?.siteUrl || "yoursite.com"}/${tripBase}/${formData.slug}`;
4135 navigator.clipboard.writeText(url);
4136 showToast(
4137 __("URL copied to clipboard", "yatra"),
4138 "success",
4139 );
4140 }}
4141 className="ml-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
4142 title={__("Copy URL", "yatra")}
4143 >
4144 <Copy className="w-3.5 h-3.5" />
4145 </button>
4146 </div>
4147 )}
4148 {!showSlugPreview && formData.slug && (
4149 <button
4150 type="button"
4151 onClick={() => setShowSlugPreview(true)}
4152 className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
4153 >
4154 {__("Show URL Preview", "yatra")}
4155 </button>
4156 )}
4157 </div>
4158
4159 {/* Short Description */}
4160 <div className="mb-4">
4161 <div className="flex items-center justify-between mb-1.5">
4162 <label className="text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
4163 {__("Short Description", "yatra")}
4164 <Badge
4165 variant="outline"
4166 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"
4167 >
4168 {__("Recommended", "yatra")}
4169 </Badge>
4170 <button
4171 type="button"
4172 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
4173 title={__(
4174 "A brief summary that appears in listings. Recommended: 100-150 characters for best results.",
4175 "yatra",
4176 )}
4177 >
4178 <HelpCircle className="w-3.5 h-3.5" />
4179 </button>
4180 </label>
4181 <div className="flex items-center gap-2">
4182 <span
4183 className={`text-xs font-medium ${
4184 formData.short_description.length > 200
4185 ? "text-red-600 dark:text-red-400"
4186 : formData.short_description.length >= 100 &&
4187 formData.short_description.length <= 150
4188 ? "text-green-600 dark:text-green-400"
4189 : formData.short_description.length > 0
4190 ? "text-yellow-600 dark:text-yellow-400"
4191 : "text-gray-500 dark:text-gray-400"
4192 }`}
4193 >
4194 {formData.short_description.length}/200
4195 </span>
4196 <AiFieldAffordance
4197 task="trip-short-description"
4198 label={__("Short Description", "yatra")}
4199 value={formData.short_description}
4200 onAccept={(v) =>
4201 handleFieldChange("short_description", v)
4202 }
4203 buildContext={() => buildTripAiContext(formData)}
4204 />
4205 </div>
4206 </div>
4207 <HelpText
4208 text={__(
4209 '💡 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."',
4210 "yatra",
4211 )}
4212 className="mb-2"
4213 />
4214 <RichTextEditor
4215 value={formData.short_description}
4216 onChange={(value) =>
4217 handleFieldChange("short_description", value)
4218 }
4219 placeholder={__(
4220 "Escape to paradise with our 7-day Bali beach retreat...",
4221 "yatra",
4222 )}
4223 minHeight={120}
4224 />
4225 {formData.short_description.length > 0 &&
4226 formData.short_description.length < 100 && (
4227 <p className="mt-1.5 text-xs text-yellow-600 dark:text-yellow-400 flex items-center gap-1">
4228 <AlertCircle className="w-3.5 h-3.5" />
4229 {__(
4230 "Consider adding more details (recommended: 100-150 characters)",
4231 "yatra",
4232 )}
4233 </p>
4234 )}
4235 </div>
4236
4237 {/* Tour Description */}
4238 <div className="mb-4">
4239 <div className="flex items-center justify-between mb-1.5">
4240 <label className="text-xs font-normal text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
4241 {__("Trip Description", "yatra")}
4242 <Badge
4243 variant="outline"
4244 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"
4245 >
4246 {__("Recommended", "yatra")}
4247 </Badge>
4248 </label>
4249 <AiFieldAffordance
4250 task="trip-description"
4251 label={__("Description", "yatra")}
4252 value={formData.description}
4253 onAccept={(v) => handleFieldChange("description", v)}
4254 buildContext={() => buildTripAiContext(formData)}
4255 />
4256 </div>
4257 <HelpText
4258 text={__(
4259 "💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!",
4260 "💡 Tell travelers what makes your trip special! Describe the experience, highlights, and what they'll see. Write 2-4 paragraphs. Be enthusiastic and detailed!",
4261 )}
4262 className="mb-2"
4263 />
4264 <RichTextEditor
4265 value={formData.description}
4266 onChange={(value) => handleFieldChange("description", value)}
4267 placeholder={__(
4268 "Escape to paradise with our 7-day Bali beach retreat... Or describe your single day trip experience...",
4269 "yatra",
4270 )}
4271 minHeight={260}
4272 />
4273 {errors.description && (
4274 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4275 <AlertCircle className="w-4 h-4" />
4276 {errors.description}
4277 </p>
4278 )}
4279 </div>
4280
4281 {/* Featured Image */}
4282 <div className="mb-6">
4283 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5">
4284 {__("Featured Image", "yatra")}
4285 <Badge
4286 variant="outline"
4287 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"
4288 >
4289 {__("Recommended", "yatra")}
4290 </Badge>
4291 <button
4292 type="button"
4293 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-auto"
4294 title={__(
4295 "The main image that represents your trip. This appears in listings and as the hero image on the trip page.",
4296 "yatra",
4297 )}
4298 >
4299 <HelpCircle className="w-3.5 h-3.5" />
4300 </button>
4301 </label>
4302 <HelpText
4303 text={__(
4304 "💡 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.",
4305 "yatra",
4306 )}
4307 className="mb-2"
4308 />
4309 {formData.featured_image ? (
4310 <div className="relative group">
4311 <div
4312 className={`aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden border-2 ${
4313 errors.featured_image
4314 ? "border-red-500 dark:border-red-600"
4315 : "border-gray-200 dark:border-gray-700"
4316 }`}
4317 >
4318 {featuredImagePreview ? (
4319 <img
4320 src={featuredImagePreview}
4321 alt={__("Featured Image", "yatra")}
4322 className="w-full h-full object-cover"
4323 />
4324 ) : (
4325 <div className="flex items-center justify-center w-full h-full text-sm text-gray-500 dark:text-gray-400">
4326 {isResolvingFeaturedImage
4327 ? __("Loading image...", "yatra")
4328 : __("Preview unavailable", "yatra")}
4329 </div>
4330 )}
4331 </div>
4332 <button
4333 type="button"
4334 onClick={() => handleFieldChange("featured_image", null)}
4335 className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1.5 opacity-0 group-hover:opacity-100 transition-opacity"
4336 title={__("Remove image", "yatra")}
4337 >
4338 <X className="w-4 h-4" />
4339 </button>
4340 </div>
4341 ) : (
4342 <button
4343 type="button"
4344 onClick={() => {
4345 // Use WordPress media library
4346 if (window.wp && window.wp.media) {
4347 const mediaUploader = window.wp.media({
4348 title: __("Select Featured Image", "yatra"),
4349 button: { text: __("Use this image", "yatra") },
4350 multiple: false,
4351 });
4352 mediaUploader.on("select", () => {
4353 const attachment = mediaUploader
4354 .state()
4355 .get("selection")
4356 .first()
4357 .toJSON();
4358 handleFieldChange("featured_image", attachment.id);
4359 });
4360 prepareWordPressMediaFrameOpen();
4361 mediaUploader.open();
4362 }
4363 }}
4364 className={`w-full aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${
4365 errors.featured_image
4366 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4367 : "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400"
4368 }`}
4369 >
4370 <Upload
4371 className={`w-10 h-10 mb-2 ${errors.featured_image ? "text-red-400" : "text-gray-400"}`}
4372 />
4373 <span
4374 className={`text-sm font-medium ${errors.featured_image ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`}
4375 >
4376 {__("Upload Featured Image", "yatra")}
4377 </span>
4378 <span
4379 className={`text-xs mt-1 ${errors.featured_image ? "text-red-500 dark:text-red-500" : "text-gray-500 dark:text-gray-500"}`}
4380 >
4381 {__("Recommended: 1200x800px", "yatra")}
4382 </span>
4383 </button>
4384 )}
4385 {errors.featured_image && (
4386 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4387 <AlertCircle className="w-4 h-4" />
4388 {errors.featured_image}
4389 </p>
4390 )}
4391 </div>
4392
4393 {/* Trip Highlights */}
4394 <Card className="mt-6">
4395 <CardHeader>
4396 <div className="flex items-start justify-between gap-2">
4397 <div className="flex-1">
4398 <CardTitle>{__("Trip Highlights", "yatra")}</CardTitle>
4399 <CardDescription>
4400 {__(
4401 "Add key highlights that make your trip special. These will be displayed prominently on your trip page.",
4402 "yatra",
4403 )}
4404 </CardDescription>
4405 </div>
4406 {/* AI: generates 5-7 highlight strings. The
4407 affordance hands back a multi-line string;
4408 we split into the highlights array shape
4409 TripForm stores. */}
4410 <AiFieldAffordance
4411 task="trip-highlights"
4412 label={__("Highlights", "yatra")}
4413 value={(formData.highlights ?? []).join("\n")}
4414 onAccept={(raw) => {
4415 const list = raw
4416 .split(/\r?\n/)
4417 .map((l) => l.replace(/^[\s\-\*•·●]+/, "").trim())
4418 .filter((l) => l !== "");
4419 handleFieldChange("highlights", list as any);
4420 }}
4421 buildContext={() => buildTripAiContext(formData)}
4422 />
4423 </div>
4424 </CardHeader>
4425 <CardContent className="space-y-3">
4426 {formData.highlights.length > 0 ? (
4427 <div className="space-y-2">
4428 {formData.highlights.map((highlight, index) => (
4429 <div
4430 key={index}
4431 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"
4432 >
4433 <CheckCircle2 className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
4434 <span className="flex-1 text-sm text-gray-900 dark:text-white">
4435 {highlight}
4436 </span>
4437 <Button
4438 type="button"
4439 variant="ghost"
4440 size="sm"
4441 onClick={() => handleHighlightRemove(index)}
4442 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
4443 >
4444 <X className="w-4 h-4" />
4445 </Button>
4446 </div>
4447 ))}
4448 </div>
4449 ) : (
4450 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
4451 <Sparkles className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
4452 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
4453 {__("No highlights added yet", "yatra")}
4454 </p>
4455 <p className="text-xs text-gray-500 dark:text-gray-500 mb-4">
4456 {__(
4457 'Add key selling points like "Private guide", "All meals included", or "Skip-the-line tickets"',
4458 "yatra",
4459 )}
4460 </p>
4461 </div>
4462 )}
4463 <Button
4464 type="button"
4465 variant="outline"
4466 onClick={handleHighlightAdd}
4467 className="w-full"
4468 >
4469 <Plus className="w-4 h-4 mr-2" />
4470 {__("Add Highlight", "yatra")}
4471 </Button>
4472 </CardContent>
4473 </Card>
4474
4475 {/* Trip Type & Duration - Moved from Duration section */}
4476 <div className="mt-6 space-y-4">
4477 <div className="border-t border-gray-200 dark:border-gray-700 pt-6">
4478 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
4479 {__("Trip Type & Duration", "yatra")}
4480 </h3>
4481
4482 {/* Trip Type */}
4483 <div className="mb-4">
4484 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3">
4485 {__("Trip Type", "yatra")}
4486 </label>
4487 <div
4488 className={`grid grid-cols-1 md:grid-cols-2 gap-4 ${
4489 errors.trip_type ? "mb-2" : ""
4490 }`}
4491 >
4492 <label
4493 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
4494 formData.trip_type === "single_day"
4495 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
4496 : errors.trip_type
4497 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4498 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
4499 }`}
4500 >
4501 <input
4502 type="radio"
4503 name="trip_type"
4504 value="single_day"
4505 checked={formData.trip_type === "single_day"}
4506 onChange={(e) => {
4507 handleFieldChange("trip_type", e.target.value);
4508 if (e.target.value === "single_day") {
4509 setFormData((prev) => ({
4510 ...prev,
4511 duration_days: "1",
4512 duration_nights: "0",
4513 }));
4514 }
4515 }}
4516 className="sr-only"
4517 />
4518 <div className="flex flex-1">
4519 <div className="flex flex-col">
4520 <span
4521 className={`block text-sm font-semibold ${
4522 formData.trip_type === "single_day"
4523 ? "text-blue-900 dark:text-blue-300"
4524 : "text-gray-900 dark:text-white"
4525 }`}
4526 >
4527 {__("Single Day Trip", "yatra")}
4528 </span>
4529 <span
4530 className={`mt-1 flex items-center text-sm ${
4531 formData.trip_type === "single_day"
4532 ? "text-blue-700 dark:text-blue-400"
4533 : "text-gray-500 dark:text-gray-400"
4534 }`}
4535 >
4536 {__(
4537 "Trip completed within one day (no overnight stay)",
4538 "yatra",
4539 )}
4540 </span>
4541 </div>
4542 </div>
4543 {formData.trip_type === "single_day" && (
4544 <div className="absolute top-4 right-4">
4545 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
4546 </div>
4547 )}
4548 </label>
4549
4550 <label
4551 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
4552 formData.trip_type === "multi_day"
4553 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
4554 : errors.trip_type
4555 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
4556 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
4557 }`}
4558 >
4559 <input
4560 type="radio"
4561 name="trip_type"
4562 value="multi_day"
4563 checked={formData.trip_type === "multi_day"}
4564 onChange={(e) => {
4565 handleFieldChange("trip_type", e.target.value);
4566 // A multi-day trip is never hour-based — drop any
4567 // hours set while it was a single-day tour.
4568 setFormData((prev) => ({
4569 ...prev,
4570 duration_hours: "",
4571 }));
4572 }}
4573 className="sr-only"
4574 />
4575 <div className="flex flex-1">
4576 <div className="flex flex-col">
4577 <span
4578 className={`block text-sm font-semibold ${
4579 formData.trip_type === "multi_day"
4580 ? "text-blue-900 dark:text-blue-300"
4581 : "text-gray-900 dark:text-white"
4582 }`}
4583 >
4584 {__("Multi-Day Trip", "yatra")}
4585 </span>
4586 <span
4587 className={`mt-1 flex items-center text-sm ${
4588 formData.trip_type === "multi_day"
4589 ? "text-blue-700 dark:text-blue-400"
4590 : "text-gray-500 dark:text-gray-400"
4591 }`}
4592 >
4593 {__(
4594 "Trip spans multiple days with overnight stays",
4595 "yatra",
4596 )}
4597 </span>
4598 </div>
4599 </div>
4600 {formData.trip_type === "multi_day" && (
4601 <div className="absolute top-4 right-4">
4602 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
4603 </div>
4604 )}
4605 </label>
4606 </div>
4607 {errors.trip_type && (
4608 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4609 <AlertCircle className="w-4 h-4" />
4610 {errors.trip_type}
4611 </p>
4612 )}
4613 </div>
4614
4615 {/* Duration Days & Nights */}
4616 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
4617 <div>
4618 <label
4619 htmlFor="duration_days"
4620 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4621 >
4622 {__("Duration (Days)", "yatra")}
4623 </label>
4624 <Input
4625 id="duration_days"
4626 type="number"
4627 min="1"
4628 value={formData.duration_days}
4629 onChange={(e) => {
4630 const days = e.target.value;
4631 handleFieldChange("duration_days", days);
4632 // Auto-set nights based on trip type
4633 if (formData.trip_type === "single_day") {
4634 setFormData((prev) => ({
4635 ...prev,
4636 duration_nights: "0",
4637 }));
4638 } else if (days && parseInt(days) > 1) {
4639 // For multi-day, typically nights = days - 1
4640 const nights = Math.max(
4641 0,
4642 parseInt(days) - 1,
4643 ).toString();
4644 setFormData((prev) => ({
4645 ...prev,
4646 duration_nights: nights,
4647 }));
4648 }
4649 }}
4650 placeholder={
4651 formData.trip_type === "single_day"
4652 ? "1"
4653 : __("e.g., 7", "yatra")
4654 }
4655 className={errors.duration_days ? "border-red-500" : ""}
4656 disabled={formData.trip_type === "single_day"}
4657 />
4658 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4659 {formData.trip_type === "single_day"
4660 ? __("Single day trips are always 1 day", "yatra")
4661 : __(
4662 "Enter the number of days for your trip",
4663 "yatra",
4664 )}
4665 </p>
4666 {errors.duration_days && (
4667 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4668 <AlertCircle className="w-4 h-4" />
4669 {errors.duration_days}
4670 </p>
4671 )}
4672 </div>
4673 <div>
4674 <label
4675 htmlFor="duration_nights"
4676 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4677 >
4678 {__("Duration (Nights)", "yatra")}
4679 </label>
4680 <Input
4681 id="duration_nights"
4682 type="number"
4683 min="0"
4684 value={formData.duration_nights}
4685 onChange={(e) =>
4686 handleFieldChange("duration_nights", e.target.value)
4687 }
4688 placeholder={
4689 formData.trip_type === "single_day"
4690 ? "0"
4691 : __("e.g., 6", "yatra")
4692 }
4693 className={
4694 errors.duration_nights ? "border-red-500" : ""
4695 }
4696 disabled={formData.trip_type === "single_day"}
4697 />
4698 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4699 {formData.trip_type === "single_day"
4700 ? __(
4701 "Single day trips have 0 nights (no overnight stay)",
4702 "yatra",
4703 )
4704 : __(
4705 "Enter the number of nights (typically days - 1)",
4706 "yatra",
4707 )}
4708 </p>
4709 {errors.duration_nights && (
4710 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4711 <AlertCircle className="w-4 h-4" />
4712 {errors.duration_nights}
4713 </p>
4714 )}
4715 </div>
4716 </div>
4717
4718 {/* Duration (Hours) — hour-based single-day tours */}
4719 {formData.trip_type === "single_day" && (
4720 <div className="mt-4">
4721 <label
4722 htmlFor="duration_hours"
4723 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
4724 >
4725 {__("Duration (Hours)", "yatra")}
4726 </label>
4727 <Input
4728 id="duration_hours"
4729 type="number"
4730 min="1"
4731 max="24"
4732 value={formData.duration_hours}
4733 onChange={(e) =>
4734 handleFieldChange("duration_hours", e.target.value)
4735 }
4736 placeholder={__("e.g., 8", "yatra")}
4737 className={
4738 errors.duration_hours ? "border-red-500" : ""
4739 }
4740 />
4741 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
4742 {__(
4743 "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.",
4744 "yatra",
4745 )}
4746 </p>
4747 {errors.duration_hours && (
4748 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4749 <AlertCircle className="w-4 h-4" />
4750 {errors.duration_hours}
4751 </p>
4752 )}
4753 </div>
4754 )}
4755 </div>
4756 </div>
4757 </div>
4758 </div>
4759 );
4760
4761 case "location":
4762 return (
4763 <div className="space-y-4">
4764 <div className="flex flex-wrap items-center gap-2 mb-2">
4765 <MapPin className="w-5 h-5 text-gray-500" />
4766 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
4767 {__("Location & Geography", "yatra")}
4768 </h2>
4769 <Badge
4770 variant="outline"
4771 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"
4772 >
4773 {__("Recommended", "yatra")}
4774 </Badge>
4775 </div>
4776 <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">
4777 <p className="text-sm font-medium text-amber-900 dark:text-amber-100 mb-1">
4778 {__(
4779 "Specify where your trip takes place, including destinations, coordinates, and key landmarks.",
4780 "yatra",
4781 )}
4782 </p>
4783 <p className="text-xs text-amber-800 dark:text-amber-200">
4784 {__(
4785 "Optional but highly recommended to help travelers understand the experience.",
4786 "yatra",
4787 )}
4788 </p>
4789 </div>
4790
4791 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
4792 {__(
4793 "Optimize how your trip appears in search engines and social shares",
4794 "yatra",
4795 )}
4796 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
4797 {__(
4798 "Optional, but completing these fields improves SEO and click-through rates.",
4799 "yatra",
4800 )}
4801 </span>
4802 </p>
4803 <div className="space-y-4">
4804 {/* Destinations - Multiple Selection */}
4805 <div>
4806 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
4807 {__("Destinations", "yatra")}
4808 </label>
4809 {destinationsData && destinationsData.length > 0 ? (
4810 <MultiSelect
4811 value={formData.destinations.map((id) => id.toString())}
4812 onChange={(values) =>
4813 handleFieldChange(
4814 "destinations",
4815 values.map((v) => Number(v)),
4816 )
4817 }
4818 options={destinationsData.map((destination: any) => ({
4819 value: destination.id.toString(),
4820 label:
4821 destination.name ||
4822 sprintf(
4823 // translators: %s: numeric destination ID, used as a fallback when the destination has no name.
4824 __("Destination #%s", "yatra"),
4825 String(destination.id),
4826 ),
4827 }))}
4828 placeholder={__("Select destinations...", "yatra")}
4829 searchPlaceholder={__("Search destinations...", "yatra")}
4830 error={!!errors.destinations}
4831 />
4832 ) : (
4833 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
4834 <p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
4835 {__(
4836 "No destinations available. Please create destinations first.",
4837 "yatra",
4838 )}
4839 </p>
4840 <div className="space-y-2">
4841 <p className="text-xs text-gray-400 dark:text-gray-500">
4842 {__("To create destinations:", "yatra")}
4843 </p>
4844 <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">
4845 <li>
4846 {__(
4847 "Go to Yatra → Destinations in your WordPress admin",
4848 "yatra",
4849 )}
4850 </li>
4851 <li>{__('Click "Add New Destination"', "yatra")}</li>
4852 <li>
4853 {__("Enter destination name and details", "yatra")}
4854 </li>
4855 <li>{__('Set status to "Published"', "yatra")}</li>
4856 </ol>
4857 <a
4858 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=destinations&action=create"
4859 target="_blank"
4860 rel="noopener noreferrer"
4861 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"
4862 >
4863 <svg
4864 className="w-3 h-3 mr-1"
4865 fill="none"
4866 stroke="currentColor"
4867 viewBox="0 0 24 24"
4868 >
4869 <path
4870 strokeLinecap="round"
4871 strokeLinejoin="round"
4872 strokeWidth={2}
4873 d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
4874 />
4875 </svg>
4876 {__("Create Destinations", "yatra")}
4877 </a>
4878 </div>
4879 </div>
4880 )}
4881 <HelpText
4882 text={__(
4883 "Select all destinations included in this trip",
4884 "yatra",
4885 )}
4886 className="mt-2"
4887 />
4888 {errors.destinations && (
4889 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
4890 <AlertCircle className="w-4 h-4" />
4891 {errors.destinations}
4892 </p>
4893 )}
4894 </div>
4895
4896 {/* Locations Section - Senior UI/UX Design */}
4897 <div className="space-y-8">
4898 {/* Section Header */}
4899 <div className="text-center space-y-2">
4900 <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center justify-center gap-2">
4901 <div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
4902 <MapPin className="w-3 h-3 text-white" />
4903 </div>
4904 {__("Trip Locations", "yatra")}
4905 </h3>
4906 <p className="text-sm text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
4907 {__(
4908 "Set precise starting and ending points with location names and GPS coordinates. Use manual entry or visual map selection for maximum accuracy.",
4909 "yatra",
4910 )}
4911 </p>
4912 </div>
4913
4914 {/* Locations Grid */}
4915 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
4916 {/* STARTING LOCATION */}
4917 <div className="space-y-6">
4918 {/* Location Header */}
4919 <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">
4920 <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">
4921 <MapPin className="w-5 h-5 text-white" />
4922 </div>
4923 <div className="flex-1">
4924 <h4 className="text-base font-semibold text-blue-900 dark:text-blue-100">
4925 {__("Starting Point", "yatra")}
4926 </h4>
4927 <p className="text-xs text-blue-700 dark:text-blue-300">
4928 {__("Where the journey begins", "yatra")}
4929 </p>
4930 </div>
4931 <div className="flex items-center gap-2">
4932 {formData.starting_location && (
4933 <div
4934 className="w-2 h-2 bg-green-500 rounded-full animate-pulse"
4935 title={__("Location set", "yatra")}
4936 ></div>
4937 )}
4938 {formData.starting_latitude &&
4939 formData.starting_longitude && (
4940 <div
4941 className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"
4942 title={__("Coordinates set", "yatra")}
4943 ></div>
4944 )}
4945 </div>
4946 </div>
4947
4948 {/* Integrated Location Input with Map */}
4949 <div className="space-y-4">
4950 <div className="flex items-center justify-between">
4951 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
4952 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
4953 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
4954 1
4955 </span>
4956 </div>
4957 {__("Starting Location", "yatra")}
4958 </label>
4959 <div className="flex items-center gap-2">
4960 <span className="text-xs text-gray-500 dark:text-gray-400">
4961 {formData.starting_location
4962 ? "✓"
4963 : __("Required", "yatra")}
4964 </span>
4965 {formData.starting_latitude &&
4966 formData.starting_longitude && (
4967 <div className="flex items-center gap-1 text-xs text-green-600">
4968 <div className="w-2 h-2 bg-green-500 rounded-full"></div>
4969 {__("Coords Set", "yatra")}
4970 </div>
4971 )}
4972 </div>
4973 </div>
4974
4975 {/* LocationPicker - Primary Interface */}
4976 <div className="space-y-3">
4977 <LocationPicker
4978 value={{
4979 name: formData.starting_location,
4980 latitude: formData.starting_latitude,
4981 longitude: formData.starting_longitude,
4982 }}
4983 onChange={(locationData) => {
4984 handleFieldChange(
4985 "starting_location",
4986 locationData.name,
4987 );
4988 handleFieldChange(
4989 "starting_latitude",
4990 locationData.latitude,
4991 );
4992 handleFieldChange(
4993 "starting_longitude",
4994 locationData.longitude,
4995 );
4996 }}
4997 label=""
4998 placeholder={__(
4999 "Search for starting location...",
5000 "yatra",
5001 )}
5002 helpText=""
5003 required={false}
5004 defaultMapCenter={
5005 formData.starting_latitude &&
5006 formData.starting_longitude
5007 ? [
5008 parseFloat(formData.starting_latitude),
5009 parseFloat(formData.starting_longitude),
5010 ]
5011 : [20, 0]
5012 }
5013 defaultZoom={
5014 formData.starting_latitude &&
5015 formData.starting_longitude
5016 ? 13
5017 : 2
5018 }
5019 mapHeight="300px"
5020 showMapButton={false}
5021 searchLimit={8}
5022 __={__}
5023 className=""
5024 mapClassName="rounded-lg"
5025 />
5026 </div>
5027 </div>
5028
5029 {/* GPS Coordinates - Manual Override */}
5030 <div className="space-y-4">
5031 <div className="flex items-center justify-between">
5032 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5033 <div className="w-4 h-4 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
5034 <span className="text-xs font-bold text-blue-600 dark:text-blue-400">
5035 2
5036 </span>
5037 </div>
5038 {__("GPS Coordinates", "yatra")}
5039 <span className="text-xs text-gray-500 dark:text-gray-400">
5040 ({__("Manual override", "yatra")})
5041 </span>
5042 </label>
5043 <div className="flex items-center gap-2">
5044 <button
5045 type="button"
5046 onClick={() => {
5047 if (navigator.geolocation) {
5048 navigator.geolocation.getCurrentPosition(
5049 (position) => {
5050 handleFieldChange(
5051 "starting_latitude",
5052 position.coords.latitude.toString(),
5053 );
5054 handleFieldChange(
5055 "starting_longitude",
5056 position.coords.longitude.toString(),
5057 );
5058 },
5059 (error) => {
5060 let message = __(
5061 "Unable to get your location",
5062 "yatra",
5063 );
5064 let showHttpsNotice = false;
5065
5066 switch (error.code) {
5067 case 1: // PERMISSION_DENIED
5068 if (
5069 error.message.includes(
5070 "secure origins",
5071 )
5072 ) {
5073 message = __(
5074 "Location access requires HTTPS. This feature will work on your live HTTPS site.",
5075 "yatra",
5076 );
5077 showHttpsNotice = true;
5078 } else {
5079 message = __(
5080 "Location access denied. Please allow location access in your browser.",
5081 "yatra",
5082 );
5083 }
5084 break;
5085 case 2: // POSITION_UNAVAILABLE
5086 message = __(
5087 "Location information is unavailable. Please try again.",
5088 "yatra",
5089 );
5090 break;
5091 case 3: // TIMEOUT
5092 message = __(
5093 "Location request timed out. Please try again.",
5094 "yatra",
5095 );
5096 break;
5097 }
5098
5099 if (showHttpsNotice) {
5100 alert(
5101 message +
5102 "\n\n" +
5103 __(
5104 "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",
5105 "yatra",
5106 ),
5107 );
5108 } else {
5109 alert(message);
5110 }
5111 },
5112 );
5113 } else {
5114 alert(
5115 __(
5116 "Geolocation is not supported by your browser",
5117 "yatra",
5118 ),
5119 );
5120 }
5121 }}
5122 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"
5123 >
5124 <div className="w-3 h-3">
5125 <svg fill="currentColor" viewBox="0 0 20 20">
5126 <path
5127 fillRule="evenodd"
5128 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"
5129 clipRule="evenodd"
5130 />
5131 </svg>
5132 </div>
5133 {__("Use Current Location", "yatra")}
5134 </button>
5135 </div>
5136 </div>
5137
5138 <div className="grid grid-cols-2 gap-3">
5139 <div className="space-y-2">
5140 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5141 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
5142 {__("Latitude", "yatra")}
5143 </label>
5144 <Input
5145 type="text"
5146 value={formData.starting_latitude}
5147 onChange={(e) =>
5148 handleFieldChange(
5149 "starting_latitude",
5150 e.target.value,
5151 )
5152 }
5153 className="w-full text-sm"
5154 />
5155 </div>
5156 <div className="space-y-2">
5157 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5158 <div className="w-2 h-2 bg-blue-400 rounded-full"></div>
5159 {__("Longitude", "yatra")}
5160 </label>
5161 <Input
5162 type="text"
5163 value={formData.starting_longitude}
5164 onChange={(e) =>
5165 handleFieldChange(
5166 "starting_longitude",
5167 e.target.value,
5168 )
5169 }
5170 placeholder={__("e.g., 115.0920", "yatra")}
5171 className="w-full text-sm"
5172 />
5173 </div>
5174 </div>
5175 <p className="text-xs text-gray-500 dark:text-gray-400">
5176 {__(
5177 "Manual coordinate entry. These will be auto-filled when you select a location from the map above.",
5178 "yatra",
5179 )}
5180 </p>
5181 </div>
5182 </div>
5183
5184 {/* ENDING LOCATION */}
5185 <div className="space-y-6">
5186 {/* Location Header */}
5187 <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">
5188 <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">
5189 <CheckCircle2 className="w-5 h-5 text-white" />
5190 </div>
5191 <div className="flex-1">
5192 <h4 className="text-base font-semibold text-green-900 dark:text-green-100">
5193 {__("Ending Point", "yatra")}
5194 </h4>
5195 <p className="text-xs text-green-700 dark:text-green-300">
5196 {__("Where the journey concludes", "yatra")}
5197 </p>
5198 </div>
5199 <div className="flex items-center gap-2">
5200 {formData.ending_location && (
5201 <div
5202 className="w-2 h-2 bg-green-500 rounded-full animate-pulse"
5203 title={__("Location set", "yatra")}
5204 ></div>
5205 )}
5206 </div>
5207 </div>
5208
5209 {/* Integrated Location Input with Map */}
5210 <div className="space-y-4">
5211 <div className="flex items-center justify-between">
5212 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5213 <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
5214 <span className="text-xs font-bold text-green-600 dark:text-green-400">
5215 1
5216 </span>
5217 </div>
5218 {__("Ending Location", "yatra")}
5219 </label>
5220 <div className="flex items-center gap-2">
5221 <span className="text-xs text-gray-500 dark:text-gray-400">
5222 {formData.ending_location
5223 ? "✓"
5224 : __("Required", "yatra")}
5225 </span>
5226 {formData.ending_latitude &&
5227 formData.ending_longitude && (
5228 <div className="flex items-center gap-1 text-xs text-green-600">
5229 <div className="w-2 h-2 bg-green-500 rounded-full"></div>
5230 {__("Coords Set", "yatra")}
5231 </div>
5232 )}
5233 </div>
5234 </div>
5235
5236 {/* LocationPicker - Primary Interface */}
5237 <div className="space-y-3">
5238 <LocationPicker
5239 value={{
5240 name: formData.ending_location,
5241 latitude: formData.ending_latitude,
5242 longitude: formData.ending_longitude,
5243 }}
5244 onChange={(locationData) => {
5245 handleFieldChange(
5246 "ending_location",
5247 locationData.name,
5248 );
5249 handleFieldChange(
5250 "ending_latitude",
5251 locationData.latitude,
5252 );
5253 handleFieldChange(
5254 "ending_longitude",
5255 locationData.longitude,
5256 );
5257 }}
5258 label=""
5259 placeholder={__(
5260 "Search for ending location...",
5261 "yatra",
5262 )}
5263 helpText=""
5264 required={false}
5265 defaultMapCenter={
5266 formData.ending_latitude &&
5267 formData.ending_longitude
5268 ? [
5269 parseFloat(formData.ending_latitude),
5270 parseFloat(formData.ending_longitude),
5271 ]
5272 : formData.starting_latitude &&
5273 formData.starting_longitude
5274 ? [
5275 parseFloat(formData.starting_latitude),
5276 parseFloat(formData.starting_longitude),
5277 ]
5278 : [20, 0]
5279 }
5280 defaultZoom={
5281 formData.ending_latitude &&
5282 formData.ending_longitude
5283 ? 13
5284 : formData.starting_latitude &&
5285 formData.starting_longitude
5286 ? 13
5287 : 2
5288 }
5289 mapHeight="300px"
5290 showMapButton={false}
5291 searchLimit={8}
5292 __={__}
5293 className=""
5294 mapClassName="rounded-lg"
5295 />
5296 </div>
5297 </div>
5298
5299 {/* GPS Coordinates - Manual Override */}
5300 <div className="space-y-4">
5301 <div className="flex items-center justify-between">
5302 <label className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
5303 <div className="w-4 h-4 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
5304 <span className="text-xs font-bold text-green-600 dark:text-green-400">
5305 2
5306 </span>
5307 </div>
5308 {__("GPS Coordinates", "yatra")}
5309 <span className="text-xs text-gray-500 dark:text-gray-400">
5310 ({__("Optional", "yatra")})
5311 </span>
5312 </label>
5313 </div>
5314
5315 <div className="grid grid-cols-2 gap-3">
5316 <div className="space-y-2">
5317 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5318 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
5319 {__("Latitude", "yatra")}
5320 </label>
5321 <Input
5322 type="text"
5323 value={formData.ending_latitude || ""}
5324 onChange={(e) =>
5325 handleFieldChange(
5326 "ending_latitude",
5327 e.target.value,
5328 )
5329 }
5330 placeholder={__("e.g., -8.5069", "yatra")}
5331 className="w-full text-sm"
5332 />
5333 </div>
5334 <div className="space-y-2">
5335 <label className="text-xs font-medium text-gray-600 dark:text-gray-400 flex items-center gap-1">
5336 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
5337 {__("Longitude", "yatra")}
5338 </label>
5339 <Input
5340 type="text"
5341 value={formData.ending_longitude || ""}
5342 onChange={(e) =>
5343 handleFieldChange(
5344 "ending_longitude",
5345 e.target.value,
5346 )
5347 }
5348 placeholder={__("e.g., 115.2625", "yatra")}
5349 className="w-full text-sm"
5350 />
5351 </div>
5352 </div>
5353 <p className="text-xs text-gray-500 dark:text-gray-400">
5354 {__(
5355 "Optional: Manual coordinate entry. Auto-filled when you select a location from the map above.",
5356 "yatra",
5357 )}
5358 </p>
5359 </div>
5360 </div>
5361 </div>
5362 </div>
5363
5364 {/* Landmarks */}
5365 <div>
5366 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
5367 {__("Key Landmarks / Points of Interest", "yatra")}
5368 </label>
5369 <HelpText
5370 text={__(
5371 "Add notable landmarks or points of interest visited during this trip",
5372 "yatra",
5373 )}
5374 className="mb-2"
5375 />
5376 {formData.landmarks.length > 0 ? (
5377 <div className="space-y-2 mb-2">
5378 {formData.landmarks.map((landmark, index) => (
5379 <div
5380 key={index}
5381 className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
5382 >
5383 <MapPin className="w-4 h-4 text-gray-400 flex-shrink-0" />
5384 <span className="flex-1 text-sm text-gray-900 dark:text-white">
5385 {landmark}
5386 </span>
5387 <Button
5388 type="button"
5389 variant="ghost"
5390 size="sm"
5391 onClick={() => {
5392 const newLandmarks = [...formData.landmarks];
5393 newLandmarks.splice(index, 1);
5394 handleFieldChange("landmarks", newLandmarks);
5395 }}
5396 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
5397 >
5398 <X className="w-4 h-4" />
5399 </Button>
5400 </div>
5401 ))}
5402 </div>
5403 ) : (
5404 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center mb-2">
5405 <MapPin className="w-6 h-6 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
5406 <p className="text-xs text-gray-500 dark:text-gray-400">
5407 {__("No landmarks added yet", "yatra")}
5408 </p>
5409 </div>
5410 )}
5411 <Button
5412 type="button"
5413 variant="outline"
5414 onClick={() => {
5415 setShowLandmarkDialog(true);
5416 setLandmarkInput("");
5417 }}
5418 className="w-full"
5419 >
5420 <Plus className="w-4 h-4 mr-2" />
5421 {__("Add Landmark", "yatra")}
5422 </Button>
5423 </div>
5424 </div>
5425 </div>
5426 );
5427
5428 case "duration":
5429 return (
5430 <div className="space-y-6">
5431 <div className="flex flex-wrap items-center gap-2 mb-2">
5432 <Calendar className="w-5 h-5 text-gray-500" />
5433 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
5434 {__("Availability & Booking", "yatra")}
5435 </h2>
5436 <Badge
5437 variant="outline"
5438 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"
5439 >
5440 {__("Recommended", "yatra")}
5441 </Badge>
5442 </div>
5443 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
5444 {__("When is it available & who can book it?", "yatra")}
5445 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
5446 {__(
5447 "Set your trip's availability period, capacity limits, and booking requirements. This helps automate bookings and prevent overbooking.",
5448 "yatra",
5449 )}
5450 </span>
5451 </p>
5452
5453 <div className="space-y-6">
5454 {/* SECTION 1: Availability Period */}
5455 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5456 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5457 <Calendar className="w-4 h-4 text-blue-500" />
5458 {__("Availability Period", "yatra")}
5459 </h3>
5460 <div className="space-y-4">
5461 {/* Availability Dates */}
5462 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5463 <div>
5464 <label
5465 htmlFor="available_from"
5466 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5467 >
5468 {__("Available From", "yatra")}
5469 </label>
5470 <DatePicker
5471 value={formData.available_from}
5472 onChange={(val) =>
5473 handleFieldChange("available_from", val)
5474 }
5475 placeholder={__("Select date", "yatra")}
5476 />
5477 <HelpText
5478 text={__(
5479 "Earliest date this trip becomes available for booking",
5480 "yatra",
5481 )}
5482 className="mt-2"
5483 />
5484 </div>
5485 <div>
5486 <label
5487 htmlFor="available_to"
5488 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5489 >
5490 {__("Available To", "yatra")}
5491 </label>
5492 <DatePicker
5493 value={formData.available_to}
5494 onChange={(val) =>
5495 handleFieldChange("available_to", val)
5496 }
5497 placeholder={__("Select date", "yatra")}
5498 />
5499 <HelpText
5500 text={__(
5501 "Latest date this trip is available for booking",
5502 "yatra",
5503 )}
5504 className="mt-2"
5505 />
5506 </div>
5507 </div>
5508
5509 {/* Booking Window */}
5510 <div>
5511 <label
5512 htmlFor="booking_window_days"
5513 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5514 >
5515 {__("Booking Window (Days in Advance)", "yatra")}
5516 </label>
5517 <Input
5518 id="booking_window_days"
5519 type="number"
5520 min="0"
5521 value={formData.booking_window_days}
5522 onChange={(e) =>
5523 handleFieldChange("booking_window_days", e.target.value)
5524 }
5525 placeholder={__("e.g., 30", "yatra")}
5526 />
5527 <HelpText
5528 text={__(
5529 "Minimum days in advance customers can book this trip",
5530 "yatra",
5531 )}
5532 className="mt-2"
5533 />
5534 </div>
5535
5536 {/* Seasonal Availability */}
5537 <div>
5538 <label
5539 htmlFor="seasonal_availability"
5540 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5541 >
5542 {__("Seasonal Availability Notes", "yatra")}
5543 </label>
5544 <Input
5545 id="seasonal_availability"
5546 type="text"
5547 value={formData.seasonal_availability}
5548 onChange={(e) =>
5549 handleFieldChange(
5550 "seasonal_availability",
5551 e.target.value,
5552 )
5553 }
5554 placeholder={__(
5555 "e.g., Available year-round except monsoon season",
5556 "yatra",
5557 )}
5558 />
5559 <HelpText
5560 text={__(
5561 "General notes about when this trip is typically available",
5562 "yatra",
5563 )}
5564 className="mt-2"
5565 />
5566 </div>
5567 </div>
5568 </div>
5569
5570 {/* SECTION 2: Capacity & Travelers */}
5571 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5572 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5573 <Users className="w-4 h-4 text-blue-500" />
5574 {__("Capacity & Travelers", "yatra")}
5575 </h3>
5576 <div className="space-y-4">
5577 {/* Group Size */}
5578 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5579 <div>
5580 <label
5581 htmlFor="min_travelers"
5582 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5583 >
5584 {__("Minimum Travelers", "yatra")}
5585 </label>
5586 <Input
5587 id="min_travelers"
5588 type="number"
5589 min="1"
5590 value={formData.min_travelers}
5591 onChange={(e) =>
5592 handleFieldChange("min_travelers", e.target.value)
5593 }
5594 className={errors.min_travelers ? "border-red-500" : ""}
5595 />
5596 {errors.min_travelers && (
5597 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5598 <AlertCircle className="w-4 h-4" />
5599 {errors.min_travelers}
5600 </p>
5601 )}
5602 </div>
5603 <div>
5604 <label
5605 htmlFor="max_travelers"
5606 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5607 >
5608 {__("Maximum Travelers", "yatra")}
5609 </label>
5610 <Input
5611 id="max_travelers"
5612 type="number"
5613 min="1"
5614 value={formData.max_travelers}
5615 onChange={(e) =>
5616 handleFieldChange("max_travelers", e.target.value)
5617 }
5618 className={errors.max_travelers ? "border-red-500" : ""}
5619 />
5620 {errors.max_travelers && (
5621 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5622 <AlertCircle className="w-4 h-4" />
5623 {errors.max_travelers}
5624 </p>
5625 )}
5626 </div>
5627 </div>
5628
5629 {/* Fallback Settings */}
5630 <div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
5631 <div className="flex items-start gap-2 mb-3">
5632 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
5633 {__("Fallback Settings", "yatra")}
5634 </h4>
5635 <span
5636 title={__(
5637 "These settings are used when the trip has no availability dates or recurring rules set. They provide default values for flexible booking.",
5638 "yatra",
5639 )}
5640 >
5641 <HelpCircle className="w-4 h-4 text-gray-400 cursor-help" />
5642 </span>
5643 </div>
5644
5645 {/* Info Banner */}
5646 <div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
5647 <div className="flex items-start gap-2">
5648 <AlertCircle className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
5649 <div className="text-xs text-amber-800 dark:text-amber-200">
5650 <p className="font-medium mb-1">
5651 {__("When are these settings used?", "yatra")}
5652 </p>
5653 <p className="text-amber-700 dark:text-amber-300">
5654 {__(
5655 "These settings apply ONLY when your trip has ZERO availability dates AND ZERO recurring rules. They provide defaults for flexible booking scenarios.",
5656 "yatra",
5657 )}
5658 </p>
5659 </div>
5660 </div>
5661 </div>
5662
5663 {/* Settings Content - Grouped by Trip Type */}
5664 <div className="space-y-4">
5665 {/* Day Tour Settings */}
5666 {formData.trip_type === "single_day" && (
5667 <div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
5668 <h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5669 <Clock className="w-4 h-4" />
5670 {__("Day Tour Time Settings", "yatra")}
5671 </h5>
5672
5673 {/* Enable Multiple Time Slots Toggle */}
5674 <div className="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
5675 <div className="flex items-start gap-3">
5676 <input
5677 type="checkbox"
5678 id="has_default_time_slots"
5679 checked={formData.has_default_time_slots}
5680 onChange={(e) =>
5681 handleFieldChange(
5682 "has_default_time_slots",
5683 e.target.checked,
5684 )
5685 }
5686 className="mt-0.5 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
5687 />
5688 <div className="flex-1">
5689 <label
5690 htmlFor="has_default_time_slots"
5691 className="text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
5692 >
5693 {__("Enable Multiple Time Slots", "yatra")}
5694 </label>
5695 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
5696 {__(
5697 "Allow customers to select from multiple departure times (e.g., Morning, Afternoon, Evening tours)",
5698 "yatra",
5699 )}
5700 </p>
5701 </div>
5702 </div>
5703 </div>
5704
5705 {/* Time Slots Configuration (if enabled) */}
5706 {formData.has_default_time_slots && (
5707 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
5708 <div className="flex items-center justify-between mb-3">
5709 <div>
5710 <label className="block text-sm font-medium text-gray-900 dark:text-white">
5711 {__("Time Slots Configuration", "yatra")}
5712 </label>
5713 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
5714 {__(
5715 "Add multiple departure times for customers to choose from.",
5716 "yatra",
5717 )}
5718 </p>
5719 </div>
5720 <Button
5721 type="button"
5722 variant="outline"
5723 size="sm"
5724 onClick={() => {
5725 const newSlot: TimeSlot = {
5726 id: `slot-${Date.now()}`,
5727 time: "09:00",
5728 label: "",
5729 };
5730 handleFieldChange("default_time_slots", [
5731 ...formData.default_time_slots,
5732 newSlot,
5733 ]);
5734 }}
5735 className="flex items-center gap-1"
5736 >
5737 <Plus className="w-4 h-4" />
5738 {__("Add Time Slot", "yatra")}
5739 </Button>
5740 </div>
5741
5742 {formData.default_time_slots.length === 0 ? (
5743 <div className="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
5744 <Clock className="w-8 h-8 mx-auto text-gray-400 mb-2" />
5745 <p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
5746 {__("No time slots added yet", "yatra")}
5747 </p>
5748 <Button
5749 type="button"
5750 variant="outline"
5751 size="sm"
5752 onClick={() => {
5753 const newSlot: TimeSlot = {
5754 id: `slot-${Date.now()}`,
5755 time: "09:00",
5756 label: __("Morning Tour", "yatra"),
5757 };
5758 handleFieldChange("default_time_slots", [
5759 newSlot,
5760 ]);
5761 }}
5762 >
5763 <Plus className="w-4 h-4 mr-1" />
5764 {__("Add First Time Slot", "yatra")}
5765 </Button>
5766 </div>
5767 ) : (
5768 <div className="space-y-3">
5769 {formData.default_time_slots.map(
5770 (slot, index) => (
5771 <div
5772 key={slot.id}
5773 className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
5774 >
5775 <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">
5776 <span className="text-sm font-semibold text-blue-600 dark:text-blue-400">
5777 {index + 1}
5778 </span>
5779 </div>
5780 <div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
5781 <div>
5782 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
5783 {__("Time", "yatra")}
5784 </label>
5785 <Input
5786 type="time"
5787 value={slot.time}
5788 onChange={(e) => {
5789 const updated = [
5790 ...formData.default_time_slots,
5791 ];
5792 updated[index] = {
5793 ...slot,
5794 time: e.target.value,
5795 };
5796 handleFieldChange(
5797 "default_time_slots",
5798 updated,
5799 );
5800 }}
5801 className="w-full"
5802 />
5803 </div>
5804 <div>
5805 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
5806 {__("Label", "yatra")}
5807 </label>
5808 <Input
5809 type="text"
5810 value={slot.label}
5811 onChange={(e) => {
5812 const updated = [
5813 ...formData.default_time_slots,
5814 ];
5815 updated[index] = {
5816 ...slot,
5817 label: e.target.value,
5818 };
5819 handleFieldChange(
5820 "default_time_slots",
5821 updated,
5822 );
5823 }}
5824 placeholder={__(
5825 "e.g., Morning Tour, Afternoon Tour",
5826 "yatra",
5827 )}
5828 className="w-full"
5829 />
5830 </div>
5831 </div>
5832 <Button
5833 type="button"
5834 variant="ghost"
5835 size="sm"
5836 onClick={() => {
5837 const updated =
5838 formData.default_time_slots.filter(
5839 (_, i) => i !== index,
5840 );
5841 handleFieldChange(
5842 "default_time_slots",
5843 updated,
5844 );
5845 }}
5846 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 flex-shrink-0 mt-1"
5847 title={__(
5848 "Remove time slot",
5849 "yatra",
5850 )}
5851 >
5852 <X className="w-4 h-4" />
5853 </Button>
5854 </div>
5855 ),
5856 )}
5857 </div>
5858 )}
5859 </div>
5860 )}
5861
5862 {/* Single Departure Time (if multiple slots disabled) */}
5863 {!formData.has_default_time_slots && (
5864 <div>
5865 <label
5866 htmlFor="departure_time_single"
5867 className="block text-sm font-medium text-gray-900 dark:text-white mb-2"
5868 >
5869 {__("Default Departure Time", "yatra")}
5870 </label>
5871 <Input
5872 id="departure_time_single"
5873 type="time"
5874 value={formData.departure_time}
5875 onChange={(e) =>
5876 handleFieldChange(
5877 "departure_time",
5878 e.target.value,
5879 )
5880 }
5881 className="max-w-xs"
5882 />
5883 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
5884 {__(
5885 "Single departure time for all bookings when multiple time slots are not enabled.",
5886 "yatra",
5887 )}
5888 </p>
5889 </div>
5890 )}
5891 </div>
5892 )}
5893
5894 {/* Multi-Day Trip Settings */}
5895 {formData.trip_type === "multi_day" && (
5896 <div className="p-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
5897 <h5 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5898 <Calendar className="w-4 h-4" />
5899 {__("Multi-Day Trip Departure Settings", "yatra")}
5900 </h5>
5901
5902 <div>
5903 <label
5904 htmlFor="departure_time_multiday"
5905 className="block text-sm font-medium text-gray-900 dark:text-white mb-2"
5906 >
5907 {__("Default Departure Time", "yatra")}
5908 </label>
5909 <Input
5910 id="departure_time_multiday"
5911 type="time"
5912 value={formData.departure_time}
5913 onChange={(e) =>
5914 handleFieldChange(
5915 "departure_time",
5916 e.target.value,
5917 )
5918 }
5919 className="max-w-xs"
5920 />
5921 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
5922 {__(
5923 "Default departure time for trips without specific availability dates.",
5924 "yatra",
5925 )}
5926 </p>
5927 </div>
5928 </div>
5929 )}
5930
5931 {/* No Trip Type Selected */}
5932 {!formData.trip_type && (
5933 <div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700 text-center">
5934 <p className="text-sm text-gray-500 dark:text-gray-400">
5935 {__(
5936 "Please select a trip type (Day Tour or Multi-Day) to configure fallback settings.",
5937 "yatra",
5938 )}
5939 </p>
5940 </div>
5941 )}
5942 </div>
5943 </div>
5944 </div>
5945 </div>
5946
5947 {/* SECTION 3: Booking Policies */}
5948 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
5949 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
5950 <FileText className="w-4 h-4 text-blue-500" />
5951 {__("Booking Policies", "yatra")}
5952 </h3>
5953 <div className="space-y-4">
5954 {/* Age Restrictions */}
5955 <div>
5956 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
5957 {__("Age Restrictions", "yatra")}
5958 </h4>
5959 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
5960 <div>
5961 <label
5962 htmlFor="age_min"
5963 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5964 >
5965 {__("Minimum Age", "yatra")}
5966 </label>
5967 <Input
5968 id="age_min"
5969 type="number"
5970 min="0"
5971 value={formData.age_min}
5972 onChange={(e) =>
5973 handleFieldChange("age_min", e.target.value)
5974 }
5975 placeholder={__("e.g., 18", "yatra")}
5976 className={errors.age_min ? "border-red-500" : ""}
5977 />
5978 {errors.age_min && (
5979 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
5980 <AlertCircle className="w-4 h-4" />
5981 {errors.age_min}
5982 </p>
5983 )}
5984 </div>
5985 <div>
5986 <label
5987 htmlFor="age_max"
5988 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
5989 >
5990 {__("Maximum Age", "yatra")}
5991 </label>
5992 <Input
5993 id="age_max"
5994 type="number"
5995 min="0"
5996 value={formData.age_max}
5997 onChange={(e) =>
5998 handleFieldChange("age_max", e.target.value)
5999 }
6000 placeholder={__("e.g., 65", "yatra")}
6001 className={errors.age_max ? "border-red-500" : ""}
6002 />
6003 {errors.age_max && (
6004 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6005 <AlertCircle className="w-4 h-4" />
6006 {errors.age_max}
6007 </p>
6008 )}
6009 </div>
6010 </div>
6011 </div>
6012
6013 {/* Requirements */}
6014 <div>
6015 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
6016 {__("Trip Requirements", "yatra")}
6017 </h4>
6018 <div className="space-y-4">
6019 <div>
6020 <label
6021 htmlFor="physical_requirements"
6022 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6023 >
6024 {__("Physical Requirements", "yatra")}
6025 </label>
6026 <textarea
6027 id="physical_requirements"
6028 value={formData.physical_requirements}
6029 onChange={(e) =>
6030 handleFieldChange(
6031 "physical_requirements",
6032 e.target.value,
6033 )
6034 }
6035 placeholder={__(
6036 "e.g., Moderate fitness level required. Some walking involved but no strenuous activities.",
6037 "yatra",
6038 )}
6039 rows={3}
6040 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"
6041 />
6042 </div>
6043 <div>
6044 <label
6045 htmlFor="visa_requirements"
6046 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6047 >
6048 {__("Visa Requirements", "yatra")}
6049 </label>
6050 <textarea
6051 id="visa_requirements"
6052 value={formData.visa_requirements}
6053 onChange={(e) =>
6054 handleFieldChange(
6055 "visa_requirements",
6056 e.target.value,
6057 )
6058 }
6059 placeholder={__(
6060 "e.g., Tourist visa required. Can be obtained on arrival or in advance.",
6061 "yatra",
6062 )}
6063 rows={3}
6064 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"
6065 />
6066 </div>
6067 <div>
6068 <label
6069 htmlFor="vaccination_requirements"
6070 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6071 >
6072 {__("Vaccination Requirements", "yatra")}
6073 </label>
6074 <textarea
6075 id="vaccination_requirements"
6076 value={formData.vaccination_requirements}
6077 onChange={(e) =>
6078 handleFieldChange(
6079 "vaccination_requirements",
6080 e.target.value,
6081 )
6082 }
6083 placeholder={__(
6084 "e.g., No mandatory vaccinations. Recommended: Hepatitis A, Typhoid, and routine vaccinations.",
6085 "yatra",
6086 )}
6087 rows={3}
6088 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"
6089 />
6090 </div>
6091 </div>
6092 </div>
6093
6094 {/* Cancellation Policy */}
6095 <div>
6096 <div className="flex items-center justify-between mb-3">
6097 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
6098 {__("Cancellation Policy", "yatra")}
6099 </h4>
6100 <AiFieldAffordance
6101 task="trip-cancellation-policy"
6102 label={__("Cancellation Policy", "yatra")}
6103 value={formData.cancellation_policy}
6104 onAccept={(v) =>
6105 handleFieldChange("cancellation_policy", v)
6106 }
6107 buildContext={() => buildTripAiContext(formData)}
6108 />
6109 </div>
6110 <textarea
6111 id="cancellation_policy"
6112 value={formData.cancellation_policy}
6113 onChange={(e) =>
6114 handleFieldChange("cancellation_policy", e.target.value)
6115 }
6116 placeholder={__(
6117 "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.",
6118 "yatra",
6119 )}
6120 rows={4}
6121 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"
6122 />
6123 </div>
6124 </div>
6125 </div>
6126
6127 {/* SECTION 3b: Disable Booking (Pro) */}
6128 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6129 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6130 <Calendar className="w-4 h-4 text-blue-500" />
6131 {__("Booking Availability", "yatra")}
6132 <ProBadge isProActive={isPro} />
6133 </h3>
6134 <ProFeature
6135 description={__(
6136 "disable the booking calendar and make trips enquiry-only",
6137 "yatra",
6138 )}
6139 moduleName={__("Enquiry Only Mode", "yatra")}
6140 pricingUrl="https://wpyatra.com/pricing"
6141 isProActive={isPro}
6142 isModuleEnabled={isPro}
6143 >
6144 <div className="flex items-start gap-3">
6145 <div className="flex items-center h-5 mt-0.5">
6146 <input
6147 id="disable_booking"
6148 type="checkbox"
6149 checked={formData.disable_booking}
6150 onChange={(e) =>
6151 handleFieldChange("disable_booking", e.target.checked)
6152 }
6153 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"
6154 />
6155 </div>
6156 <div>
6157 <label
6158 htmlFor="disable_booking"
6159 className="block text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
6160 >
6161 {__("Disable Booking Calendar", "yatra")}
6162 </label>
6163 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
6164 {__(
6165 "When enabled, the booking form and date picker will be hidden. Visitors can only send an enquiry for this trip.",
6166 "yatra",
6167 )}
6168 </p>
6169 </div>
6170 </div>
6171 </ProFeature>
6172 </div>
6173
6174 {/* SECTION 4: Accommodation */}
6175 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6176 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6177 <Home className="w-4 h-4 text-blue-500" />
6178 {__("Accommodation", "yatra")}
6179 </h3>
6180 <div className="space-y-4">
6181 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6182 <div>
6183 <label
6184 htmlFor="accommodation_type"
6185 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6186 >
6187 {__("Accommodation Type", "yatra")}
6188 </label>
6189 <Input
6190 id="accommodation_type"
6191 type="text"
6192 value={formData.accommodation_type}
6193 onChange={(e) =>
6194 handleFieldChange(
6195 "accommodation_type",
6196 e.target.value,
6197 )
6198 }
6199 placeholder={__(
6200 "e.g., Hotel, Resort, Teahouse, Camping",
6201 "yatra",
6202 )}
6203 />
6204 </div>
6205 <div>
6206 <label
6207 htmlFor="meal_plan"
6208 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6209 >
6210 {__("Meal Plan", "yatra")}
6211 </label>
6212 <Select
6213 id="meal_plan"
6214 value={formData.meal_plan}
6215 onChange={(e) =>
6216 handleFieldChange("meal_plan", e.target.value)
6217 }
6218 >
6219 {MEAL_PLAN_SELECT_OPTIONS.map(({ value, label }) => (
6220 <option
6221 key={value || "meal-plan-empty"}
6222 value={value}
6223 >
6224 {label}
6225 </option>
6226 ))}
6227 </Select>
6228 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
6229 {__(
6230 "Tells travelers which meals are included with lodging (e.g. breakfast only vs full board).",
6231 "yatra",
6232 )}
6233 </p>
6234 </div>
6235 </div>
6236 <div>
6237 <label
6238 htmlFor="accommodation_details"
6239 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6240 >
6241 {__("Accommodation Details", "yatra")}
6242 </label>
6243 <textarea
6244 id="accommodation_details"
6245 value={formData.accommodation_details}
6246 onChange={(e) =>
6247 handleFieldChange(
6248 "accommodation_details",
6249 e.target.value,
6250 )
6251 }
6252 placeholder={__(
6253 "e.g., 4-star beachfront resort with private balconies, infinity pool, and spa facilities",
6254 "yatra",
6255 )}
6256 rows={3}
6257 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"
6258 />
6259 </div>
6260 </div>
6261 </div>
6262
6263 {/* SECTION 5: Transportation */}
6264 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6265 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6266 <Car className="w-4 h-4 text-blue-500" />
6267 {__("Transportation", "yatra")}
6268 </h3>
6269 <div className="space-y-4">
6270 <div className="flex items-center gap-2">
6271 <input
6272 type="checkbox"
6273 id="transportation_included"
6274 checked={formData.transportation_included}
6275 onChange={(e) =>
6276 handleFieldChange(
6277 "transportation_included",
6278 e.target.checked,
6279 )
6280 }
6281 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"
6282 />
6283 <label
6284 htmlFor="transportation_included"
6285 className="text-sm font-medium text-gray-900 dark:text-white"
6286 >
6287 {__("Transportation Included", "yatra")}
6288 </label>
6289 </div>
6290 {formData.transportation_included && (
6291 <>
6292 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6293 <div>
6294 <label
6295 htmlFor="pickup_location"
6296 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6297 >
6298 {__("Pickup Location", "yatra")}
6299 </label>
6300 <Input
6301 id="pickup_location"
6302 type="text"
6303 value={formData.pickup_location}
6304 onChange={(e) =>
6305 handleFieldChange(
6306 "pickup_location",
6307 e.target.value,
6308 )
6309 }
6310 placeholder={__(
6311 "e.g., Airport, Hotel, City Center",
6312 "yatra",
6313 )}
6314 />
6315 </div>
6316 <div>
6317 <label
6318 htmlFor="dropoff_location"
6319 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6320 >
6321 {__("Dropoff Location", "yatra")}
6322 </label>
6323 <Input
6324 id="dropoff_location"
6325 type="text"
6326 value={formData.dropoff_location}
6327 onChange={(e) =>
6328 handleFieldChange(
6329 "dropoff_location",
6330 e.target.value,
6331 )
6332 }
6333 placeholder={__(
6334 "e.g., Airport, Hotel, City Center",
6335 "yatra",
6336 )}
6337 />
6338 </div>
6339 </div>
6340 <div>
6341 <label
6342 htmlFor="transportation_details"
6343 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6344 >
6345 {__("Transportation Details", "yatra")}
6346 </label>
6347 <textarea
6348 id="transportation_details"
6349 value={formData.transportation_details}
6350 onChange={(e) =>
6351 handleFieldChange(
6352 "transportation_details",
6353 e.target.value,
6354 )
6355 }
6356 placeholder={__(
6357 "e.g., Private air-conditioned vehicle with professional driver",
6358 "yatra",
6359 )}
6360 rows={3}
6361 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"
6362 />
6363 </div>
6364 </>
6365 )}
6366 </div>
6367 </div>
6368 </div>
6369 </div>
6370 );
6371
6372 case "categorization":
6373 return (
6374 <div className="space-y-6">
6375 <div className="flex flex-wrap items-center gap-2 mb-2">
6376 <Tag className="w-5 h-5 text-gray-500" />
6377 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6378 {__("Categories & Attributes", "yatra")}
6379 </h2>
6380 <Badge
6381 variant="outline"
6382 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"
6383 >
6384 {__("Recommended", "yatra")}
6385 </Badge>
6386 </div>
6387 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
6388 {__("How should travelers find this trip?", "yatra")}
6389 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
6390 {__(
6391 "Organize your trip with categories, activities, and custom attributes to help travelers discover it through search and filters.",
6392 "yatra",
6393 )}
6394 </span>
6395 </p>
6396
6397 <div className="space-y-6">
6398 {/* SECTION 1: Categories & Classification */}
6399 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6400 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6401 <Tag className="w-4 h-4 text-blue-500" />
6402 {__("Categories & Classification", "yatra")}
6403 </h3>
6404 <div className="space-y-4">
6405 {/* Trip Category */}
6406 <div>
6407 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6408 {__("Trip Categories", "yatra")}
6409 </label>
6410 {tripCategories && tripCategories.length > 0 ? (
6411 <MultiSelect
6412 value={formData.trip_category}
6413 onChange={(values) =>
6414 handleFieldChange(
6415 "trip_category",
6416 values.map((v) => Number(v)),
6417 )
6418 }
6419 options={(() => {
6420 // Flatten categories with hierarchy indication
6421 const flattenCategories = (
6422 cats: any[],
6423 prefix = "",
6424 ): { value: number; label: string }[] => {
6425 const result: { value: number; label: string }[] =
6426 [];
6427 cats.forEach((cat) => {
6428 result.push({
6429 value: cat.id,
6430 label: prefix + cat.name,
6431 });
6432 if (
6433 cat.subcategories &&
6434 cat.subcategories.length > 0
6435 ) {
6436 result.push(
6437 ...flattenCategories(
6438 cat.subcategories,
6439 prefix + "— ",
6440 ),
6441 );
6442 }
6443 });
6444 return result;
6445 };
6446 return flattenCategories(tripCategories);
6447 })()}
6448 placeholder={__("Select categories...", "yatra")}
6449 searchPlaceholder={__("Search categories...", "yatra")}
6450 />
6451 ) : (
6452 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6453 <p className="text-sm text-gray-500 dark:text-gray-400">
6454 {__("No categories available. ", "yatra")}
6455 <a
6456 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories"
6457 target="_blank"
6458 rel="noopener noreferrer"
6459 className="text-blue-600 hover:text-blue-700 underline"
6460 >
6461 {__("Create categories here", "yatra")}
6462 </a>
6463 </p>
6464 </div>
6465 )}
6466 <HelpText
6467 text={__(
6468 "Select one or more categories for this trip",
6469 "yatra",
6470 )}
6471 className="mt-2"
6472 />
6473 </div>
6474
6475 {/* Difficulty Level */}
6476 <div>
6477 <label
6478 htmlFor="difficulty_level"
6479 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6480 >
6481 {__("Difficulty Level", "yatra")}
6482 </label>
6483 <Select
6484 id="difficulty_level"
6485 value={formData.difficulty_level}
6486 onChange={(e) =>
6487 handleFieldChange("difficulty_level", e.target.value)
6488 }
6489 disabled={isLoadingDifficultyLevels}
6490 >
6491 <option value="">
6492 {__("Select difficulty", "yatra")}
6493 </option>
6494 {isLoadingDifficultyLevels && (
6495 <option value="" disabled>
6496 {__("Loading difficulty levels...", "yatra")}
6497 </option>
6498 )}
6499 {!isLoadingDifficultyLevels &&
6500 difficultyLevels.length === 0 && (
6501 <option value="" disabled>
6502 {__(
6503 "No difficulty levels available - Click below to create",
6504 "yatra",
6505 )}
6506 </option>
6507 )}
6508 {!isLoadingDifficultyLevels &&
6509 difficultyLevels.map((level) => (
6510 <option
6511 key={`difficulty-${level.id}`}
6512 value={level.id?.toString() || ""}
6513 >
6514 {level.name}
6515 </option>
6516 ))}
6517 </Select>
6518 {!isLoadingDifficultyLevels &&
6519 difficultyLevels.length === 0 && (
6520 <div className="mt-3 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md">
6521 <p className="text-sm text-amber-700 dark:text-amber-300">
6522 {__("No difficulty levels available. ", "yatra")}
6523 <a
6524 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=difficulty-levels"
6525 target="_blank"
6526 rel="noopener noreferrer"
6527 className="text-amber-800 hover:text-amber-900 underline font-medium"
6528 >
6529 {__("Create difficulty levels here", "yatra")}
6530 </a>
6531 </p>
6532 </div>
6533 )}
6534 <HelpText
6535 text={__(
6536 "Physical difficulty level required for this trip",
6537 "yatra",
6538 )}
6539 className="mt-2"
6540 />
6541 </div>
6542
6543 {/* Activity Types */}
6544 <div>
6545 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6546 {__("Activity Types", "yatra")}
6547 </label>
6548 {activitiesData && activitiesData.length > 0 ? (
6549 <MultiSelect
6550 value={formData.activity_types}
6551 onChange={(values) =>
6552 handleFieldChange(
6553 "activity_types",
6554 values.map((v) => Number(v)),
6555 )
6556 }
6557 options={activitiesData.map((activity: any) => ({
6558 value: activity.id,
6559 label: activity.name,
6560 }))}
6561 placeholder={__("Select activities...", "yatra")}
6562 searchPlaceholder={__("Search activities...", "yatra")}
6563 />
6564 ) : (
6565 <div className="p-4 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
6566 <p className="text-sm text-gray-500 dark:text-gray-400">
6567 {__("No activities available. ", "yatra")}
6568 <a
6569 href="/wp-admin/admin.php?page=yatra&subpage=trips&tab=activities"
6570 target="_blank"
6571 rel="noopener noreferrer"
6572 className="text-blue-600 hover:text-blue-700 underline"
6573 >
6574 {__("Create activities here", "yatra")}
6575 </a>
6576 </p>
6577 </div>
6578 )}
6579 <HelpText
6580 text={__(
6581 "Select all activities included in this trip",
6582 "yatra",
6583 )}
6584 className="mt-2"
6585 />
6586 </div>
6587
6588 {/* Featured Priority */}
6589 <div>
6590 <label
6591 htmlFor="featured_priority"
6592 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
6593 >
6594 {__("Featured Priority", "yatra")}
6595 </label>
6596 <Select
6597 id="featured_priority"
6598 value={formData.featured_priority}
6599 onChange={(e) =>
6600 handleFieldChange(
6601 "featured_priority",
6602 e.target.value as TripFormData["featured_priority"],
6603 )
6604 }
6605 >
6606 <option value="none">{__("None", "yatra")}</option>
6607 <option value="featured">
6608 {__("Featured", "yatra")}
6609 </option>
6610 <option value="new">{__("New", "yatra")}</option>
6611 <option value="limited">
6612 {__("Limited Time", "yatra")}
6613 </option>
6614 </Select>
6615 <HelpText
6616 text={__(
6617 "Special designation for frontend display and promotion",
6618 "yatra",
6619 )}
6620 className="mt-2"
6621 />
6622 </div>
6623 </div>
6624 </div>
6625
6626 {/* SECTION 2: Custom Attributes */}
6627 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
6628 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
6629 <Settings className="w-4 h-4 text-blue-500" />
6630 {__("Custom Attributes", "yatra")}
6631 </h3>
6632 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
6633 {__(
6634 "Add custom attributes for advanced filtering and search capabilities.",
6635 "yatra",
6636 )}
6637 </p>
6638 <TripAttributesSection
6639 formData={formData}
6640 handleFieldChange={handleFieldChange}
6641 tripId={tripId || undefined}
6642 isEditMode={isEditMode}
6643 tripAttributesData={tripAttributesData}
6644 tripAttributesReady={tripAttributesReady}
6645 />
6646 </div>
6647 </div>
6648 </div>
6649 );
6650
6651 case "media":
6652 return (
6653 <div className="space-y-6">
6654 <div className="flex flex-wrap items-center gap-2 mb-2">
6655 <Image className="w-5 h-5 text-gray-500" />
6656 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6657 {__("Media & Content", "yatra")}
6658 </h2>
6659 <Badge
6660 variant="outline"
6661 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"
6662 >
6663 {__("Recommended", "yatra")}
6664 </Badge>
6665 </div>
6666 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
6667 {__(
6668 "Add visual content, videos, stories, and testimonials to showcase your trip",
6669 "yatra",
6670 )}
6671 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
6672 {__(
6673 "Optional, but adding rich media greatly increases engagement and conversions.",
6674 "yatra",
6675 )}
6676 </span>
6677 </p>
6678
6679 <div className="space-y-6">
6680 {/* Photo Gallery */}
6681 <Card>
6682 <CardHeader>
6683 <CardTitle>{__("Photo Gallery", "yatra")}</CardTitle>
6684 <CardDescription>
6685 {__(
6686 "Upload images to showcase your trip. These will be displayed on the trip page.",
6687 "yatra",
6688 )}
6689 </CardDescription>
6690 </CardHeader>
6691 <CardContent>
6692 <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
6693 {formData.gallery_images.map((image, index) => (
6694 <div key={index} className="relative group">
6695 <div className="aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden">
6696 <img
6697 src={image.thumbnail_url || image.url}
6698 alt={image.alt_text || `Gallery ${index + 1}`}
6699 className="w-full h-full object-cover"
6700 />
6701 </div>
6702 {/* Reorder buttons */}
6703 <div className="absolute top-2 left-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
6704 {index > 0 && (
6705 <button
6706 type="button"
6707 onClick={() =>
6708 handleGalleryReorder(index, index - 1)
6709 }
6710 className="bg-black/60 text-white rounded p-1 hover:bg-black/80"
6711 title={__("Move left", "yatra")}
6712 >
6713 <ChevronLeft className="w-4 h-4" />
6714 </button>
6715 )}
6716 {index < formData.gallery_images.length - 1 && (
6717 <button
6718 type="button"
6719 onClick={() =>
6720 handleGalleryReorder(index, index + 1)
6721 }
6722 className="bg-black/60 text-white rounded p-1 hover:bg-black/80"
6723 title={__("Move right", "yatra")}
6724 >
6725 <ChevronRight className="w-4 h-4" />
6726 </button>
6727 )}
6728 </div>
6729 {/* Order badge */}
6730 <span className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-1.5 py-0.5 rounded">
6731 {index + 1}
6732 </span>
6733 {/* Remove button */}
6734 <button
6735 type="button"
6736 onClick={() => handleGalleryRemove(index)}
6737 className="absolute top-2 right-2 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
6738 >
6739 <X className="w-4 h-4" />
6740 </button>
6741 </div>
6742 ))}
6743 <button
6744 type="button"
6745 onClick={handleGalleryAdd}
6746 className={`aspect-video border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors ${
6747 errors.gallery_images
6748 ? "border-red-500 dark:border-red-600 bg-red-50 dark:bg-red-900/20"
6749 : "border-gray-300 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-400"
6750 }`}
6751 >
6752 <Upload
6753 className={`w-8 h-8 mb-2 ${errors.gallery_images ? "text-red-400" : "text-gray-400"}`}
6754 />
6755 <span
6756 className={`text-sm ${errors.gallery_images ? "text-red-600 dark:text-red-400" : "text-gray-600 dark:text-gray-400"}`}
6757 >
6758 {__("Add Image", "yatra")}
6759 </span>
6760 </button>
6761 </div>
6762 {errors.gallery_images && (
6763 <p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6764 <AlertCircle className="w-4 h-4" />
6765 {errors.gallery_images}
6766 </p>
6767 )}
6768 </CardContent>
6769 </Card>
6770 {/* Video & Virtual Tour */}
6771 <Card>
6772 <CardHeader>
6773 <CardTitle>{__("Video & Virtual Tour", "yatra")}</CardTitle>
6774 <CardDescription>
6775 {__(
6776 "Add video content to showcase your trip visually",
6777 "yatra",
6778 )}
6779 </CardDescription>
6780 </CardHeader>
6781 <CardContent className="space-y-4">
6782 <div>
6783 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6784 {__("Video URL", "yatra")} (YouTube/Vimeo)
6785 </label>
6786 <HelpText
6787 text={__(
6788 "Paste the full URL from YouTube or Vimeo. The video will be embedded on your trip page.",
6789 "yatra",
6790 )}
6791 className="mb-2"
6792 />
6793 <Input
6794 type="url"
6795 value={formData.video_url}
6796 onChange={(e) =>
6797 handleFieldChange("video_url", e.target.value)
6798 }
6799 placeholder="https://www.youtube.com/watch?v=..."
6800 className={`font-mono text-xs ${errors.video_url ? "border-red-500" : ""}`}
6801 />
6802 {errors.video_url && (
6803 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6804 <AlertCircle className="w-4 h-4" />
6805 {errors.video_url}
6806 </p>
6807 )}
6808 </div>
6809 <div>
6810 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6811 {__("360° Virtual Tour URL", "yatra")}
6812 </label>
6813 <HelpText
6814 text={__(
6815 "Add a link to an interactive 360° virtual tour if available",
6816 "yatra",
6817 )}
6818 className="mb-2"
6819 />
6820 <Input
6821 type="url"
6822 value={formData.virtual_tour_url}
6823 onChange={(e) =>
6824 handleFieldChange("virtual_tour_url", e.target.value)
6825 }
6826 placeholder="https://..."
6827 className={`font-mono text-xs ${errors.virtual_tour_url ? "border-red-500" : ""}`}
6828 />
6829 {errors.virtual_tour_url && (
6830 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
6831 <AlertCircle className="w-4 h-4" />
6832 {errors.virtual_tour_url}
6833 </p>
6834 )}
6835 </div>
6836 </CardContent>
6837 </Card>
6838
6839 {/* Trip Story & What Makes Special */}
6840 <Card>
6841 <CardHeader>
6842 <CardTitle>
6843 {__("Trip Story & Special Features", "yatra")}
6844 </CardTitle>
6845 <CardDescription>
6846 {__(
6847 "Tell an engaging story and highlight what makes this trip unique",
6848 "yatra",
6849 )}
6850 </CardDescription>
6851 </CardHeader>
6852 <CardContent className="space-y-4">
6853 <div>
6854 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6855 {__("What Makes This Trip Special", "yatra")}
6856 </label>
6857 <HelpText
6858 text={__(
6859 "Highlight the unique selling points and what sets this trip apart from others",
6860 "yatra",
6861 )}
6862 className="mb-2"
6863 />
6864 <textarea
6865 value={formData.what_makes_special}
6866 onChange={(e) =>
6867 handleFieldChange("what_makes_special", e.target.value)
6868 }
6869 placeholder={__(
6870 "Describe what makes this trip unique and special...",
6871 "yatra",
6872 )}
6873 rows={5}
6874 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"
6875 />
6876 </div>
6877 <div>
6878 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6879 {__("Trip Story / Narrative", "yatra")}
6880 </label>
6881 <HelpText
6882 text={__(
6883 "Tell an engaging story about this trip. Use storytelling to connect with potential travelers emotionally",
6884 "yatra",
6885 )}
6886 className="mb-2"
6887 />
6888 <textarea
6889 value={formData.trip_story}
6890 onChange={(e) =>
6891 handleFieldChange("trip_story", e.target.value)
6892 }
6893 placeholder={__(
6894 "Write an engaging narrative about this trip...",
6895 "yatra",
6896 )}
6897 rows={8}
6898 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"
6899 />
6900 </div>
6901 </CardContent>
6902 </Card>
6903
6904 {/* Testimonials - Select from Reviews */}
6905 <TestimonialsSelector
6906 tripId={tripId}
6907 selectedReviewIds={formData.testimonial_review_ids}
6908 onChange={(reviewIds) =>
6909 handleFieldChange("testimonial_review_ids", reviewIds)
6910 }
6911 />
6912 </div>
6913 </div>
6914 );
6915
6916 case "downloads":
6917 if (!showDownloadsUI) {
6918 return (
6919 <div className="space-y-6">
6920 <Alert variant="info">
6921 {__("Downloads module is not enabled.", "yatra")}
6922 </Alert>
6923 </div>
6924 );
6925 }
6926
6927 return (
6928 <div className="space-y-6">
6929 <div className="flex flex-wrap items-center gap-2 mb-2">
6930 <Download className="w-5 h-5 text-gray-500" />
6931 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
6932 {__("Downloads", "yatra")}
6933 </h2>
6934 </div>
6935 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
6936 {__(
6937 "Attach files to this trip and control who can access them.",
6938 "yatra",
6939 )}
6940 </p>
6941
6942 <Card>
6943 <CardHeader>
6944 <CardTitle>{__("Downloads", "yatra")}</CardTitle>
6945 <CardDescription>
6946 {__(
6947 "Attach files to this trip and control who can access them.",
6948 "yatra",
6949 )}
6950 </CardDescription>
6951 </CardHeader>
6952 <CardContent className="space-y-4">
6953 {(formData.downloadable_items || []).length > 0 ? (
6954 <div className="space-y-3">
6955 {(formData.downloadable_items || []).map((item, index) => (
6956 <div
6957 key={index}
6958 className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg space-y-3"
6959 >
6960 <div className="flex items-start justify-between gap-2">
6961 <div className="flex-1">
6962 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
6963 {__("Title", "yatra")}
6964 </label>
6965 <Input
6966 value={item.title}
6967 onChange={(e) =>
6968 handleDownloadableItemChange(
6969 index,
6970 "title",
6971 e.target.value,
6972 )
6973 }
6974 placeholder={__(
6975 "E.g. Packing list, itinerary PDF, waiver form...",
6976 "yatra",
6977 )}
6978 />
6979 </div>
6980
6981 <div className="flex gap-1 pt-7">
6982 {index > 0 && (
6983 <Button
6984 type="button"
6985 variant="ghost"
6986 size="sm"
6987 onClick={() =>
6988 handleDownloadableItemMove(index, index - 1)
6989 }
6990 title={__("Move up", "yatra")}
6991 >
6992 <ChevronUp className="w-4 h-4" />
6993 </Button>
6994 )}
6995 {index <
6996 (formData.downloadable_items || []).length -
6997 1 && (
6998 <Button
6999 type="button"
7000 variant="ghost"
7001 size="sm"
7002 onClick={() =>
7003 handleDownloadableItemMove(index, index + 1)
7004 }
7005 title={__("Move down", "yatra")}
7006 >
7007 <ChevronDown className="w-4 h-4" />
7008 </Button>
7009 )}
7010 <Button
7011 type="button"
7012 variant="ghost"
7013 size="sm"
7014 onClick={() =>
7015 handleDownloadableItemRemove(index)
7016 }
7017 className="text-red-600 hover:text-red-700"
7018 title={__("Remove", "yatra")}
7019 >
7020 <X className="w-4 h-4" />
7021 </Button>
7022 </div>
7023 </div>
7024
7025 <div>
7026 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7027 {__("Description", "yatra")} (
7028 {__("Optional", "yatra")})
7029 </label>
7030 <textarea
7031 value={item.description}
7032 onChange={(e) =>
7033 handleDownloadableItemChange(
7034 index,
7035 "description",
7036 e.target.value,
7037 )
7038 }
7039 rows={3}
7040 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"
7041 />
7042 </div>
7043
7044 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7045 <div>
7046 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7047 {__("Visibility", "yatra")}
7048 </label>
7049 <Select
7050 value={item.visibility}
7051 onChange={(e) =>
7052 handleDownloadableItemChange(
7053 index,
7054 "visibility",
7055 e.target.value,
7056 )
7057 }
7058 >
7059 <option value="public">
7060 {__("Public", "yatra")}
7061 </option>
7062 <option value="logged_in">
7063 {__("Private (My Account only)", "yatra")}
7064 </option>
7065 <option value="booked_only">
7066 {__(
7067 "Private (Booking confirmation only)",
7068 "yatra",
7069 )}
7070 </option>
7071 </Select>
7072 </div>
7073 <div>
7074 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7075 {__("File", "yatra")}
7076 </label>
7077 <div className="flex items-center gap-2">
7078 {item.attachment_url ? (
7079 <a
7080 href={item.attachment_url}
7081 target="_blank"
7082 rel="noreferrer"
7083 className="flex-shrink-0"
7084 title={__("View file", "yatra")}
7085 >
7086 {/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(
7087 item.attachment_url,
7088 ) ? (
7089 <img
7090 src={item.attachment_url}
7091 alt={
7092 item.attachment_title ||
7093 __("Selected file", "yatra")
7094 }
7095 className="w-9 h-9 rounded border border-gray-200 dark:border-gray-700 object-cover bg-white dark:bg-gray-900"
7096 />
7097 ) : (
7098 <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">
7099 <FileText className="w-4 h-4 text-gray-500 dark:text-gray-400" />
7100 </div>
7101 )}
7102 </a>
7103 ) : (
7104 <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">
7105 <FileText className="w-4 h-4 text-gray-400 dark:text-gray-600" />
7106 </div>
7107 )}
7108
7109 <div className="min-w-0 flex-1">
7110 <div className="flex items-center gap-2 min-w-0">
7111 <Button
7112 type="button"
7113 variant="outline"
7114 onClick={() =>
7115 handleDownloadableItemSelectFile(index)
7116 }
7117 className="flex items-center gap-2 flex-shrink-0"
7118 >
7119 <Upload className="w-4 h-4" />
7120 {item.attachment_id
7121 ? __("Change File", "yatra")
7122 : __("Select File", "yatra")}
7123 </Button>
7124 <span className="text-xs text-gray-600 dark:text-gray-400 truncate min-w-0">
7125 {item.attachment_title ||
7126 (item.attachment_id
7127 ? `#${item.attachment_id}`
7128 : __("No file selected", "yatra"))}
7129 </span>
7130 {item.attachment_url && (
7131 <a
7132 href={item.attachment_url}
7133 target="_blank"
7134 rel="noreferrer"
7135 className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 flex-shrink-0"
7136 >
7137 <Eye className="w-3 h-3" />
7138 {__("View", "yatra")}
7139 </a>
7140 )}
7141 </div>
7142 </div>
7143 </div>
7144 </div>
7145 </div>
7146
7147 <div className="flex items-start gap-3">
7148 <div className="flex items-center gap-2 pt-0.5">
7149 <input
7150 type="checkbox"
7151 checked={item.enabled}
7152 onChange={(e) =>
7153 handleDownloadableItemChange(
7154 index,
7155 "enabled",
7156 e.target.checked,
7157 )
7158 }
7159 className="h-4 w-4"
7160 />
7161 <span className="text-sm text-gray-700 dark:text-gray-300">
7162 {__("Enabled", "yatra")}
7163 </span>
7164 </div>
7165 <div className="flex-1">
7166 <p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed">
7167 {__(
7168 "Controls whether this file appears and is available for download on the trip page. When disabled, the file is completely hidden from users.",
7169 "yatra",
7170 )}
7171 </p>
7172 </div>
7173 </div>
7174 </div>
7175 ))}
7176 </div>
7177 ) : (
7178 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
7179 <FileText className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
7180 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
7181 {__("No downloads added yet", "yatra")}
7182 </p>
7183 </div>
7184 )}
7185
7186 <Button
7187 type="button"
7188 variant="outline"
7189 onClick={handleDownloadableItemAdd}
7190 className="w-full"
7191 >
7192 <Plus className="w-4 h-4 mr-2" />
7193 {__("Add Download", "yatra")}
7194 </Button>
7195 </CardContent>
7196 </Card>
7197 </div>
7198 );
7199
7200 case "pricing":
7201 return (
7202 <div className="space-y-4">
7203 <div className="flex flex-wrap items-center gap-2 mb-2">
7204 <DollarSign className="w-5 h-5 text-gray-500" />
7205 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7206 {__("Pricing & Payment", "yatra")}
7207 </h2>
7208 <Badge
7209 variant="outline"
7210 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"
7211 >
7212 {__("Recommended", "yatra")}
7213 </Badge>
7214 </div>
7215 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
7216 {__("How much does it cost?", "yatra")}
7217 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
7218 {__(
7219 "Set pricing for different traveler types and payment options. Optional for drafts, but adding pricing now makes publishing easier.",
7220 "yatra",
7221 )}
7222 </span>
7223 </p>
7224
7225 <div className="space-y-4">
7226 {/* Pricing Type Selection */}
7227 <div>
7228 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-3">
7229 {__("Pricing Type", "yatra")}
7230 </label>
7231 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7232 <label
7233 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
7234 formData.pricing_type === "regular"
7235 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
7236 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
7237 }`}
7238 >
7239 <input
7240 type="radio"
7241 name="pricing_type"
7242 value="regular"
7243 checked={formData.pricing_type === "regular"}
7244 onChange={(e) =>
7245 handleFieldChange("pricing_type", e.target.value)
7246 }
7247 className="sr-only"
7248 />
7249 <div className="flex flex-1">
7250 <div className="flex flex-col">
7251 <span
7252 className={`block text-sm font-semibold ${
7253 formData.pricing_type === "regular"
7254 ? "text-blue-900 dark:text-blue-300"
7255 : "text-gray-900 dark:text-white"
7256 }`}
7257 >
7258 {__("Regular Pricing", "yatra")}
7259 </span>
7260 <span
7261 className={`mt-1 flex items-center text-sm ${
7262 formData.pricing_type === "regular"
7263 ? "text-blue-700 dark:text-blue-400"
7264 : "text-gray-500 dark:text-gray-400"
7265 }`}
7266 >
7267 {__("Set a single price for all travelers", "yatra")}
7268 </span>
7269 </div>
7270 </div>
7271 {formData.pricing_type === "regular" && (
7272 <div className="absolute top-4 right-4">
7273 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
7274 </div>
7275 )}
7276 </label>
7277
7278 <label
7279 className={`relative flex cursor-pointer rounded-lg border p-4 focus:outline-none ${
7280 formData.pricing_type === "traveler_based"
7281 ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-600"
7282 : "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
7283 }`}
7284 >
7285 <input
7286 type="radio"
7287 name="pricing_type"
7288 value="traveler_based"
7289 checked={formData.pricing_type === "traveler_based"}
7290 onChange={(e) =>
7291 handleFieldChange("pricing_type", e.target.value)
7292 }
7293 className="sr-only"
7294 />
7295 <div className="flex flex-1">
7296 <div className="flex flex-col">
7297 <span
7298 className={`block text-sm font-semibold ${
7299 formData.pricing_type === "traveler_based"
7300 ? "text-blue-900 dark:text-blue-300"
7301 : "text-gray-900 dark:text-white"
7302 }`}
7303 >
7304 {__("Traveler-Based Pricing", "yatra")}
7305 </span>
7306 <span
7307 className={`mt-1 flex items-center text-sm ${
7308 formData.pricing_type === "traveler_based"
7309 ? "text-blue-700 dark:text-blue-400"
7310 : "text-gray-500 dark:text-gray-400"
7311 }`}
7312 >
7313 {__(
7314 "Set different prices for each traveler category",
7315 "yatra",
7316 )}
7317 </span>
7318 </div>
7319 </div>
7320 {formData.pricing_type === "traveler_based" && (
7321 <div className="absolute top-4 right-4">
7322 <div className="h-4 w-4 rounded-full bg-blue-600"></div>
7323 </div>
7324 )}
7325 </label>
7326 </div>
7327 </div>
7328
7329 {/* Regular Pricing */}
7330 {formData.pricing_type === "regular" && (
7331 <div className="space-y-4">
7332 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7333 <div>
7334 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7335 {__("Original Price", "yatra")} (
7336 {__("Optional", "yatra")})
7337 </label>
7338 <div className="relative">
7339 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7340 {getCurrencySymbol(globalCurrency)}
7341 </span>
7342 <Input
7343 type="number"
7344 step="0.01"
7345 min="0"
7346 value={formData.original_price}
7347 onChange={(e) =>
7348 handleFieldChange("original_price", e.target.value)
7349 }
7350 placeholder="0.00"
7351 className={`pl-7 ${errors.original_price ? "border-red-500" : ""}`}
7352 />
7353 </div>
7354 {errors.original_price && (
7355 <p className="mt-1 text-xs text-red-600">
7356 {errors.original_price}
7357 </p>
7358 )}
7359 </div>
7360
7361 <div>
7362 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
7363 {__("Discounted Price", "yatra")} (
7364 {__("Optional", "yatra")})
7365 </label>
7366 <div className="relative">
7367 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7368 {getCurrencySymbol(globalCurrency)}
7369 </span>
7370 <Input
7371 type="number"
7372 step="0.01"
7373 min="0"
7374 value={formData.discounted_price}
7375 onChange={(e) =>
7376 handleFieldChange(
7377 "discounted_price",
7378 e.target.value,
7379 )
7380 }
7381 placeholder="0.00"
7382 className={`pl-7 ${errors.discounted_price ? "border-red-500" : ""}`}
7383 />
7384 </div>
7385 {errors.discounted_price && (
7386 <p className="mt-1 text-xs text-red-600">
7387 {errors.discounted_price}
7388 </p>
7389 )}
7390 </div>
7391 </div>
7392 </div>
7393 )}
7394
7395 {/* Traveler Category Pricing */}
7396 {formData.pricing_type === "traveler_based" && (
7397 <div>
7398 <div className="flex items-center justify-between mb-3">
7399 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400">
7400 {__("Traveler Category Pricing", "yatra")}
7401 </label>
7402 </div>
7403 <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
7404 {__(
7405 "Add pricing for traveler categories. Categories are managed in Traveler Categories page.",
7406 "yatra",
7407 )}
7408 </p>
7409 {isLoadingCategories ? (
7410 <div className="flex items-center justify-center py-8">
7411 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
7412 </div>
7413 ) : activeCategories.length === 0 ? (
7414 <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
7415 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
7416 {__("No active traveler categories found.", "yatra")}
7417 </p>
7418 <Button
7419 type="button"
7420 variant="outline"
7421 onClick={() =>
7422 (window.location.href =
7423 "?page=yatra&subpage=traveler-categories&action=create")
7424 }
7425 className="flex items-center gap-2 mx-auto"
7426 >
7427 <Plus className="w-4 h-4" />
7428 {__("Create Category", "yatra")}
7429 </Button>
7430 </div>
7431 ) : (
7432 <div className="space-y-4">
7433 {/* Add Pricing Button */}
7434 <div className="relative">
7435 <Button
7436 type="button"
7437 variant="outline"
7438 onClick={() =>
7439 setShowCategorySelector(!showCategorySelector)
7440 }
7441 className="flex items-center gap-2"
7442 disabled={
7443 activeCategories.filter(
7444 (cat) =>
7445 !formData.price_types.some(
7446 (pt) =>
7447 Number(pt.category_id) === Number(cat.id),
7448 ),
7449 ).length === 0
7450 }
7451 >
7452 <Plus className="w-4 h-4" />
7453 {__("Add Pricing", "yatra")}
7454 </Button>
7455
7456 {/* Category Selection Dropdown */}
7457 {showCategorySelector && (
7458 <>
7459 <div
7460 className="fixed inset-0 z-10"
7461 onClick={() => setShowCategorySelector(false)}
7462 />
7463 <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">
7464 <div className="p-2">
7465 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
7466 {__(
7467 "Select a category to add pricing",
7468 "yatra",
7469 )}
7470 </div>
7471 {activeCategories.filter(
7472 (cat) =>
7473 !formData.price_types.some(
7474 (pt) =>
7475 Number(pt.category_id) ===
7476 Number(cat.id),
7477 ),
7478 ).length === 0 ? (
7479 <div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
7480 {__(
7481 "All categories have pricing added",
7482 "yatra",
7483 )}
7484 </div>
7485 ) : (
7486 <div className="space-y-1">
7487 {activeCategories
7488 .filter(
7489 (cat) =>
7490 !formData.price_types.some(
7491 (pt) =>
7492 Number(pt.category_id) ===
7493 Number(cat.id),
7494 ),
7495 )
7496 .map((category) => {
7497 const ageRange =
7498 category.age_min !== undefined ||
7499 category.age_max !== undefined
7500 ? category.age_min !== undefined &&
7501 category.age_max !== undefined
7502 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
7503 : category.age_min !== undefined
7504 ? `${category.age_min}+ ${__("years", "yatra")}`
7505 : category.age_max !== undefined
7506 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
7507 : ""
7508 : null;
7509
7510 const pricingMode =
7511 category.pricing_mode || "per_person";
7512 const hasMin =
7513 category.min_pax !== null &&
7514 category.min_pax !== undefined;
7515 const hasMax =
7516 category.max_pax !== null &&
7517 category.max_pax !== undefined;
7518
7519 let pricingLabel = "";
7520 if (pricingMode === "per_group") {
7521 if (hasMin && hasMax) {
7522 pricingLabel = `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`;
7523 } else if (hasMin) {
7524 pricingLabel = `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`;
7525 } else if (hasMax) {
7526 pricingLabel = `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`;
7527 } else {
7528 pricingLabel = __(
7529 "Per group",
7530 "yatra",
7531 );
7532 }
7533 } else {
7534 pricingLabel = __(
7535 "Per person",
7536 "yatra",
7537 );
7538 }
7539
7540 return (
7541 <button
7542 key={category.id}
7543 type="button"
7544 onClick={() => {
7545 handlePriceTypeAdd(category.id);
7546 setShowCategorySelector(false);
7547 }}
7548 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
7549 >
7550 <div className="font-medium text-sm text-gray-900 dark:text-white">
7551 {category.label}
7552 {ageRange && (
7553 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
7554 ({ageRange})
7555 </span>
7556 )}
7557 </div>
7558 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7559 {category.description}
7560 {pricingLabel && (
7561 <span className="ml-1 text-[11px] font-medium text-gray-600 dark:text-gray-300">
7562 • {pricingLabel}
7563 </span>
7564 )}
7565 </div>
7566 </button>
7567 );
7568 })}
7569 </div>
7570 )}
7571 </div>
7572 </div>
7573 </>
7574 )}
7575 </div>
7576
7577 {/* Added Pricing List */}
7578 {formData.price_types.length > 0 && (
7579 <div className="space-y-3">
7580 {formData.price_types.map((priceType, index) => {
7581 // Use Number() to ensure consistent comparison (handles string/number mismatch)
7582 const category = activeCategories.find(
7583 (cat) =>
7584 Number(cat.id) ===
7585 Number(priceType.category_id),
7586 );
7587 if (!category) {
7588 return null;
7589 }
7590
7591 return (
7592 <div
7593 key={priceType.category_id}
7594 className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
7595 >
7596 <div className="flex items-start justify-between mb-3">
7597 <div className="flex-1">
7598 <div className="flex items-center gap-2 mb-1">
7599 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
7600 {category.label}
7601 {(category.age_min !== undefined ||
7602 category.age_max !== undefined) && (
7603 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
7604 (
7605 {category.age_min !== undefined &&
7606 category.age_max !== undefined
7607 ? `${category.age_min}-${category.age_max} ${__("years", "yatra")}`
7608 : category.age_min !== undefined
7609 ? `${category.age_min}+ ${__("years", "yatra")}`
7610 : category.age_max !== undefined
7611 ? `${__("Under", "yatra")} ${category.age_max} ${__("years", "yatra")}`
7612 : ""}
7613 )
7614 </span>
7615 )}
7616
7617 {/* Pricing mode & group size */}
7618 <span className="ml-2 inline-flex items-center gap-1 text-[11px] font-medium text-gray-600 dark:text-gray-300">
7619 {(() => {
7620 const pricingMode =
7621 category.pricing_mode ||
7622 "per_person";
7623 const hasMin =
7624 category.min_pax !== null &&
7625 category.min_pax !== undefined;
7626 const hasMax =
7627 category.max_pax !== null &&
7628 category.max_pax !== undefined;
7629
7630 if (pricingMode === "per_group") {
7631 if (hasMin && hasMax) {
7632 return `${__("Per group", "yatra")} (${category.min_pax}-${category.max_pax})`;
7633 }
7634 if (hasMin) {
7635 return `${__("Per group", "yatra")} (${__("From", "yatra")} ${category.min_pax})`;
7636 }
7637 if (hasMax) {
7638 return `${__("Per group", "yatra")} (${__("Up to", "yatra")} ${category.max_pax})`;
7639 }
7640 return __("Per group", "yatra");
7641 }
7642
7643 return __("Per person", "yatra");
7644 })()}
7645 </span>
7646 </h4>
7647 </div>
7648 <p className="text-xs text-gray-600 dark:text-gray-400">
7649 {category.description}
7650 </p>
7651 </div>
7652 <button
7653 type="button"
7654 onClick={() =>
7655 handlePriceTypeRemove(category.id)
7656 }
7657 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
7658 title={__("Remove Pricing", "yatra")}
7659 >
7660 <X className="w-4 h-4" />
7661 </button>
7662 </div>
7663 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
7664 <div>
7665 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
7666 {__("Original Price", "yatra")} (
7667 {__("Optional", "yatra")})
7668 </label>
7669 <div className="relative">
7670 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7671 {getCurrencySymbol(globalCurrency)}
7672 </span>
7673 <Input
7674 type="number"
7675 step="0.01"
7676 min="0"
7677 value={priceType.original_price}
7678 onChange={(e) =>
7679 handlePriceTypeChange(
7680 category.id,
7681 "original_price",
7682 e.target.value,
7683 )
7684 }
7685 placeholder="0.00"
7686 className={`pl-7 ${errors[`price_type_${index}_original`] ? "border-red-500" : ""}`}
7687 />
7688 </div>
7689 {errors[`price_type_${index}_original`] && (
7690 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
7691 {errors[`price_type_${index}_original`]}
7692 </p>
7693 )}
7694 </div>
7695 <div>
7696 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
7697 {__("Discounted Price", "yatra")} (
7698 {__("Optional", "yatra")})
7699 </label>
7700 <div className="relative">
7701 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
7702 {getCurrencySymbol(globalCurrency)}
7703 </span>
7704 <Input
7705 type="number"
7706 step="0.01"
7707 min="0"
7708 value={priceType.discounted_price}
7709 onChange={(e) =>
7710 handlePriceTypeChange(
7711 category.id,
7712 "discounted_price",
7713 e.target.value,
7714 )
7715 }
7716 placeholder="0.00"
7717 className={`pl-7 ${errors[`price_type_${index}_discounted`] ? "border-red-500" : ""}`}
7718 />
7719 </div>
7720 {errors[
7721 `price_type_${index}_discounted`
7722 ] && (
7723 <p className="mt-1 text-xs text-red-600 dark:text-red-400">
7724 {
7725 errors[
7726 `price_type_${index}_discounted`
7727 ]
7728 }
7729 </p>
7730 )}
7731 </div>
7732 </div>
7733
7734 <div className="mt-3 flex items-center justify-between">
7735 <label className="inline-flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-300">
7736 <input
7737 type="checkbox"
7738 checked={Boolean(priceType.is_default)}
7739 onChange={(e) =>
7740 handlePriceTypeDefaultChange(
7741 category.id,
7742 e.target.checked,
7743 )
7744 }
7745 className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
7746 />
7747 {__(
7748 "Default price (used on page load)",
7749 "yatra",
7750 )}
7751 </label>
7752 <span className="text-[11px] text-gray-500 dark:text-gray-400">
7753 {__("Traveler-based pricing only", "yatra")}
7754 </span>
7755 </div>
7756 </div>
7757 );
7758 })}
7759 </div>
7760 )}
7761
7762 {/* Empty State */}
7763 {formData.price_types.length === 0 && (
7764 <div className="p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center">
7765 <Tag className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
7766 <p className="text-sm text-gray-600 dark:text-gray-400">
7767 {__(
7768 "No pricing added yet. Select a category above to add pricing.",
7769 "yatra",
7770 )}
7771 </p>
7772 </div>
7773 )}
7774 </div>
7775 )}
7776 {errors.price_types && (
7777 <p className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
7778 <AlertCircle className="w-4 h-4" />
7779 {errors.price_types}
7780 </p>
7781 )}
7782 </div>
7783 )}
7784
7785 {/*
7786 Deposit & Payment Terms — Pro feature (FlexiblePayments module).
7787 The DB columns live in the free TripsTable schema so existing data
7788 survives toggling Pro on/off. Editing is gated on
7789 window.yatraAdmin.flexiblePaymentsEnabled (set true by Pro's
7790 FlexiblePaymentsModule::addAdminData() on yatra_admin_localized_data).
7791
7792 We deliberately render the section in all three states (free / Pro
7793 installed-but-module-off / Pro+module-on) so the feature is
7794 discoverable in the free plugin. When unlocked → editable. When
7795 locked → fields shown disabled inside a <fieldset disabled>, with a
7796 PRO badge on the heading and a context-aware info banner above:
7797 - free: "Upgrade to Pro" link
7798 - Pro installed but module disabled: "Activate Module" link
7799 */}
7800 {(() => {
7801 const flexiblePaymentsEnabled = Boolean(
7802 (window as any)?.yatraAdmin?.flexiblePaymentsEnabled,
7803 );
7804 const isLocked = !flexiblePaymentsEnabled;
7805 const siteUrl =
7806 (window as any)?.yatraAdmin?.siteUrl ||
7807 "/wp-admin/admin.php?page=yatra";
7808 const modulesPageUrl = `${siteUrl}/wp-admin/admin.php?page=yatra&subpage=modules`;
7809 const pricingUrl = "https://mantrabrain.com/yatra-pricing/";
7810
7811 return (
7812 <div className="border-t border-gray-200 dark:border-gray-700 pt-4">
7813 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
7814 {__("Deposit & Payment Terms", "yatra")}
7815 {isLocked && <ProBadge isProActive={isPro} />}
7816 </h3>
7817
7818 {isLocked && (
7819 <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">
7820 <svg
7821 className="w-4 h-4 mt-0.5 text-purple-500 flex-shrink-0"
7822 fill="none"
7823 stroke="currentColor"
7824 viewBox="0 0 24 24"
7825 aria-hidden="true"
7826 >
7827 <path
7828 strokeLinecap="round"
7829 strokeLinejoin="round"
7830 strokeWidth={2}
7831 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"
7832 />
7833 </svg>
7834 <div className="flex-1">
7835 {isPro ? (
7836 <>
7837 {__(
7838 "Per-trip deposits need the Flexible Payments module.",
7839 "yatra",
7840 )}{" "}
7841 <a
7842 href={modulesPageUrl}
7843 target="_blank"
7844 rel="noopener noreferrer"
7845 className="underline font-medium"
7846 >
7847 {__("Activate module", "yatra")}
7848 </a>
7849 </>
7850 ) : (
7851 <>
7852 {__(
7853 "Per-trip deposit amount, percentage and custom payment terms are a Yatra Pro feature.",
7854 "yatra",
7855 )}{" "}
7856 <a
7857 href={pricingUrl}
7858 target="_blank"
7859 rel="noopener noreferrer"
7860 className="underline font-medium"
7861 >
7862 {__("Upgrade to Pro", "yatra")}
7863 </a>
7864 </>
7865 )}
7866 </div>
7867 </div>
7868 )}
7869
7870 {/*
7871 <fieldset disabled> is the HTML-standard way to make every
7872 form control inside it non-interactive in one go — works
7873 for our <Input> and <textarea> without per-field disabled props.
7874 */}
7875 <fieldset
7876 disabled={isLocked}
7877 className={
7878 isLocked
7879 ? "opacity-60 cursor-not-allowed select-none"
7880 : ""
7881 }
7882 aria-label={
7883 isLocked
7884 ? __(
7885 "Deposit & Payment Terms (Pro feature, disabled)",
7886 "yatra",
7887 )
7888 : undefined
7889 }
7890 >
7891 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7892 <div>
7893 <label
7894 htmlFor="deposit_amount"
7895 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7896 >
7897 {__("Deposit Amount", "yatra")}
7898 </label>
7899 <div className="relative">
7900 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
7901 {getCurrencySymbol(globalCurrency)}
7902 </span>
7903 <Input
7904 id="deposit_amount"
7905 type="number"
7906 step="0.01"
7907 min="0"
7908 value={formData.deposit_amount}
7909 onChange={(e) =>
7910 handleFieldChange(
7911 "deposit_amount",
7912 e.target.value,
7913 )
7914 }
7915 placeholder="0.00"
7916 className="pl-7"
7917 />
7918 </div>
7919 </div>
7920 <div>
7921 <label
7922 htmlFor="deposit_percentage"
7923 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7924 >
7925 {__("Deposit Percentage", "yatra")}
7926 </label>
7927 <div className="relative">
7928 <Input
7929 id="deposit_percentage"
7930 type="number"
7931 step="0.01"
7932 min="0"
7933 max="100"
7934 value={formData.deposit_percentage}
7935 onChange={(e) =>
7936 handleFieldChange(
7937 "deposit_percentage",
7938 e.target.value,
7939 )
7940 }
7941 placeholder="0"
7942 className="pr-7"
7943 />
7944 <span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500">
7945 %
7946 </span>
7947 </div>
7948 </div>
7949 </div>
7950 <div className="mt-4">
7951 <label
7952 htmlFor="payment_terms"
7953 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
7954 >
7955 {__("Payment Terms", "yatra")}
7956 </label>
7957 <textarea
7958 id="payment_terms"
7959 value={formData.payment_terms}
7960 onChange={(e) =>
7961 handleFieldChange("payment_terms", e.target.value)
7962 }
7963 placeholder={__(
7964 "e.g., 50% deposit required at booking, balance due 30 days before departure",
7965 "yatra",
7966 )}
7967 rows={2}
7968 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"
7969 />
7970 </div>
7971 </fieldset>
7972 </div>
7973 );
7974 })()}
7975
7976 {/* Group Pricing */}
7977 {/* Removed */}
7978 </div>
7979 </div>
7980 );
7981
7982 case "attributes":
7983 // Redirect to categorization section (attributes now merged there)
7984 setCurrentSection("categorization");
7985 return null;
7986
7987 case "itinerary":
7988 return (
7989 <div className="space-y-6">
7990 {/* Header */}
7991 <div className="flex flex-wrap items-center gap-2 mb-2">
7992 <BookOpen className="w-5 h-5 text-gray-500" />
7993 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
7994 {__("Trip Details", "yatra")}
7995 </h2>
7996 <Badge
7997 variant="outline"
7998 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"
7999 >
8000 {__("Recommended", "yatra")}
8001 </Badge>
8002 </div>
8003 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
8004 {__("What will travelers experience?", "yatra")}
8005 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
8006 {__(
8007 "Build your day-by-day itinerary and list what's included/excluded to set clear expectations.",
8008 "yatra",
8009 )}
8010 </span>
8011 </p>
8012
8013 {/* Sub-tabs */}
8014 <div className="border-b border-gray-200 dark:border-gray-700">
8015 <nav className="flex gap-6" aria-label="Trip Details Tabs">
8016 <button
8017 onClick={() => setTripDetailsTab("itinerary")}
8018 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
8019 tripDetailsTab === "itinerary"
8020 ? "border-blue-500 text-blue-600 dark:text-blue-400"
8021 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
8022 }`}
8023 >
8024 <div className="flex items-center gap-2">
8025 <Calendar className="w-4 h-4" />
8026 {__("Itinerary", "yatra")}
8027 </div>
8028 </button>
8029 <button
8030 onClick={() => setTripDetailsTab("included")}
8031 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
8032 tripDetailsTab === "included"
8033 ? "border-blue-500 text-blue-600 dark:text-blue-400"
8034 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
8035 }`}
8036 >
8037 <div className="flex items-center gap-2">
8038 <CheckSquare className="w-4 h-4" />
8039 {__("Included/Excluded", "yatra")}
8040 </div>
8041 </button>
8042 </nav>
8043 </div>
8044
8045 {/* Tab Content */}
8046 <div className="mt-6">
8047 {tripDetailsTab === "itinerary" && (
8048 <>
8049 {isAiEligible() && isAiModuleEnabled() && (
8050 <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">
8051 <div className="text-xs text-blue-900 dark:text-blue-200">
8052 <Sparkles className="mr-1 inline h-3.5 w-3.5" />
8053 {isEditMode
8054 ? __(
8055 "Let AI draft a day-by-day plan with real activity blocks — you can edit each day after.",
8056 "yatra",
8057 )
8058 : __(
8059 "Save the trip first to enable AI itinerary generation — the agent needs a trip ID to attach days to.",
8060 "yatra",
8061 )}
8062 </div>
8063 <Button
8064 type="button"
8065 size="sm"
8066 variant="outline"
8067 className="border-blue-300 text-blue-700 hover:bg-blue-100 dark:border-blue-500/60 dark:text-blue-200"
8068 disabled={!isEditMode || !tripId}
8069 onClick={() => {
8070 // Same modal the Itinerary page uses, so all
8071 // three entry points share one agent + one
8072 // persistence path.
8073 if (isEditMode && tripId) {
8074 setItineraryBuildOpen(true);
8075 }
8076 }}
8077 >
8078 <Sparkles className="mr-1 h-3.5 w-3.5" />
8079 {__("Build with AI", "yatra")}
8080 </Button>
8081 </div>
8082 )}
8083 <ItinerarySection
8084 formData={formData as any}
8085 isEditMode={isEditMode}
8086 tripId={tripId}
8087 />
8088 </>
8089 )}
8090 {tripDetailsTab === "included" && (
8091 <IncludedSection
8092 formData={formData}
8093 errors={errors}
8094 handleFieldChange={handleFieldChange as any}
8095 />
8096 )}
8097 </div>
8098 </div>
8099 );
8100
8101 case "included":
8102 // Redirect to itinerary with included tab selected
8103 setCurrentSection("itinerary");
8104 setTripDetailsTab("included");
8105 return null;
8106
8107 case "faqs":
8108 // Redirect to seo section (FAQs now merged there)
8109 setCurrentSection("seo");
8110 return null;
8111
8112 // Removed: case 'frontend-tabs' - now part of 'advanced' case
8113
8114 case "seo":
8115 return (
8116 <div className="space-y-6">
8117 <div className="flex flex-wrap items-center gap-2 mb-2">
8118 <Search className="w-5 h-5 text-gray-500" />
8119 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
8120 {__("SEO & Marketing", "yatra")}
8121 </h2>
8122 <Badge
8123 variant="outline"
8124 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"
8125 >
8126 {__("Recommended", "yatra")}
8127 </Badge>
8128 </div>
8129 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
8130 {__("How will people find & trust this trip?", "yatra")}
8131 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
8132 {__(
8133 "Optimize for search engines and answer common questions to build trust and improve discoverability.",
8134 "yatra",
8135 )}
8136 </span>
8137 </p>
8138
8139 <div className="space-y-6">
8140 {/* SECTION 1: SEO Settings */}
8141 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
8142 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
8143 <Search className="w-4 h-4 text-blue-500" />
8144 {__("Search Engine Optimization", "yatra")}
8145 </h3>
8146 <div className="space-y-4">
8147 <div>
8148 <div className="flex items-center justify-between mb-1.5">
8149 <label
8150 htmlFor="meta_title"
8151 className="block text-xs font-normal text-gray-500 dark:text-gray-400"
8152 >
8153 {__("Meta Title", "yatra")}
8154 </label>
8155 <AiFieldAffordance
8156 task="seo-meta-title"
8157 label={__("Meta Title", "yatra")}
8158 value={formData.meta_title}
8159 onAccept={(v) =>
8160 handleFieldChange("meta_title", v.slice(0, 60))
8161 }
8162 buildContext={() => buildTripAiContext(formData)}
8163 />
8164 </div>
8165 <Input
8166 id="meta_title"
8167 type="text"
8168 value={formData.meta_title}
8169 onChange={(e) =>
8170 handleFieldChange("meta_title", e.target.value)
8171 }
8172 placeholder={formData.title || __("Trip Title", "yatra")}
8173 maxLength={60}
8174 />
8175 <HelpText
8176 text={__(
8177 "Best for search engines: 50-60 characters. Include main keywords.",
8178 "yatra",
8179 )}
8180 className="mt-1"
8181 />
8182 </div>
8183 <div>
8184 <div className="flex items-center justify-between mb-1.5">
8185 <label
8186 htmlFor="meta_description"
8187 className="block text-xs font-normal text-gray-500 dark:text-gray-400"
8188 >
8189 {__("Meta Description", "yatra")}
8190 </label>
8191 <AiFieldAffordance
8192 task="seo-meta-description"
8193 label={__("Meta Description", "yatra")}
8194 value={formData.meta_description}
8195 onAccept={(v) =>
8196 handleFieldChange("meta_description", v.slice(0, 160))
8197 }
8198 buildContext={() => buildTripAiContext(formData)}
8199 />
8200 </div>
8201 <textarea
8202 id="meta_description"
8203 value={formData.meta_description}
8204 onChange={(e) =>
8205 handleFieldChange("meta_description", e.target.value)
8206 }
8207 rows={3}
8208 maxLength={160}
8209 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"
8210 placeholder={__(
8211 "Compelling description that includes key travel terms...",
8212 "yatra",
8213 )}
8214 />
8215 <HelpText
8216 text={__(
8217 "160 characters max. Include location, duration, and key features.",
8218 "yatra",
8219 )}
8220 className="mt-1"
8221 />
8222 </div>
8223 <div>
8224 <label
8225 htmlFor="meta_keywords"
8226 className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5"
8227 >
8228 {__("Meta Keywords", "yatra")}
8229 </label>
8230 <Input
8231 id="meta_keywords"
8232 type="text"
8233 value={formData.meta_keywords}
8234 onChange={(e) =>
8235 handleFieldChange("meta_keywords", e.target.value)
8236 }
8237 placeholder={__(
8238 "adventure, travel, tour, guide, experience",
8239 "yatra",
8240 )}
8241 />
8242 <HelpText
8243 text={__(
8244 "Comma-separated keywords. Include location, activities, and travel terms.",
8245 "yatra",
8246 )}
8247 className="mt-1"
8248 />
8249 </div>
8250 </div>
8251
8252 {/* SEO Preview */}
8253 <Card>
8254 <CardHeader>
8255 <CardTitle>{__("SEO Preview", "yatra")}</CardTitle>
8256 <CardDescription>
8257 {__(
8258 "Preview how your trip appears in search results",
8259 "yatra",
8260 )}
8261 </CardDescription>
8262 </CardHeader>
8263 <CardContent className="space-y-4">
8264 <div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
8265 <div className="text-xs text-gray-500 dark:text-gray-400 mb-2">
8266 {__("Google Search Result Preview", "yatra")}
8267 </div>
8268 <div className="space-y-2">
8269 <div className="text-blue-800 dark:text-blue-400 text-sm font-medium">
8270 {sanitizeTextForSEO(
8271 formData.meta_title ||
8272 formData.title ||
8273 "Trip Title",
8274 60,
8275 )}
8276 </div>
8277 <div className="text-green-700 dark:text-green-400 text-sm">
8278 {formData.meta_description
8279 ? sanitizeTextForSEO(formData.meta_description, 150)
8280 : formData.description
8281 ? sanitizeTextForSEO(formData.description, 150)
8282 : "Trip description..."}
8283 </div>
8284 <div className="text-gray-600 dark:text-gray-400 text-xs">
8285 {window.location.hostname}/trip/
8286 {formData.slug || "trip-slug"}
8287 </div>
8288 </div>
8289 </div>
8290 </CardContent>
8291 </Card>
8292 </div>
8293
8294 {/* SECTION 2: FAQs */}
8295 <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5 bg-gray-50 dark:bg-gray-800/30">
8296 <div className="mb-4 flex items-center justify-between gap-2">
8297 <h3 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2">
8298 <HelpCircle className="w-4 h-4 text-blue-500" />
8299 {__("Frequently Asked Questions", "yatra")}
8300 </h3>
8301 {/* AI: generates 5-7 Q&A pairs. Result text is
8302 parsed from "Q: …\nA: …" blocks separated by
8303 blank lines — same shape as the trip-faq
8304 prompt's documented output. Accept replaces
8305 the existing FAQ list wholesale. */}
8306 <AiFieldAffordance
8307 task="trip-faq"
8308 label={__("FAQ", "yatra")}
8309 value={(formData.faqs ?? [])
8310 .map((f: any) => `Q: ${f.question}\nA: ${f.answer}`)
8311 .join("\n\n")}
8312 onAccept={(raw) => {
8313 const parsed: Array<{
8314 question: string;
8315 answer: string;
8316 }> = [];
8317 const blocks = raw
8318 .replace(/\r\n?/g, "\n")
8319 .split(/\n{2,}/);
8320 for (const block of blocks) {
8321 const q = block.match(/^\s*Q[:\.\-]\s*(.+)/im);
8322 const a = block.match(/A[:\.\-]\s*([\s\S]+)/im);
8323 if (q) {
8324 parsed.push({
8325 question: q[1].trim(),
8326 answer: a ? a[1].trim() : "",
8327 });
8328 }
8329 }
8330 if (parsed.length > 0) {
8331 handleFieldChange("faqs", parsed as any);
8332 }
8333 }}
8334 buildContext={() => buildTripAiContext(formData)}
8335 />
8336 </div>
8337 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8338 {__(
8339 "Answer common questions to build trust and reduce support inquiries. FAQs also help with SEO.",
8340 "yatra",
8341 )}
8342 </p>
8343 <div className="space-y-4">
8344 {formData.faqs.map((faq, index) => (
8345 <div
8346 key={index}
8347 className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-900"
8348 >
8349 <div className="flex items-start justify-between mb-3">
8350 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
8351 {__("FAQ", "yatra")} {index + 1}
8352 </h4>
8353 <button
8354 type="button"
8355 onClick={() => handleFAQRemove(index)}
8356 className="text-red-600 hover:text-red-700"
8357 >
8358 <X className="w-4 h-4" />
8359 </button>
8360 </div>
8361 <div className="space-y-3">
8362 <div>
8363 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8364 {__("Question", "yatra")}
8365 </label>
8366 <Input
8367 type="text"
8368 value={faq.question}
8369 onChange={(e) =>
8370 handleFAQChange(index, "question", e.target.value)
8371 }
8372 placeholder={__("Enter question...", "yatra")}
8373 />
8374 </div>
8375 <div>
8376 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8377 {__("Answer", "yatra")}
8378 </label>
8379 <textarea
8380 value={faq.answer}
8381 onChange={(e) =>
8382 handleFAQChange(index, "answer", e.target.value)
8383 }
8384 placeholder={__("Enter answer...", "yatra")}
8385 rows={3}
8386 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"
8387 />
8388 </div>
8389 </div>
8390 </div>
8391 ))}
8392 <Button
8393 type="button"
8394 variant="outline"
8395 onClick={handleFAQAdd}
8396 className="w-full"
8397 >
8398 <Plus className="w-4 h-4 mr-2" />
8399 {__("Add FAQ", "yatra")}
8400 </Button>
8401 </div>
8402 </div>
8403 </div>
8404 </div>
8405 );
8406
8407 case "advanced":
8408 return (
8409 <div className="space-y-6">
8410 <div className="flex flex-wrap items-center gap-2 mb-2">
8411 <Settings className="w-5 h-5 text-gray-500" />
8412 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
8413 {__("Lifecycle Management", "yatra")}
8414 </h2>
8415 <Badge
8416 variant="outline"
8417 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"
8418 >
8419 {__("Power Users", "yatra")}
8420 </Badge>
8421 </div>
8422 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
8423 {__("When should this trip be published or archived?", "yatra")}
8424 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-1">
8425 {__(
8426 "Schedule automatic publishing and unpublishing for seasonal trips. Optional but useful for managing trip lifecycles.",
8427 "yatra",
8428 )}
8429 </span>
8430 </p>
8431
8432 <Card>
8433 <CardHeader>
8434 <CardTitle>{__("Version Control", "yatra")}</CardTitle>
8435 <CardDescription>
8436 {__("Track changes and version history", "yatra")}
8437 </CardDescription>
8438 </CardHeader>
8439 <CardContent className="space-y-4">
8440 <div>
8441 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8442 {__("Version", "yatra")}
8443 </label>
8444 <Input
8445 type="number"
8446 value={formData.version}
8447 onChange={(e) =>
8448 handleFieldChange(
8449 "version",
8450 parseInt(e.target.value) || 1,
8451 )
8452 }
8453 min="1"
8454 readOnly
8455 className="bg-gray-50 dark:bg-gray-800"
8456 />
8457 <HelpText
8458 text={__(
8459 "Version number is automatically incremented when changes are saved",
8460 "yatra",
8461 )}
8462 className="mt-2"
8463 />
8464 </div>
8465 </CardContent>
8466 </Card>
8467
8468 <Card>
8469 <CardHeader>
8470 <CardTitle>{__("Scheduled Publishing", "yatra")}</CardTitle>
8471 <CardDescription>
8472 {__(
8473 "Automatically publish or unpublish your trip on specific dates",
8474 "yatra",
8475 )}
8476 </CardDescription>
8477 </CardHeader>
8478 <CardContent className="space-y-4">
8479 <div>
8480 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8481 {__("Schedule Publish Date", "yatra")}
8482 </label>
8483 <DatePicker
8484 value={formData.scheduled_publish_date}
8485 onChange={(val) =>
8486 handleFieldChange("scheduled_publish_date", val)
8487 }
8488 placeholder={__("Select date", "yatra")}
8489 />
8490 <HelpText
8491 text={__(
8492 "Trip will be automatically published on this date and time",
8493 "yatra",
8494 )}
8495 className="mt-2"
8496 />
8497 </div>
8498 <div>
8499 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8500 {__("Schedule Unpublish Date", "yatra")}
8501 </label>
8502 <DatePicker
8503 value={formData.scheduled_unpublish_date}
8504 onChange={(val) =>
8505 handleFieldChange("scheduled_unpublish_date", val)
8506 }
8507 placeholder={__("Select date", "yatra")}
8508 />
8509 <HelpText
8510 text={__(
8511 "Trip will be automatically unpublished (archived) on this date and time",
8512 "yatra",
8513 )}
8514 className="mt-2"
8515 />
8516 </div>
8517 </CardContent>
8518 </Card>
8519
8520 <Card>
8521 <CardHeader>
8522 <CardTitle>
8523 {__("Seasonal Auto-Enable/Disable", "yatra")}
8524 </CardTitle>
8525 <CardDescription>
8526 {__(
8527 "Automatically enable or disable trip availability based on seasonal dates",
8528 "yatra",
8529 )}
8530 </CardDescription>
8531 </CardHeader>
8532 <CardContent className="space-y-4">
8533 <div className="flex items-center gap-2">
8534 <input
8535 type="checkbox"
8536 id="seasonal_auto_enable"
8537 checked={formData.seasonal_auto_enable}
8538 onChange={(e) =>
8539 handleFieldChange(
8540 "seasonal_auto_enable",
8541 e.target.checked,
8542 )
8543 }
8544 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
8545 />
8546 <label
8547 htmlFor="seasonal_auto_enable"
8548 className="text-sm font-medium text-gray-700 dark:text-gray-300"
8549 >
8550 {__("Enable seasonal auto-management", "yatra")}
8551 </label>
8552 </div>
8553 {formData.seasonal_auto_enable && (
8554 <>
8555 <div>
8556 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8557 {__("Auto-Enable Date", "yatra")}
8558 </label>
8559 <DatePicker
8560 value={formData.seasonal_enable_date}
8561 onChange={(val) =>
8562 handleFieldChange("seasonal_enable_date", val)
8563 }
8564 placeholder={__("Select date", "yatra")}
8565 />
8566 <HelpText
8567 text={__(
8568 "Trip will become available for booking on this date",
8569 "yatra",
8570 )}
8571 className="mt-2"
8572 />
8573 </div>
8574 <div>
8575 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
8576 {__("Auto-Disable Date", "yatra")}
8577 </label>
8578 <DatePicker
8579 value={formData.seasonal_disable_date}
8580 onChange={(val) =>
8581 handleFieldChange("seasonal_disable_date", val)
8582 }
8583 placeholder={__("Select date", "yatra")}
8584 />
8585 <HelpText
8586 text={__(
8587 "Trip will become unavailable for booking on this date",
8588 "yatra",
8589 )}
8590 className="mt-2"
8591 />
8592 </div>
8593 </>
8594 )}
8595 </CardContent>
8596 </Card>
8597
8598 {/* Frontend Tabs Management */}
8599 <Card>
8600 <CardHeader>
8601 <CardTitle>{__("Frontend Tabs Management", "yatra")}</CardTitle>
8602 <CardDescription>
8603 {__(
8604 "Manage which tabs appear on the trip single page and in what order. Enable or disable tabs, customize labels, and reorder them.",
8605 "yatra",
8606 )}
8607 </CardDescription>
8608 </CardHeader>
8609 <CardContent>
8610 <div className="space-y-3">
8611 {[...formData.frontend_tabs]
8612 .sort((a, b) => a.order - b.order)
8613 .map((tab, index) => (
8614 <div
8615 key={tab.id}
8616 draggable
8617 onDragStart={(e) => handleDragStart(e, tab.id)}
8618 onDragOver={handleDragOver}
8619 onDragEnter={() => handleDragEnter(tab.id)}
8620 onDragLeave={handleDragLeave}
8621 onDrop={(e) => handleDrop(e, tab.id)}
8622 onDragEnd={handleDragEnd}
8623 className={`p-4 rounded-lg border cursor-move relative ${
8624 tab.enabled
8625 ? "bg-gray-50 dark:bg-gray-850 border-gray-200 dark:border-gray-700 shadow-sm"
8626 : "bg-gray-100 dark:bg-gray-900 border-gray-200 dark:border-gray-700 opacity-60 shadow-sm"
8627 } ${
8628 draggedTab === tab.id
8629 ? "opacity-50 scale-95 shadow-lg"
8630 : ""
8631 } ${
8632 dragOverTab === tab.id && draggedTab !== tab.id
8633 ? "border-blue-400 bg-blue-50 dark:bg-blue-900/20"
8634 : ""
8635 }`}
8636 >
8637 {tab.content_type === "custom" && (
8638 <Button
8639 type="button"
8640 variant="ghost"
8641 size="sm"
8642 onClick={() => handleTabRemove(tab.id)}
8643 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"
8644 title={__("Delete tab", "yatra")}
8645 >
8646 <Trash2 className="w-5 h-5" />
8647 </Button>
8648 )}
8649 <div className="flex items-center gap-3 mb-3">
8650 <GripVertical className="w-6 h-6 text-gray-400 cursor-grab active:cursor-grabbing" />
8651 <div className="flex-1">
8652 <div className="flex items-center gap-2 mb-2">
8653 <Input
8654 type="text"
8655 value={tab.label}
8656 onChange={(e) =>
8657 handleTabLabelChange(tab.id, e.target.value)
8658 }
8659 className="text-sm font-medium flex-1"
8660 disabled={!tab.enabled}
8661 placeholder={__("Tab Label", "yatra")}
8662 />
8663 <Badge variant="outline" className="text-xs">
8664 {tab.content_type}
8665 </Badge>
8666 </div>
8667 <div className="mb-2">
8668 {tab.enabled ? (
8669 <IconPicker
8670 value={tab.icon || null}
8671 onChange={(value) =>
8672 handleTabIconChange(tab.id, value)
8673 }
8674 label={__("Tab Icon", "yatra")}
8675 helpText={__(
8676 "Select an icon or upload an image for this tab",
8677 "yatra",
8678 )}
8679 size="sm"
8680 />
8681 ) : (
8682 <div className="opacity-50">
8683 <IconPicker
8684 value={tab.icon || null}
8685 onChange={() => {}} // No-op when disabled
8686 label={__("Tab Icon", "yatra")}
8687 helpText={__(
8688 "Enable this tab to select an icon",
8689 "yatra",
8690 )}
8691 size="sm"
8692 />
8693 </div>
8694 )}
8695 </div>
8696 {tab.content_type === "custom" && (
8697 <div className="mt-2">
8698 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
8699 {__("Custom Content", "yatra")}
8700 </label>
8701 <textarea
8702 value={tab.custom_content || ""}
8703 onChange={(e) =>
8704 handleTabCustomContentChange(
8705 tab.id,
8706 e.target.value,
8707 )
8708 }
8709 rows={3}
8710 disabled={!tab.enabled}
8711 placeholder={__(
8712 "Enter custom content for this tab...",
8713 "yatra",
8714 )}
8715 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"
8716 />
8717 </div>
8718 )}
8719 {tab.content_type !== "custom" && (
8720 <div className="mt-2">
8721 <Select
8722 value={tab.content_type}
8723 onChange={(e) =>
8724 handleTabContentTypeChange(
8725 tab.id,
8726 e.target
8727 .value as FrontendTab["content_type"],
8728 )
8729 }
8730 disabled={!tab.enabled}
8731 className="text-xs"
8732 >
8733 <option value="overview">
8734 {__("Overview", "yatra")}
8735 </option>
8736 <option value="itinerary">
8737 {__("Itinerary", "yatra")}
8738 </option>
8739 <option value="included_excluded">
8740 {__("Included", "yatra")}
8741 </option>
8742 <option value="location">
8743 {__("Location", "yatra")}
8744 </option>
8745 <option value="important_info">
8746 {__("Important Info", "yatra")}
8747 </option>
8748 <option value="downloads">
8749 {__("Downloads", "yatra")}
8750 </option>
8751 <option value="faq">
8752 {__("FAQ", "yatra")}
8753 </option>
8754 <option value="trip_story">
8755 {__("Story", "yatra")}
8756 </option>
8757 <option value="what_makes_special">
8758 {__("Special", "yatra")}
8759 </option>
8760 <option value="testimonials">
8761 {__("Testimonials", "yatra")}
8762 </option>
8763 <option value="custom">
8764 {__("Custom Content", "yatra")}
8765 </option>
8766 </Select>
8767 </div>
8768 )}
8769 </div>
8770 <div className="flex items-center gap-2">
8771 <Button
8772 type="button"
8773 variant="ghost"
8774 size="sm"
8775 onClick={() => handleTabMove(tab.id, "up")}
8776 disabled={index === 0}
8777 className="h-10 w-10 p-0"
8778 title={__("Move up", "yatra")}
8779 >
8780 <ChevronUp className="w-5 h-5" />
8781 </Button>
8782 <Button
8783 type="button"
8784 variant="ghost"
8785 size="sm"
8786 onClick={() => handleTabMove(tab.id, "down")}
8787 disabled={
8788 index === formData.frontend_tabs.length - 1
8789 }
8790 className="h-10 w-10 p-0"
8791 title={__("Move down", "yatra")}
8792 >
8793 <ChevronDown className="w-5 h-5" />
8794 </Button>
8795 <label className="flex items-center gap-2 cursor-pointer">
8796 <input
8797 type="checkbox"
8798 checked={tab.enabled}
8799 onChange={() => handleTabToggle(tab.id)}
8800 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
8801 />
8802 <span className="text-sm text-gray-700 dark:text-gray-300">
8803 {tab.enabled
8804 ? __("Enabled", "yatra")
8805 : __("Disabled", "yatra")}
8806 </span>
8807 </label>
8808 </div>
8809 </div>
8810 </div>
8811 ))}
8812 </div>
8813 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
8814 <Button
8815 type="button"
8816 variant="outline"
8817 onClick={handleTabAdd}
8818 className="w-full"
8819 >
8820 <Plus className="w-4 h-4 mr-2" />
8821 {__("Add Custom Tab", "yatra")}
8822 </Button>
8823 </div>
8824 </CardContent>
8825 </Card>
8826 </div>
8827 );
8828
8829 default:
8830 return (
8831 <div className="text-center py-8 text-gray-500">
8832 {__("Section content coming soon...", "yatra")}
8833 </div>
8834 );
8835 }
8836 };
8837
8838 return (
8839 <div className="flex-1 min-h-0 flex flex-col bg-white dark:bg-gray-900 overflow-hidden">
8840 {/* Header */}
8841 <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">
8842 {/* Left Side - Title, Status, Saved Time */}
8843 <div className="flex items-center gap-4 min-w-0 flex-1">
8844 <div className="min-w-0">
8845 <h1 className="text-base font-semibold text-gray-900 dark:text-white truncate">
8846 {formData.title || __("New Trip", "yatra")}
8847 </h1>
8848 <div className="flex items-center gap-2 mt-1">
8849 <Badge
8850 variant="outline"
8851 className={`bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 ${
8852 formData.status === "publish"
8853 ? "text-green-700 dark:text-green-400 border-green-300 dark:border-green-800"
8854 : formData.status === "review"
8855 ? "text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-800"
8856 : formData.status === "approved"
8857 ? "text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-800"
8858 : formData.status === "suspended"
8859 ? "text-orange-700 dark:text-orange-400 border-orange-300 dark:border-orange-800"
8860 : formData.status === "archived"
8861 ? "text-gray-700 dark:text-gray-400 border-gray-300 dark:border-gray-600"
8862 : "text-gray-700 dark:text-gray-300"
8863 }`}
8864 >
8865 {formData.status === "draft"
8866 ? __("Draft", "yatra")
8867 : formData.status === "review"
8868 ? __("Review", "yatra")
8869 : formData.status === "approved"
8870 ? __("Approved", "yatra")
8871 : formData.status === "publish"
8872 ? __("Published", "yatra")
8873 : formData.status === "suspended"
8874 ? __("Suspended", "yatra")
8875 : __("Archived", "yatra")}
8876 </Badge>
8877 </div>
8878 </div>
8879
8880 {/* Quick Start Mode Toggle */}
8881 {!isEditMode && (
8882 <div className="flex items-center gap-2 px-2 py-1 rounded-md bg-gray-50 dark:bg-gray-700/50 flex-shrink-0">
8883 <button
8884 onClick={() => setSimpleMode(!simpleMode)}
8885 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"
8886 title={
8887 simpleMode
8888 ? __("Switch to Advanced Mode", "yatra")
8889 : __("Switch to Simple Mode", "yatra")
8890 }
8891 >
8892 <Lightbulb
8893 className={`w-3.5 h-3.5 ${simpleMode ? "text-yellow-500" : ""}`}
8894 />
8895 <span>
8896 {simpleMode ? __("Simple", "yatra") : __("Advanced", "yatra")}
8897 </span>
8898 </button>
8899 </div>
8900 )}
8901
8902 {/* Guided Tour Button (First Time Users) */}
8903 {!isEditMode && (
8904 <Button
8905 variant="ghost"
8906 size="sm"
8907 onClick={() => {
8908 showToast(
8909 __("Guided tour feature coming soon!", "yatra"),
8910 "info",
8911 );
8912 }}
8913 className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0"
8914 >
8915 <BookOpen className="w-3.5 h-3.5 mr-1" />
8916 {__("Take a Tour", "yatra")}
8917 </Button>
8918 )}
8919
8920 {/* Dummy Data Button */}
8921 {!isEditMode && (
8922 <Button
8923 variant="ghost"
8924 size="sm"
8925 onClick={handleFillDummyData}
8926 className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 flex-shrink-0"
8927 title={__("Fill form with dummy data", "yatra")}
8928 >
8929 <Database className="w-3.5 h-3.5 mr-1" />
8930 {__("Fill Dummy Data", "yatra")} ({dummyDataIndex + 1}/
8931 {dummyTripsData.length})
8932 </Button>
8933 )}
8934 </div>
8935
8936 {/* Right Side - Action Buttons */}
8937 <div className="flex items-center gap-2 flex-shrink-0">
8938 {isEditMode && (
8939 <Button
8940 variant="outline"
8941 onClick={() => setShowRevisionsDialog(true)}
8942 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8943 >
8944 <History className="w-4 h-4 mr-2" />
8945 {__("Revisions", "yatra")}
8946 </Button>
8947 )}
8948 {isAiEligible() && isAiModuleEnabled() && (
8949 <Button
8950 type="button"
8951 onClick={() => {
8952 setAiModalMode("all");
8953 setAiModalOpen(true);
8954 }}
8955 className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white border-0 hover:from-purple-700 hover:to-indigo-700"
8956 title={__(
8957 "Generate description, itinerary, included items, SEO meta, and more — from this trip's facts.",
8958 "yatra",
8959 )}
8960 >
8961 <Sparkles className="w-4 h-4 mr-2" />
8962 {__("Auto-fill with AI", "yatra")}
8963 </Button>
8964 )}
8965 <Button
8966 variant="outline"
8967 onClick={handlePreview}
8968 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8969 disabled={!formData.slug}
8970 >
8971 <Eye className="w-4 h-4 mr-2" />
8972 {__("Preview", "yatra")}
8973 </Button>
8974 <Button
8975 variant="outline"
8976 onClick={handleSaveDraft}
8977 disabled={isSubmitting}
8978 className="text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
8979 >
8980 <Save className="w-4 h-4 mr-2" />
8981 {isEditMode
8982 ? __("Update Draft", "yatra")
8983 : __("Save Draft", "yatra")}
8984 </Button>
8985 <div className="relative group">
8986 <Button
8987 onClick={handlePublish}
8988 disabled={isSubmitting}
8989 className="bg-blue-600 hover:bg-blue-700 text-white border-0 relative"
8990 >
8991 {isSubmitting ? (
8992 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
8993 ) : (
8994 <>
8995 {/* Send icon (paper-plane) — used to be Sparkles,
8996 which made it look like an AI feature; replaced
8997 so the publish button reads as a regular submit
8998 action, distinct from the actual AI affordances
8999 sprinkled across the form. */}
9000 <Send className="w-4 h-4 mr-2" />
9001 {isEditMode
9002 ? __("Update Trip", "yatra")
9003 : __("Publish Trip", "yatra")}
9004 <ChevronDown className="w-4 h-4 ml-2" />
9005 </>
9006 )}
9007 </Button>
9008 {!isSubmitting && (
9009 <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">
9010 <div className="py-1">
9011 <button
9012 type="button"
9013 onClick={() => {
9014 handleFieldChange("status", "draft");
9015 handleSaveDraft();
9016 }}
9017 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"
9018 >
9019 <span>{__("Save as Draft", "yatra")}</span>
9020 </button>
9021 <button
9022 type="button"
9023 onClick={() => {
9024 handleFieldChange("status", "review");
9025 handlePublish();
9026 }}
9027 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"
9028 >
9029 <span>{__("Save for Review", "yatra")}</span>
9030 </button>
9031 <button
9032 type="button"
9033 onClick={() => {
9034 handleFieldChange("status", "approved");
9035 handlePublish();
9036 }}
9037 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"
9038 >
9039 <span>{__("Mark as Approved", "yatra")}</span>
9040 </button>
9041 <button
9042 type="button"
9043 onClick={handlePublish}
9044 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"
9045 >
9046 <Sparkles className="w-4 h-4" />
9047 <span>
9048 {isEditMode
9049 ? __("Update & Publish", "yatra")
9050 : __("Publish", "yatra")}
9051 </span>
9052 </button>
9053 <button
9054 type="button"
9055 onClick={() => {
9056 handleFieldChange("status", "suspended");
9057 handlePublish();
9058 }}
9059 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"
9060 >
9061 <span>{__("Suspend", "yatra")}</span>
9062 </button>
9063 <button
9064 type="button"
9065 onClick={() => {
9066 handleFieldChange("status", "archived");
9067 handlePublish();
9068 }}
9069 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"
9070 >
9071 <span>{__("Archive", "yatra")}</span>
9072 </button>
9073 </div>
9074 </div>
9075 )}
9076 </div>
9077 </div>
9078 </div>
9079
9080 {/* Navigation Bar */}
9081 <div className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
9082 <div className="flex items-center justify-between gap-4">
9083 {/* Previous Button */}
9084 <Button
9085 variant="outline"
9086 size="default"
9087 onClick={goToPreviousSection}
9088 disabled={currentStepIndex === 0}
9089 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"
9090 >
9091 <ChevronLeft className="w-4 h-4" />
9092 {__("Previous", "yatra")}
9093 </Button>
9094
9095 {/* Next Button */}
9096 <Button
9097 variant="outline"
9098 size="default"
9099 onClick={goToNextSection}
9100 disabled={currentStepIndex >= allSections.length - 1}
9101 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"
9102 >
9103 {__("Next", "yatra")}
9104 <ChevronRight className="w-4 h-4" />
9105 </Button>
9106 </div>
9107 </div>
9108
9109 <div className="flex flex-1 min-h-0 p-0">
9110 {/* Sidebar */}
9111 <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">
9112 <div className="p-5 pb-8 space-y-6">
9113 {/* PHASE 1: ESSENTIALS */}
9114 <div>
9115 <div className="flex items-center gap-2 mb-3 px-1">
9116 <div className="flex items-center gap-2 flex-1">
9117 <Box className="w-3.5 h-3.5 text-red-500 dark:text-red-400 flex-shrink-0" />
9118 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
9119 {__("PHASE 1: ESSENTIALS", "yatra")}
9120 </h3>
9121 </div>
9122 <Badge
9123 variant="outline"
9124 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"
9125 >
9126 {__("Must Complete", "yatra")}
9127 </Badge>
9128 </div>
9129 <div className="space-y-0.5">
9130 {essentialsSections.map((section, index) => {
9131 const Icon = section.icon;
9132 const isActive = currentSection === section.id;
9133 const isNext =
9134 !section.completed &&
9135 essentialsSections
9136 .slice(0, index)
9137 .every((s) => s.completed);
9138 return (
9139 <button
9140 key={section.id}
9141 type="button"
9142 onClick={() => setCurrentSection(section.id)}
9143 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 ${
9144 isActive
9145 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
9146 : isNext
9147 ? "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"
9148 : "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"
9149 }`}
9150 >
9151 <div className="flex items-center gap-2 flex-shrink-0">
9152 <span
9153 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
9154 isActive
9155 ? "bg-blue-600 text-white dark:bg-blue-500"
9156 : isNext
9157 ? "bg-amber-500 text-white"
9158 : section.completed
9159 ? "bg-green-500 text-white"
9160 : section.hasErrors
9161 ? "bg-red-500 text-white"
9162 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
9163 }`}
9164 >
9165 {index + 1}
9166 </span>
9167 <Icon
9168 className={`w-4 h-4 transition-colors ${
9169 isActive
9170 ? "text-blue-600 dark:text-blue-400"
9171 : isNext
9172 ? "text-amber-600 dark:text-amber-400"
9173 : section.hasErrors
9174 ? "text-red-600 dark:text-red-400"
9175 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
9176 }`}
9177 />
9178 </div>
9179 <span className="flex-1 min-w-0 break-words leading-snug">
9180 {section.label}
9181 {section.required && (
9182 <span className="ml-1 text-[9px] text-red-500">
9183 *
9184 </span>
9185 )}
9186 </span>
9187 {section.hasErrors && (
9188 <span
9189 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
9190 title={__("This section has errors", "yatra")}
9191 />
9192 )}
9193 </button>
9194 );
9195 })}
9196 </div>
9197 </div>
9198
9199 {/* PHASE 2: DETAILS */}
9200 <div>
9201 <div className="flex items-center gap-2 mb-3 px-1">
9202 <div className="flex items-center gap-2 flex-1">
9203 <BookOpen className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400 flex-shrink-0" />
9204 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
9205 {__("PHASE 2: DETAILS", "yatra")}
9206 </h3>
9207 </div>
9208 <Badge
9209 variant="outline"
9210 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"
9211 >
9212 {__("Recommended", "yatra")}
9213 </Badge>
9214 </div>
9215 <div className="space-y-0.5">
9216 {detailsSections.map((section, index) => {
9217 const Icon = section.icon;
9218 const isActive = currentSection === section.id;
9219 const sectionNumber = essentialsSections.length + index + 1;
9220 return (
9221 <button
9222 key={section.id}
9223 type="button"
9224 onClick={() => setCurrentSection(section.id)}
9225 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 ${
9226 isActive
9227 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
9228 : section.hasErrors
9229 ? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10"
9230 : "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"
9231 }`}
9232 >
9233 <div className="flex items-center gap-2 flex-shrink-0">
9234 <span
9235 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
9236 isActive
9237 ? "bg-blue-600 text-white dark:bg-blue-500"
9238 : section.completed
9239 ? "bg-green-500 text-white"
9240 : section.hasErrors
9241 ? "bg-red-500 text-white"
9242 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
9243 }`}
9244 >
9245 {sectionNumber}
9246 </span>
9247 <Icon
9248 className={`w-4 h-4 transition-colors ${
9249 isActive
9250 ? "text-blue-600 dark:text-blue-400"
9251 : section.hasErrors
9252 ? "text-red-600 dark:text-red-400"
9253 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
9254 }`}
9255 />
9256 </div>
9257 <span className="flex-1 min-w-0 break-words leading-snug">
9258 {section.label}
9259 </span>
9260 {section.hasErrors && (
9261 <span
9262 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
9263 title={__("This section has errors", "yatra")}
9264 />
9265 )}
9266 </button>
9267 );
9268 })}
9269 </div>
9270 </div>
9271
9272 {/* PHASE 3: OPTIMIZATION */}
9273 <div>
9274 <div className="flex items-center gap-2 mb-3 px-1">
9275 <div className="flex items-center gap-2 flex-1">
9276 <BarChart3 className="w-3.5 h-3.5 text-purple-500 dark:text-purple-400 flex-shrink-0" />
9277 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
9278 {__("PHASE 3: OPTIMIZATION", "yatra")}
9279 </h3>
9280 </div>
9281 <Badge
9282 variant="outline"
9283 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"
9284 >
9285 {__("Optional", "yatra")}
9286 </Badge>
9287 </div>
9288 <div className="space-y-0.5">
9289 {optimizationSections.map((section, index) => {
9290 const Icon = section.icon;
9291 const isActive = currentSection === section.id;
9292 const sectionNumber =
9293 essentialsSections.length +
9294 detailsSections.length +
9295 index +
9296 1;
9297 return (
9298 <button
9299 key={section.id}
9300 type="button"
9301 onClick={() => setCurrentSection(section.id)}
9302 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 ${
9303 isActive
9304 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
9305 : section.hasErrors
9306 ? "border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10"
9307 : "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"
9308 }`}
9309 >
9310 <div className="flex items-center gap-2 flex-shrink-0">
9311 <span
9312 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
9313 isActive
9314 ? "bg-blue-600 text-white dark:bg-blue-500"
9315 : section.completed
9316 ? "bg-green-500 text-white"
9317 : section.hasErrors
9318 ? "bg-red-500 text-white"
9319 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
9320 }`}
9321 >
9322 {sectionNumber}
9323 </span>
9324 <Icon
9325 className={`w-4 h-4 transition-colors ${
9326 isActive
9327 ? "text-blue-600 dark:text-blue-400"
9328 : section.hasErrors
9329 ? "text-red-600 dark:text-red-400"
9330 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
9331 }`}
9332 />
9333 </div>
9334 <span className="flex-1 min-w-0 break-words leading-snug">
9335 {section.label}
9336 </span>
9337 {section.hasErrors && (
9338 <span
9339 className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full"
9340 title={__("This section has errors", "yatra")}
9341 />
9342 )}
9343 </button>
9344 );
9345 })}
9346 </div>
9347 </div>
9348
9349 {/* PHASE 4: ADVANCED */}
9350 <div className="pb-12">
9351 <div className="flex items-center gap-2 mb-3 px-1">
9352 <div className="flex items-center gap-2 flex-1">
9353 <Settings className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
9354 <h3 className="text-[10px] font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-widest">
9355 {__("PHASE 4: ADVANCED", "yatra")}
9356 </h3>
9357 </div>
9358 <Badge
9359 variant="outline"
9360 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"
9361 >
9362 {__("Power Users", "yatra")}
9363 </Badge>
9364 </div>
9365 <div className="space-y-0.5">
9366 {advancedSections.map((section, index) => {
9367 const Icon = section.icon;
9368 const isActive = currentSection === section.id;
9369 const sectionNumber =
9370 essentialsSections.length +
9371 detailsSections.length +
9372 optimizationSections.length +
9373 index +
9374 1;
9375 return (
9376 <button
9377 key={section.id}
9378 type="button"
9379 onClick={() => setCurrentSection(section.id)}
9380 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 ${
9381 isActive
9382 ? "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 shadow-sm border-blue-200 dark:border-blue-800"
9383 : "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"
9384 }`}
9385 >
9386 <div className="flex items-center gap-2 flex-shrink-0">
9387 <span
9388 className={`text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full ${
9389 isActive
9390 ? "bg-blue-600 text-white dark:bg-blue-500"
9391 : "bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
9392 }`}
9393 >
9394 {sectionNumber}
9395 </span>
9396 <Icon
9397 className={`w-4 h-4 transition-colors ${
9398 isActive
9399 ? "text-blue-600 dark:text-blue-400"
9400 : "text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400"
9401 }`}
9402 />
9403 </div>
9404 <span className="flex-1 min-w-0 break-words leading-snug">
9405 {section.label}
9406 </span>
9407 </button>
9408 );
9409 })}
9410 </div>
9411 </div>
9412 </div>
9413 </div>
9414
9415 {/* Main Content */}
9416 <div className="flex-1 overflow-y-auto bg-white dark:bg-gray-900">
9417 <div className="p-6 pb-24 max-w-4xl">
9418 {errors.submit && (
9419 <Alert variant="error" className="mb-4">
9420 {errors.submit}
9421 </Alert>
9422 )}
9423 {renderSectionContent()}
9424 </div>
9425 </div>
9426 </div>
9427
9428 {/* Modals */}
9429 {/* Highlight Modal */}
9430 {showHighlightModal && (
9431 <div
9432 className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
9433 onClick={() => setShowHighlightModal(false)}
9434 >
9435 <div
9436 className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"
9437 onClick={(e) => e.stopPropagation()}
9438 >
9439 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
9440 {__("Add Highlight", "yatra")}
9441 </h3>
9442 <div className="space-y-4">
9443 <div>
9444 <label className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5">
9445 {__("Highlight Text", "yatra")}
9446 </label>
9447 <Input
9448 type="text"
9449 value={modalInput.text}
9450 onChange={(e) =>
9451 setModalInput({ ...modalInput, text: e.target.value })
9452 }
9453 placeholder={__("e.g., Private guide included", "yatra")}
9454 autoFocus
9455 onKeyDown={(e) => {
9456 if (e.key === "Enter") {
9457 e.preventDefault();
9458 handleHighlightSave();
9459 }
9460 }}
9461 />
9462 <HelpText
9463 text={__(
9464 'Keep it short and impactful. Examples: "All meals included", "Skip-the-line tickets", "Free airport transfer"',
9465 "yatra",
9466 )}
9467 className="mt-2"
9468 />
9469 </div>
9470 <div className="flex items-center justify-end gap-2">
9471 <Button
9472 variant="outline"
9473 onClick={() => setShowHighlightModal(false)}
9474 >
9475 {__("Cancel", "yatra")}
9476 </Button>
9477 <Button
9478 onClick={handleHighlightSave}
9479 disabled={!modalInput.text.trim()}
9480 >
9481 {__("Add", "yatra")}
9482 </Button>
9483 </div>
9484 </div>
9485 </div>
9486 </div>
9487 )}
9488
9489 {/* Revisions Dialog */}
9490 {showRevisionsDialog && (
9491 <div
9492 className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/50 backdrop-blur-sm"
9493 onClick={() => setShowRevisionsDialog(false)}
9494 >
9495 <Card
9496 className="w-full max-w-2xl max-h-[80vh] mx-4 shadow-xl overflow-hidden flex flex-col"
9497 onClick={(e) => e.stopPropagation()}
9498 >
9499 <CardHeader className="pb-3 flex-shrink-0">
9500 <div className="flex items-center justify-between">
9501 <CardTitle className="text-lg flex items-center gap-2">
9502 <History className="w-5 h-5" />
9503 {__("Trip Revisions", "yatra")}
9504 </CardTitle>
9505 <button
9506 onClick={() => setShowRevisionsDialog(false)}
9507 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
9508 aria-label={__("Close", "yatra")}
9509 >
9510 <X className="w-5 h-5" />
9511 </button>
9512 </div>
9513 </CardHeader>
9514 <CardContent className="flex-1 overflow-y-auto pb-4">
9515 {isLoadingRevisions ? (
9516 <div className="flex items-center justify-center py-8">
9517 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
9518 <span className="ml-2 text-gray-600 dark:text-gray-400">
9519 {__("Loading revisions...", "yatra")}
9520 </span>
9521 </div>
9522 ) : revisions.length === 0 ? (
9523 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
9524 {__("No revisions found", "yatra")}
9525 </div>
9526 ) : (
9527 <div className="space-y-2">
9528 {revisions.map((revision) => (
9529 <div
9530 key={revision.id}
9531 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"
9532 onClick={() => handleRevisionClick(revision.id)}
9533 >
9534 <div className="flex items-center justify-between">
9535 <div className="flex-1">
9536 <div className="flex items-center gap-2 mb-1">
9537 <Badge variant="outline">v{revision.version}</Badge>
9538 <span className="text-sm font-medium text-gray-900 dark:text-white">
9539 {__("Revision", "yatra")} {revision.version}
9540 </span>
9541 </div>
9542 <div className="text-xs text-gray-500 dark:text-gray-400">
9543 {__("Created by", "yatra")}{" "}
9544 {revision.created_by_name} •{" "}
9545 {new Date(revision.created_at).toLocaleString()}
9546 </div>
9547 </div>
9548 <Button
9549 variant="outline"
9550 size="sm"
9551 onClick={(e) => {
9552 e.stopPropagation();
9553 handleRevisionClick(revision.id);
9554 }}
9555 >
9556 {__("Use This Revision", "yatra")}
9557 </Button>
9558 </div>
9559 </div>
9560 ))}
9561 </div>
9562 )}
9563 </CardContent>
9564 </Card>
9565 </div>
9566 )}
9567
9568 {/* Revision Confirmation Dialog */}
9569 <ConfirmationDialog
9570 isOpen={showRevisionConfirm}
9571 onClose={() => {
9572 setShowRevisionConfirm(false);
9573 setSelectedRevisionId(null);
9574 }}
9575 onConfirm={handleRevisionConfirm}
9576 title={__("Use This Revision?", "yatra")}
9577 message={__(
9578 "Do you want to use this revision? This will replace all current form data with the revision data.",
9579 "yatra",
9580 )}
9581 confirmText={__("Yes, Use This Revision", "yatra")}
9582 cancelText={__("Cancel", "yatra")}
9583 variant="info"
9584 isLoading={false}
9585 />
9586
9587 {/* Landmark Input Modal */}
9588 <Modal
9589 isOpen={showLandmarkDialog}
9590 onClose={() => {
9591 setShowLandmarkDialog(false);
9592 setLandmarkInput("");
9593 }}
9594 title={__("Add Landmark", "yatra")}
9595 description={__(
9596 "Enter the name of the landmark or point of interest",
9597 "yatra",
9598 )}
9599 size="sm"
9600 hideFooter={false}
9601 footer={
9602 <div className="flex gap-2 justify-end">
9603 <Button
9604 variant="outline"
9605 onClick={() => {
9606 setShowLandmarkDialog(false);
9607 setLandmarkInput("");
9608 }}
9609 >
9610 {__("Cancel", "yatra")}
9611 </Button>
9612 <Button
9613 onClick={() => {
9614 if (landmarkInput.trim()) {
9615 handleFieldChange("landmarks", [
9616 ...formData.landmarks,
9617 landmarkInput.trim(),
9618 ]);
9619 setLandmarkInput("");
9620 setShowLandmarkDialog(false);
9621 }
9622 }}
9623 disabled={!landmarkInput.trim()}
9624 >
9625 <Plus className="w-4 h-4 mr-2" />
9626 {__("Add Landmark", "yatra")}
9627 </Button>
9628 </div>
9629 }
9630 >
9631 <div className="space-y-4">
9632 <div>
9633 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
9634 {__("Landmark Name", "yatra")}
9635 </label>
9636 <Input
9637 type="text"
9638 value={landmarkInput}
9639 onChange={(e) => setLandmarkInput(e.target.value)}
9640 placeholder={__(
9641 "e.g., Eiffel Tower, Central Park, Grand Canyon",
9642 "yatra",
9643 )}
9644 className="w-full"
9645 autoFocus
9646 onKeyDown={(e) => {
9647 if (e.key === "Enter" && landmarkInput.trim()) {
9648 handleFieldChange("landmarks", [
9649 ...formData.landmarks,
9650 landmarkInput.trim(),
9651 ]);
9652 setLandmarkInput("");
9653 setShowLandmarkDialog(false);
9654 }
9655 }}
9656 />
9657 </div>
9658 <div className="text-xs text-gray-500 dark:text-gray-400">
9659 {__(
9660 "Examples: Famous monuments, natural landmarks, historical sites, viewpoints, or points of interest that travelers will visit during this trip.",
9661 "yatra",
9662 )}
9663 </div>
9664 </div>
9665 </Modal>
9666 {/* AI Assistant — auto-fill / itinerary modal. Mounted at the root
9667 so it overlays the whole form regardless of which section is
9668 currently active. */}
9669 <AutoFillTripModal
9670 open={aiModalOpen}
9671 onClose={() => setAiModalOpen(false)}
9672 buildContext={() => buildTripAiContext(formData)}
9673 itineraryOnly={aiModalMode === "itinerary"}
9674 onFieldsAccepted={(updates) => {
9675 for (const [key, value] of Object.entries(updates)) {
9676 handleFieldChange(key as any, value as any);
9677 }
9678 }}
9679 />
9680 {/* Same modal as the standalone Itinerary page. We mount it
9681 here so the Itinerary tab's "Build with AI" button can drive
9682 the SAME agent + applyItinerary flow that the other two
9683 surfaces use, instead of forking through AutoFillTripModal's
9684 single-shot legacy parser. */}
9685 {isEditMode && tripId && (
9686 <BuildItineraryModal
9687 open={itineraryBuildOpen}
9688 onClose={() => setItineraryBuildOpen(false)}
9689 tripId={tripId}
9690 tripName={(formData as any)?.title || __("Trip", "yatra")}
9691 tripDurationDays={Number((formData as any)?.duration_days) || 0}
9692 onApplied={() => {
9693 // Persistence happened server-side via applyItinerary; the
9694 // trip form's local itinerary state needs to refresh so the
9695 // newly-created day rows show up. The TripForm reloads
9696 // itinerary data on a routing event — we just close the
9697 // modal and let the user navigate / save normally.
9698 setItineraryBuildOpen(false);
9699 }}
9700 />
9701 )}
9702 </div>
9703 );
9704 };
9705
9706 export default TripForm;
9707