/** * Trip Attributes Section Component * Handles attribute selection and value assignment for trips */ import React, { useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { Plus, X, Tag, ChevronDown, ChevronUp } from "lucide-react"; import { __ } from "../../lib/i18n"; import { apiClient } from "../../lib/api-client"; import { Card, CardContent } from "../../components/ui/card"; import { Badge } from "../../components/ui/badge"; import { Input } from "../../components/ui/input"; import { Select } from "../../components/ui/select"; import { Button } from "../../components/ui/button"; import { TimePicker } from "../../components/ui/time-picker"; interface Attribute { id: number; name: string; slug: string; field_type: string; field_options: string; default_value: string; placeholder: string; required: boolean; description?: string; validation_rules: string; display_order: number; show_on_frontend: boolean; show_in_filters: boolean; filter_type: string; searchable: boolean; status: string; } interface TripAttributesSectionProps { formData: any; handleFieldChange: (field: "attributes", value: any) => void; tripId?: number; isEditMode?: boolean; tripAttributesData?: any; // Add this prop /** False while parent is loading GET /trips/:id/attributes in edit mode; avoids init effect locking empty before data arrives */ tripAttributesReady?: boolean; } function parseAttributeFieldOptions( field_options: string | unknown, ): Array<{ label: string; value: string }> { if (Array.isArray(field_options)) { return field_options .filter((o) => o && typeof o === "object") .map((o: { label?: string; value?: string }) => ({ label: String(o?.label ?? ""), value: String(o?.value ?? ""), })) .filter((o) => o.label !== "" || o.value !== ""); } if (typeof field_options === "string" && field_options.trim()) { try { const p = JSON.parse(field_options) as unknown; if (Array.isArray(p)) { return p .filter((o) => o && typeof o === "object") .map((o: { label?: string; value?: string }) => ({ label: String(o?.label ?? ""), value: String(o?.value ?? ""), })) .filter((o) => o.label !== "" || o.value !== ""); } } catch { return []; } } return []; } function normalizeAttributeStoredValue( fieldType: string, raw: unknown, ): string | string[] { if (fieldType === "checkbox") { if (Array.isArray(raw)) { return raw.map((v) => String(v)); } if (typeof raw === "string") { const t = raw.trim(); if (t.startsWith("[")) { try { const p = JSON.parse(t) as unknown; if (Array.isArray(p)) { return p.map((v) => String(v)); } } catch { /* fall through */ } } return t ? [t] : []; } if (typeof raw === "boolean") { return raw ? ["1"] : []; } return []; } if (raw === null || raw === undefined) { return ""; } return typeof raw === "string" ? raw : String(raw); } const TripAttributesSection: React.FC = ({ formData, handleFieldChange, tripId, isEditMode = false, tripAttributesData = {}, // Add this prop tripAttributesReady = true, }) => { const [selectedAttributes, setSelectedAttributes] = useState([]); const [attributeValues, setAttributeValues] = useState>( {}, ); const [showAttributeDropdown, setShowAttributeDropdown] = useState(false); const isInitializing = useRef(true); // Fetch available attributes const { data: attributesData, isLoading: isLoadingAttributes } = useQuery({ queryKey: ["attributes"], queryFn: async () => { const response = await apiClient.get("/attributes?status=publish"); const rawData = response?.data ?? []; const list = Array.isArray(rawData) ? rawData : Array.isArray(rawData?.data) ? rawData.data : []; // Normalize attribute shape (ids sometimes arrive as strings) return list.map((item: any) => ({ ...item, id: Number(item?.id) || 0, })); }, }); // Initialize from formData when component mounts or when trip attributes are loaded useEffect(() => { // Only run initialization logic, not when user is typing or deleting if (!isInitializing.current) return; // Edit mode: wait for GET /trips/:id/attributes — otherwise we mark initialized empty and never hydrate when data arrives if (isEditMode && tripId && !tripAttributesReady) { return; } // In edit mode, prioritize trip attributes from prop (from main form) if ( isEditMode && tripAttributesData && Object.keys(tripAttributesData).length > 0 ) { const attributeIds = Object.keys(tripAttributesData).map((id) => Number(id), ); setSelectedAttributes(attributeIds); setAttributeValues(tripAttributesData); // Set initialization to false only after successful initialization isInitializing.current = false; } // Fallback to formData attributes (only for initial load) else if ( formData.attributes && Object.keys(formData.attributes).length > 0 && isInitializing.current ) { const attributeIds = Object.keys(formData.attributes).map((id) => Number(id), ); setSelectedAttributes(attributeIds); setAttributeValues(formData.attributes); isInitializing.current = false; } else { isInitializing.current = false; } }, [ tripAttributesData, formData.attributes, isEditMode, tripId, tripAttributesReady, ]); // Add attribute to selected list const handleAddAttribute = (attributeId: number) => { if (!selectedAttributes.includes(attributeId)) { const newSelectedAttributes = [...selectedAttributes, attributeId]; setSelectedAttributes(newSelectedAttributes); // Initialize with default value if not exists if (!attributeValues[attributeId]) { const newAttributeValues = { ...attributeValues, [attributeId]: "" }; setAttributeValues(newAttributeValues); handleFieldChange("attributes", newAttributeValues); } setShowAttributeDropdown(false); } }; // Remove attribute from selected list const handleRemoveAttribute = (attributeId: number) => { const newSelectedAttributes = selectedAttributes.filter( (id) => id !== attributeId, ); const newAttributeValues = { ...attributeValues }; delete newAttributeValues[attributeId]; setSelectedAttributes(newSelectedAttributes); setAttributeValues(newAttributeValues); handleFieldChange("attributes", newAttributeValues); }; // Handle attribute value change const handleAttributeValueChange = (attributeId: number, value: any) => { const newAttributeValues = { ...attributeValues, [attributeId]: value }; setAttributeValues(newAttributeValues); handleFieldChange("attributes", newAttributeValues); }; // Render attribute input based on field type const renderAttributeInput = (attribute: Attribute) => { const raw = attributeValues[attribute.id]; const value = normalizeAttributeStoredValue(attribute.field_type, raw); switch (attribute.field_type) { case "text_field": case "email": case "url": return ( handleAttributeValueChange(attribute.id, e.target.value) } placeholder={attribute.placeholder || __("Enter value", "yatra")} className="mt-2" /> ); case "number": return ( handleAttributeValueChange(attribute.id, e.target.value) } placeholder={attribute.placeholder || __("Enter number", "yatra")} className="mt-2" /> ); case "textarea": return (