/** * Category Form Page * Add/Edit Trip Category with parent/subcategory support */ import React, { useState, useEffect, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Save, Loader2, Edit2, X, Eye } from "lucide-react"; import { __ } from "../lib/i18n"; import { usePermissions } from "../hooks/usePermissions"; import { useToast } from "../components/ui/toast"; import { fetchSettings } from "../api/settings-api"; import { apiClient } from "../lib/api-client"; import { generateSlug } from "../lib/slug"; import { buildYatraSinglePreviewUrl } from "../lib/frontend-permalink-urls"; 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 { Card, CardContent, CardHeader, CardTitle, } from "../components/ui/card"; import { ConditionalRender } from "../components/ui/conditional-render"; import { IconPicker, IconPickerValue } from "../components/ui/icon-picker"; import { RichTextEditor } from "../components/ui/rich-text-editor"; import { ClassificationLandingPageField, fetchPublishedPagePermalink, parseLandingPageIdFromMetadata, } from "../components/classifications/ClassificationLandingPageField"; /** DB / REST may send 0|1 as number or string; never use Boolean() on string "0". */ function parseTripCategoryIsFeatured(raw: unknown): boolean { if (raw === true || raw === 1) { return true; } if (raw === false || raw === 0 || raw === null || raw === undefined) { return false; } if (typeof raw === "string") { const t = raw.trim(); return t === "1" || t.toLowerCase() === "true"; } return Number(raw) === 1; } interface CategoryFormData { name: string; slug: string; description: string; icon: IconPickerValue | null; parent_id: number | ""; status: string; /** Trip category card "Featured" badge on shortcode / Trip Category block */ is_featured: boolean; seo_title: string; seo_description: string; seo_keywords: string; landing_page_id: number | null; } interface CategoryOption { id: number; name: string; parent_id: number | null; } const CategoryForm: React.FC = () => { const queryClient = useQueryClient(); const { can } = usePermissions(); const { showToast } = useToast(); const [formData, setFormData] = useState({ name: "", slug: "", description: "", icon: null, parent_id: "", status: "publish", is_featured: false, seo_title: "", seo_description: "", seo_keywords: "", landing_page_id: null, }); const [errors, setErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isSlugEditable, setIsSlugEditable] = useState(false); const action = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("action") || "create"; }, []); const categoryId = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("id") ? parseInt(params.get("id") || "0", 10) : null; }, []); const isEditMode = action === "edit" && categoryId !== null; // Fetch category data if editing const { data: categoryData, isLoading: isLoadingCategory } = useQuery({ queryKey: ["trip-category", categoryId], queryFn: async () => { if (!categoryId) return null; try { const response = await apiClient.get(`/trip-categories/${categoryId}`); return response; } catch (error: any) { showToast( error?.message || __("Failed to load category", "yatra"), "error", ); throw error; } }, enabled: isEditMode && can("yatra_view_trips"), }); // Fetch parent category options (top-level categories) const { data: parentCategoriesData } = useQuery({ queryKey: ["trip-categories", "parent-options"], queryFn: async () => { try { const response = await apiClient.get("/trip-categories", { params: { per_page: 100, parent_id: null, hierarchical: false, orderby: "name", order: "ASC", }, }); const payload = response?.data || response; if (!payload) { return []; } if (Array.isArray(payload)) { return payload; } return payload.data || []; } catch (error) { return []; } }, enabled: can("yatra_view_trips"), }); // Fetch settings for permalink handling const { data: settings } = useQuery({ queryKey: ["settings"], queryFn: async () => { try { const response = await fetchSettings(); return response?.data || response; } catch (error) { return {}; } }, }); const parentOptions: CategoryOption[] = useMemo(() => { if (!parentCategoriesData || !Array.isArray(parentCategoriesData)) { return []; } return parentCategoriesData.filter((cat: any) => { if (!cat || typeof cat !== "object") return false; // Only allow top-level categories (no parent) if (cat.parent_id) return false; // Prevent selecting itself as parent when editing if (isEditMode && categoryId && cat.id === categoryId) return false; return true; }); }, [parentCategoriesData, isEditMode, categoryId]); // Load category data into form when editing useEffect(() => { if (categoryData && isEditMode) { setFormData({ name: categoryData.name || "", slug: categoryData.slug || "", description: categoryData.description || "", icon: (categoryData.icon as IconPickerValue) || null, parent_id: categoryData.parent_id ?? "", status: categoryData.status || "publish", is_featured: parseTripCategoryIsFeatured( (categoryData as { is_featured?: unknown }).is_featured, ), seo_title: categoryData.metadata?.seo_title || "", seo_description: categoryData.metadata?.seo_description || "", seo_keywords: categoryData.metadata?.seo_keywords || "", landing_page_id: parseLandingPageIdFromMetadata( categoryData.metadata as { landing_page_id?: unknown }, ), }); } }, [categoryData, isEditMode]); const handleNameChange = (value: string) => { // Only auto-generate slug on create; in edit mode, keep existing slug if (!isEditMode && !isSlugEditable) { const newSlug = generateSlug(value); setFormData((prev) => ({ ...prev, name: value, slug: newSlug, })); } else { setFormData((prev) => ({ ...prev, name: value })); } if (errors.name) { setErrors((prev) => ({ ...prev, name: "" })); } }; const handleSlugChange = (value: string) => { if (isSlugEditable) { setFormData((prev) => ({ ...prev, slug: value })); if (errors.slug) { setErrors((prev) => ({ ...prev, slug: "" })); } } }; const handleToggleSlugEdit = () => { if (isSlugEditable) { const newSlug = generateSlug(formData.name); setFormData((prev) => ({ ...prev, slug: newSlug })); } setIsSlugEditable(!isSlugEditable); }; const handleFieldChange = (field: keyof CategoryFormData, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); if (errors[field]) { setErrors((prev) => ({ ...prev, [field]: "" })); } }; const validateForm = (): boolean => { const newErrors: Record = {}; if (!formData.name.trim()) { newErrors.name = __("Name is required", "yatra"); } if (!formData.slug.trim()) { newErrors.slug = __("Slug is required", "yatra"); } else if (!/^[\p{L}\p{N}-]+$/u.test(formData.slug)) { newErrors.slug = __( "Slug can only contain letters, numbers, and hyphens", "yatra", ); } if ( formData.parent_id && categoryId && Number(formData.parent_id) === categoryId ) { newErrors.parent_id = __("Category cannot be its own parent", "yatra"); } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const saveMutation = useMutation({ mutationFn: async (data: CategoryFormData) => { const payload: any = { name: data.name.trim(), slug: data.slug.trim(), description: data.description.trim(), icon: data.icon, parent_id: data.parent_id === "" ? null : Number(data.parent_id), status: data.status, is_featured: data.is_featured ? 1 : 0, seo_title: data.seo_title.trim(), seo_description: data.seo_description.trim(), seo_keywords: data.seo_keywords.trim(), landing_page_id: data.landing_page_id ?? null, }; if (isEditMode && isSlugEditable) { payload.preserve_slug = true; } if (isEditMode && categoryId) { return await apiClient.put(`/trip-categories/${categoryId}`, payload); } return await apiClient.post("/trip-categories", payload); }, onSuccess: (response) => { queryClient.invalidateQueries({ queryKey: ["trip-categories"] }); queryClient.invalidateQueries({ queryKey: ["trip-category", categoryId], }); showToast( isEditMode ? __("Category updated successfully", "yatra") : __("Category created successfully", "yatra"), "success", ); setIsSubmitting(false); if (!isEditMode) { const newId = response?.id; if (newId) { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories&action=edit&id=${newId}`; } else { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories`; } } }, onError: (error: any) => { const errorMessage = error?.message || __("An error occurred while saving the category", "yatra"); showToast(errorMessage, "error"); setIsSubmitting(false); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { showToast(__("Please fix the form errors", "yatra"), "warning"); return; } setIsSubmitting(true); setErrors({}); saveMutation.mutate(formData); }; const handleCancel = () => { window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories`; }; if (isEditMode && isLoadingCategory) { return (
{/* Header Skeleton */}
{/* Form Skeleton */}
{/* Main Fields */}
{/* Name field */}
{/* Slug field */}
{/* Description field */}
{/* Icon field */}
{/* Sidebar */}
); } return (
{formData.slug && ( )}
} />
{__("Basic Information", "yatra")}
handleNameChange(e.target.value)} placeholder={__("Enter category name", "yatra")} className={errors.name ? "border-red-500" : ""} required /> {errors.name && (

{errors.name}

)}
handleSlugChange(e.target.value)} placeholder={__("category-slug", "yatra")} className={`pr-10 ${errors.slug ? "border-red-500" : ""} ${!isSlugEditable ? "bg-gray-50 dark:bg-gray-800 cursor-not-allowed" : ""}`} disabled={!isSlugEditable} required />
{errors.slug && (

{errors.slug}

)}

{isSlugEditable ? __( "Manually editing slug. Click X to cancel and regenerate from name.", "yatra", ) : __( "Auto-generated from name. Click edit icon to customize.", "yatra", )}

{/* Description */}
handleFieldChange("description", value) } placeholder={__("Enter category description...", "yatra")} helperText={__( "Describe what this category is for and what types of trips it contains.", "yatra", )} minHeight={120} maxHeight={360} />
setFormData((prev) => ({ ...prev, landing_page_id: id })) } />
{__("Hierarchy", "yatra")}
{errors.parent_id && (

{errors.parent_id}

)}

{__( "Assign a parent category to create subcategories.", "yatra", )}

{__("Status", "yatra")} {__("Visibility", "yatra")} {/* SEO Settings */} {__("SEO Settings", "yatra")}
handleFieldChange("seo_title", e.target.value) } placeholder={__( "e.g., {name} Trip Categories | Your Travel Agency", "yatra", )} className="w-full" />

{__( "Custom title for search engines. Use {name} as placeholder.", "yatra", )}