/** * Additional Services Form * * Form component for creating and editing additional services. * This is part of the premium module - functionality provided by Yatra Pro. * * @package Yatra * @since 3.0.0 */ import React, { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle, } from "../components/ui/card"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/select"; import { PageHeader } from "../components/common/PageHeader"; import { useToast } from "../components/ui/toast"; import { usePermissions } from "../hooks/usePermissions"; import { IconPicker, IconPickerValue } from "../components/ui/icon-picker"; import { apiClient } from "../lib/api-client"; import { __ } from "../lib/i18n"; import { ArrowLeft, Save, Package, Loader2, X } from "lucide-react"; // Types interface AdditionalService { id?: number; name: string; description: string; price: number; price_type: "fixed" | "percentage"; price_per: "person" | "booking" | "day"; icon: IconPickerValue | null; status: "publish" | "draft" | "trash"; sort_order: number; applicable_to: "all" | "specific_trips"; trip_ids: number[]; is_required: boolean; } const defaultService: AdditionalService = { name: "", description: "", price: 0, price_type: "fixed", price_per: "person", icon: null, status: "publish", sort_order: 0, applicable_to: "all", trip_ids: [], is_required: false, }; // Check if module is available const isModuleAvailable = (): boolean => { const yatraAdmin = (window as any)?.yatraAdmin; return Boolean(yatraAdmin?.isPro && yatraAdmin?.additionalServicesEnabled); }; const AdditionalServicesForm: React.FC = () => { const [formData, setFormData] = useState(defaultService); const [errors, setErrors] = useState>({}); const [showTripDropdown, setShowTripDropdown] = useState(false); const [tripSearchQuery, setTripSearchQuery] = useState(""); const [debouncedTripSearch, setDebouncedTripSearch] = useState(""); const { showToast } = useToast(); const queryClient = useQueryClient(); const { can } = usePermissions(); // Get ID from URL for edit mode const params = new URLSearchParams(window.location.search); const serviceId = params.get("id"); const isEditMode = !!serviceId; // Debounce trip search useEffect(() => { const timer = setTimeout(() => { setDebouncedTripSearch(tripSearchQuery); }, 300); return () => clearTimeout(timer); }, [tripSearchQuery]); // Fetch trips for applicable_to selection const tripsQuery = useQuery({ queryKey: ["trips-for-service", debouncedTripSearch], queryFn: async () => { try { const params: Record = { per_page: 100, }; if (debouncedTripSearch.trim()) { params.search = debouncedTripSearch.trim(); } const response = await apiClient.get("/trips", { params }); const trips = response?.data?.data || response?.data || response || []; return Array.isArray(trips) ? trips : []; } catch (error: any) { console.error("Failed to load trips:", error); return []; } }, enabled: formData.applicable_to === "specific_trips", staleTime: 5 * 60 * 1000, }); // Trip options for dropdown const tripOptions = (tripsQuery.data || []) .filter( (t: any) => !!t && (typeof t.id === "number" || typeof t.id === "string"), ) .map((trip: any) => ({ value: Number(trip.id), label: trip.title || trip.name || `Trip #${trip.id}`, })); // Fetch service data for edit mode const { data: serviceData, isLoading: isLoadingService } = useQuery({ queryKey: ["additional-service", serviceId], queryFn: async () => { const response = await apiClient.get(`/additional-services/${serviceId}`); // API returns { success: true, data: service }, extract the service object return response.data?.data || response.data; }, enabled: isEditMode && isModuleAvailable(), }); // Populate form with existing data useEffect(() => { if (serviceData) { // Handle icon - could be old format (image_id/image_url) or new format (IconPickerValue) let iconValue: IconPickerValue | null = null; if (serviceData.icon) { iconValue = serviceData.icon as IconPickerValue; } else if (serviceData.image_url) { // Convert old format to new IconPickerValue format iconValue = { type: "image", value: serviceData.image_url }; } setFormData({ id: serviceData.id, name: serviceData.name || "", description: serviceData.description || "", price: parseFloat(serviceData.price) || 0, price_type: serviceData.price_type || "fixed", price_per: serviceData.price_per || "person", icon: iconValue, status: serviceData.status || "draft", sort_order: serviceData.sort_order || 0, applicable_to: serviceData.applicable_to || "all", trip_ids: serviceData.trip_ids || [], is_required: serviceData.is_required === true || serviceData.is_required === 1 || serviceData.is_required === "1", }); } }, [serviceData]); // Save mutation const saveMutation = useMutation({ mutationFn: async (data: AdditionalService) => { if (isEditMode) { return apiClient.put(`/additional-services/${serviceId}`, data); } return apiClient.post("/additional-services", data); }, onSuccess: () => { showToast( isEditMode ? __("Service updated successfully") : __("Service created successfully"), "success", ); queryClient.invalidateQueries({ queryKey: ["additional-services"] }); // Only navigate back on create, stay on page for updates if (!isEditMode) { navigateBack(); } }, onError: (error: any) => { const message = error?.response?.data?.message || __("Failed to save service"); showToast(message, "error"); }, }); const navigateBack = () => { const params = new URLSearchParams(window.location.search); params.delete("action"); params.delete("id"); params.set("subpage", "trips"); params.set("tab", "additional-services"); window.history.pushState({}, "", `${window.location.pathname}?${params}`); window.dispatchEvent(new PopStateEvent("popstate")); }; const handleFieldChange = (field: keyof AdditionalService, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); // Clear error when field is modified if (errors[field]) { setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[field]; return newErrors; }); } }; const validateForm = (): boolean => { const newErrors: Record = {}; if (!formData.name.trim()) { newErrors.name = __("Service name is required"); } if (formData.price < 0) { newErrors.price = __("Price cannot be negative"); } if (formData.price_type === "percentage" && formData.price > 100) { newErrors.price = __("Percentage cannot exceed 100%"); } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { showToast(__("Please fix the errors before saving"), "error"); return; } saveMutation.mutate(formData); }; // Check permissions - allow admins and users with yatra capabilities const hasPermission = can("manage_options") || can("yatra_manage_settings") || can("yatra_edit_trips"); if (!hasPermission) { return (

{__("You do not have permission to manage services.")}

); } // Show premium gate if module not available if (!isModuleAvailable()) { return (

{__("Premium Feature")}

{__( "Additional Services is a premium feature. Upgrade to Yatra Pro to unlock.", )}

); } if (isEditMode && isLoadingService) { return (
{/* Header Skeleton */}
{/* Form Skeleton */}
{/* Main Fields */}
{/* Service Details Card */}
{/* Name field */}
{/* Description field */}
{/* Pricing Card */}
{/* Sidebar */}
{/* Publish Card */}
{/* Icon/Image Card */}
); } return (
{__("Back to Services")} } />
{/* Main Content */}
{/* Basic Information */} {__("Service Details")}
handleFieldChange("name", e.target.value)} placeholder={__("e.g., Airport Transfer, Travel Insurance")} className={errors.name ? "border-red-500" : ""} /> {errors.name && (

{errors.name}

)}