PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / TripForm.tsx

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

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