import React, { useState, useEffect, useMemo, useRef } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Switch } from '@/components/ui/switch' import { Ic } from '@/icons' import { ShimmerBox, ShimmerLine } from '@/components/shared/shimmer_frontend' import { TourDetailModalShimmer, TourItineraryShimmer, TourPassengersShimmer, TourCommentsShimmer, } from './TourDetailShimmer' import { fetchTourDetail, fetchTourItineraries, fetchTourPassengers, fetchTourComments, fetchTourTranslations, saveTourTranslations, fetchTourItineraryTranslations, saveTourItineraryTranslationsBatch, createTourItineraryStep, deleteTourItineraryStep, updateTour, deleteTour, toggleTourVisibility, uploadTourImage, } from '../services/tour-api' import type { TourDetailItem, TourItineraryItem, TourPassengerGroupItem, TourCommentItem, TourTranslationsResponse, TourItineraryTranslationsResponseItem, TourTranslationData, } from '../types' import { TourStatusBadge } from './TourStatusBadge' import DeleteConfirmDialog from '@/components/shared/DeleteConfirmDialog' interface TourDetailDialogProps { tourId: number | null isOpen: boolean onClose: () => void onUpdated?: () => void } const SUPPORTED_LANGUAGES = [ { code: 'ar', label: 'العربية', flag: '🇸🇦', dir: 'rtl' }, { code: 'fa', label: 'فارسی', flag: '🇮🇷', dir: 'rtl' }, { code: 'en', label: 'English', flag: '🇬🇧', dir: 'ltr' }, { code: 'ru', label: 'Русский', flag: '🇷🇺', dir: 'ltr' }, { code: 'id', label: 'Indonesia', flag: '🇮🇩', dir: 'ltr' }, ] as const type LangCode = (typeof SUPPORTED_LANGUAGES)[number]['code'] interface LangDraft { title: string description: string features: string[] tips: { title: string; desc: string }[] } type TranslationsDraftMap = Record type ItinDraftMap = Record> interface CoreTourFields { started_at: string ended_at: string capacity: number | string status: string trip_status: string slug: string price: string price_child: string price_infant: string percent_off: number | string visibility: boolean } interface TourImageDraftItem { id?: number // TourImage ID (if existing on server) filer_id?: number // FilerImage ID (for newly uploaded images) url: string isNew?: boolean isUploading?: boolean localId?: string } const createEmptyLangDraft = (): LangDraft => ({ title: '', description: '', features: [], tips: [], }) const createEmptyTranslationsMap = (): TranslationsDraftMap => ({ fa: createEmptyLangDraft(), ar: createEmptyLangDraft(), en: createEmptyLangDraft(), ru: createEmptyLangDraft(), id: createEmptyLangDraft(), }) const createEmptyCoreFields = (): CoreTourFields => ({ started_at: '', ended_at: '', capacity: 0, status: 'AVAILABLE', trip_status: 'PENDING', slug: '', price: '0', price_child: '0', price_infant: '0', percent_off: 0, visibility: true, }) export function TourDetailDialog({ tourId, isOpen, onClose, onUpdated, }: TourDetailDialogProps) { const [activeTab, setActiveTab] = useState<'overview' | 'itinerary' | 'features' | 'passengers' | 'comments'>('overview') const [selectedLang, setSelectedLang] = useState('ar') // Edit mode toggle (Default: false / Read-Only) const [isEditMode, setIsEditMode] = useState(false) // Main tour data from server const [tour, setTour] = useState(null) const [itineraries, setItineraries] = useState([]) const [passengersGroup, setPassengersGroup] = useState([]) const [comments, setComments] = useState([]) const [transData, setTransData] = useState(null) const [itinTransList, setItinTransList] = useState([]) // Unified Multi-Language Form States (Holds in-memory edits across all 5 languages) const [allTranslations, setAllTranslations] = useState(createEmptyTranslationsMap) const [initialTranslations, setInitialTranslations] = useState(createEmptyTranslationsMap) // Itineraries drafts across all languages: { [itId]: { fa: { title, summary }, ar: ... } } const [allItinDrafts, setAllItinDrafts] = useState({}) const [initialItinDrafts, setInitialItinDrafts] = useState({}) // Core tour properties state const [coreFields, setCoreFields] = useState(createEmptyCoreFields) const [initialCoreFields, setInitialCoreFields] = useState(createEmptyCoreFields) // Image management states const [tourImages, setTourImages] = useState([]) const [initialTourImages, setInitialTourImages] = useState([]) const [deletedTourImageIds, setDeletedTourImageIds] = useState([]) const [newFilerImageIds, setNewFilerImageIds] = useState([]) const [isUploadingImage, setIsUploadingImage] = useState(false) const [imageUploadError, setImageUploadError] = useState(null) const fileInputRef = useRef(null) // Temporary local inputs for adding feature / tip const [newFeatureInput, setNewFeatureInput] = useState('') const [newTipTitle, setNewTipTitle] = useState('') const [newTipDesc, setNewTipDesc] = useState('') // Add new itinerary step state const [showAddItinForm, setShowAddItinForm] = useState(false) const [newItinTitle, setNewItinTitle] = useState('') const [newItinSummary, setNewItinSummary] = useState('') const [isAddingItin, setIsAddingItin] = useState(false) // Unsaved changes confirmation dialog state const [showUnsavedDialog, setShowUnsavedDialog] = useState(false) const [pendingAction, setPendingAction] = useState<'close' | 'toggle_edit' | null>(null) // Delete Tour confirmation dialog state const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [isDeletingTour, setIsDeletingTour] = useState(false) // Loading and feedback states const [isLoading, setIsLoading] = useState(false) const [isLoadingSub, setIsLoadingSub] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isTogglingVisibility, setIsTogglingVisibility] = useState(false) const [error, setError] = useState(null) const [successMessage, setSuccessMessage] = useState(null) // Load tour detail and all translations on modal open useEffect(() => { if (!tourId || !isOpen) { setTour(null) setTransData(null) setIsEditMode(false) setShowAddItinForm(false) setShowUnsavedDialog(false) setShowDeleteDialog(false) setPendingAction(null) setTourImages([]) setInitialTourImages([]) setDeletedTourImageIds([]) setNewFilerImageIds([]) setImageUploadError(null) return } let isMounted = true setIsLoading(true) setError(null) setSuccessMessage(null) setActiveTab('overview') setIsEditMode(false) setShowAddItinForm(false) Promise.all([ fetchTourDetail(tourId), fetchTourTranslations(tourId).catch(() => null), fetchTourItineraryTranslations(tourId).catch(() => []), ]) .then(([detail, trans, itinTrans]) => { if (!isMounted) return setTour(detail) setTransData(trans) setItinTransList(itinTrans || []) // Initialize Core Fields const initialCore: CoreTourFields = { started_at: detail.started_at || '', ended_at: detail.ended_at || '', capacity: detail.capacity || 0, status: detail.status || 'AVAILABLE', trip_status: detail.trip_status || 'PENDING', slug: detail.slug || '', price: detail.price ? String(parseFloat(detail.price)) : '0', price_child: detail.price_child ? String(parseFloat(detail.price_child)) : '0', price_infant: detail.price_infant ? String(parseFloat(detail.price_infant)) : '0', percent_off: detail.percent_off || 0, visibility: detail.visibility !== false, } setCoreFields(initialCore) setInitialCoreFields(initialCore) // Initialize Images const initialImgs: TourImageDraftItem[] = (detail.images || []).map((img) => ({ id: img.id, url: img.image_url?.md || img.image_url?.medium || img.image_url?.lg || img.image_url?.large || img.image_url?.original || '/placeholder.jpg', })) setTourImages(initialImgs) setInitialTourImages(JSON.parse(JSON.stringify(initialImgs))) setDeletedTourImageIds([]) setNewFilerImageIds([]) setImageUploadError(null) // Initialize 5 Languages Unified Drafts const initialDrafts = createEmptyTranslationsMap() const primaryCode = (trans?.primary_language || 'ar') as LangCode SUPPORTED_LANGUAGES.forEach((lang) => { const isPrimary = primaryCode === lang.code const langTrans = trans?.translations?.[lang.code] if (langTrans) { initialDrafts[lang.code] = { title: langTrans.title ?? '', description: langTrans.description ?? '', features: (langTrans.tour_feature || []).map((f: any) => (typeof f === 'string' ? f : f.title)), tips: Object.entries(langTrans.travel_tips || {}).map(([tTitle, tDesc]) => ({ title: tTitle, desc: String(tDesc), })), } } else if (isPrimary && trans?.primary_data) { initialDrafts[lang.code] = { title: trans.primary_data.title ?? '', description: trans.primary_data.description ?? '', features: (trans.primary_data.tour_feature || []).map((f: any) => (typeof f === 'string' ? f : f.title)), tips: Object.entries(trans.primary_data.travel_tips || {}).map(([tTitle, tDesc]) => ({ title: tTitle, desc: String(tDesc), })), } } else if (isPrimary && detail) { initialDrafts[lang.code] = { title: detail.title ?? '', description: detail.description ?? '', features: (detail.tour_features || []).map((f) => f.title), tips: (detail.travel_tips || []).map((t) => ({ title: t.title, desc: t.description, })), } } else { initialDrafts[lang.code] = createEmptyLangDraft() } }) setAllTranslations(initialDrafts) setInitialTranslations(JSON.parse(JSON.stringify(initialDrafts))) setSelectedLang(primaryCode) // Initialize Itinerary Drafts for all languages const initialItins: ItinDraftMap = {} const itinList = itinTrans || [] itinList.forEach((it) => { initialItins[it.id] = {} as Record SUPPORTED_LANGUAGES.forEach((lang) => { const itTrans = it.translations[lang.code] initialItins[it.id][lang.code] = { title: itTrans?.title ?? (lang.code === primaryCode ? it.title : ''), summary: itTrans?.summary ?? (lang.code === primaryCode ? it.summary : ''), } }) }) setAllItinDrafts(initialItins) setInitialItinDrafts(JSON.parse(JSON.stringify(initialItins))) setIsLoading(false) }) .catch((err: any) => { if (isMounted) { setError(err?.message || 'خطا در بارگذاری اطلاعات کامل تور') setIsLoading(false) } }) return () => { isMounted = false } }, [tourId, isOpen]) // Load sub-tabs (passengers, comments, itineraries) on demand useEffect(() => { if (!tourId || !isOpen || !tour) return let isMounted = true if (activeTab === 'itinerary' && itineraries.length === 0) { setIsLoadingSub(true) fetchTourItineraries(tourId) .then((res) => { if (isMounted) { setItineraries(res.results || []) setIsLoadingSub(false) } }) .catch(() => isMounted && setIsLoadingSub(false)) } else if (activeTab === 'passengers' && passengersGroup.length === 0) { setIsLoadingSub(true) fetchTourPassengers(tourId) .then((res) => { if (isMounted) { setPassengersGroup(res.results || []) setIsLoadingSub(false) } }) .catch(() => isMounted && setIsLoadingSub(false)) } else if (activeTab === 'comments' && comments.length === 0 && tour.slug) { setIsLoadingSub(true) fetchTourComments(tour.slug) .then((res) => { if (isMounted) { setComments(res.results || []) setIsLoadingSub(false) } }) .catch(() => isMounted && setIsLoadingSub(false)) } return () => { isMounted = false } }, [activeTab, tourId, isOpen, tour, itineraries.length, passengersGroup.length, comments.length]) // Check if a specific language has unsaved changes const isLanguageDirty = (langCode: LangCode): boolean => { const cur = allTranslations[langCode] const init = initialTranslations[langCode] if (!cur || !init) return false if (cur.title !== init.title || cur.description !== init.description) return true if (JSON.stringify(cur.features) !== JSON.stringify(init.features)) return true if (JSON.stringify(cur.tips) !== JSON.stringify(init.tips)) return true // Check itineraries for this language for (const itId of Object.keys(allItinDrafts).map(Number)) { const curIt = allItinDrafts[itId]?.[langCode] const initIt = initialItinDrafts[itId]?.[langCode] if (curIt && initIt) { if (curIt.title !== initIt.title || curIt.summary !== initIt.summary) return true } else if (curIt && (curIt.title || curIt.summary)) { return true } } return false } // Global isDirty check across core fields, images, all 5 languages, and itineraries const isDirty = useMemo(() => { if (!tour) return false // Core fields check const coreChanged = coreFields.started_at !== initialCoreFields.started_at || coreFields.ended_at !== initialCoreFields.ended_at || String(coreFields.capacity) !== String(initialCoreFields.capacity) || coreFields.status !== initialCoreFields.status || coreFields.trip_status !== initialCoreFields.trip_status || coreFields.slug !== initialCoreFields.slug || String(coreFields.price) !== String(initialCoreFields.price) || String(coreFields.price_child) !== String(initialCoreFields.price_child) || String(coreFields.price_infant) !== String(initialCoreFields.price_infant) || String(coreFields.percent_off) !== String(initialCoreFields.percent_off) || coreFields.visibility !== initialCoreFields.visibility if (coreChanged) return true // Images check const imagesChanged = deletedTourImageIds.length > 0 || newFilerImageIds.length > 0 || tourImages.length !== initialTourImages.length if (imagesChanged) return true // Check all 5 languages for (const lang of SUPPORTED_LANGUAGES) { if (isLanguageDirty(lang.code)) return true } return false }, [ coreFields, initialCoreFields, tourImages, initialTourImages, deletedTourImageIds, newFilerImageIds, allTranslations, initialTranslations, allItinDrafts, initialItinDrafts, tour, ]) // Validation: No tour can be without an image const hasNoImages = tourImages.length === 0 // Switching language preserves all typed values seamlessly const handleLanguageSwitch = (lang: LangCode) => { setSelectedLang(lang) setSuccessMessage(null) setNewFeatureInput('') setNewTipTitle('') setNewTipDesc('') } // Current active language draft const currentLangDraft = allTranslations[selectedLang] || createEmptyLangDraft() const handleTitleChange = (val: string) => { setAllTranslations((prev) => ({ ...prev, [selectedLang]: { ...prev[selectedLang], title: val }, })) } const handleDescChange = (val: string) => { setAllTranslations((prev) => ({ ...prev, [selectedLang]: { ...prev[selectedLang], description: val }, })) } const handleAddFeature = () => { if (!newFeatureInput.trim()) return setAllTranslations((prev) => ({ ...prev, [selectedLang]: { ...prev[selectedLang], features: [...prev[selectedLang].features, newFeatureInput.trim()], }, })) setNewFeatureInput('') } const handleRemoveFeature = (idx: number) => { setAllTranslations((prev) => ({ ...prev, [selectedLang]: { ...prev[selectedLang], features: prev[selectedLang].features.filter((_, i) => i !== idx), }, })) } const handleAddTip = () => { if (!newTipTitle.trim() || !newTipDesc.trim()) return setAllTranslations((prev) => ({ ...prev, [selectedLang]: { ...prev[selectedLang], tips: [...prev[selectedLang].tips, { title: newTipTitle.trim(), desc: newTipDesc.trim() }], }, })) setNewTipTitle('') setNewTipDesc('') } const handleRemoveTip = (idx: number) => { setAllTranslations((prev) => ({ ...prev, [selectedLang]: { ...prev[selectedLang], tips: prev[selectedLang].tips.filter((_, i) => i !== idx), }, })) } const handleItinDraftChange = (itId: number, field: 'title' | 'summary', val: string) => { setAllItinDrafts((prev) => { const itEntry = prev[itId] || ({} as Record) const langEntry = itEntry[selectedLang] || { title: '', summary: '' } return { ...prev, [itId]: { ...itEntry, [selectedLang]: { ...langEntry, [field]: val, }, }, } }) } // Fast interactive toggle for Tour Visibility (Active / Inactive) const handleToggleVisibility = async (newVal: boolean) => { if (!tourId) return // If currently in edit mode, update draft coreFields state if (isEditMode) { setCoreFields((prev) => ({ ...prev, visibility: newVal })) return } // If in view mode, perform instant update via toggleTourVisibility API setIsTogglingVisibility(true) setError(null) try { await toggleTourVisibility(tourId, newVal) setCoreFields((prev) => ({ ...prev, visibility: newVal })) setInitialCoreFields((prev) => ({ ...prev, visibility: newVal })) setTour((prev) => (prev ? { ...prev, visibility: newVal } : prev)) setSuccessMessage( newVal ? 'تور با موفقیت در اپلیکیشن و وب‌سایت فعال (روشن) گردید.' : 'تور با موفقیت غیرفعال (خاموش) و در اپلیکیشن و وب‌سایت مخفی گردید.' ) if (onUpdated) onUpdated() } catch (err: any) { setError(err?.message || 'خطا در تغییر وضعیت فعال/غیرفعال بودن تور') } finally { setIsTogglingVisibility(false) } } // Upload image handler with instant previews and parallel processing const handleImageUpload = async (e: React.ChangeEvent) => { const files = e.target.files if (!files || files.length === 0) return const validFiles: File[] = [] for (let i = 0; i < files.length; i++) { const file = files[i] if (file.type.startsWith('image/')) { validFiles.push(file) } } if (validFiles.length === 0) { setImageUploadError('لطفاً فقط فایل‌های تصویری معتبر (JPG، PNG، WEBP) انتخاب نمایید.') if (fileInputRef.current) fileInputRef.current.value = '' return } setIsUploadingImage(true) setImageUploadError(null) // 1. Create immediate local preview placeholders const pendingItems: TourImageDraftItem[] = validFiles.map((file) => ({ localId: `pending-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, url: URL.createObjectURL(file), isNew: true, isUploading: true, })) setTourImages((prev) => [...prev, ...pendingItems]) // 2. Upload files in parallel await Promise.all( validFiles.map(async (file, idx) => { const pendingItem = pendingItems[idx] try { const res = await uploadTourImage(file) setTourImages((prev) => prev.map((item) => item.localId === pendingItem.localId ? { filer_id: res.id, url: res.url, isNew: true, isUploading: false } : item ) ) setNewFilerImageIds((prev) => [...prev, res.id]) } catch (err: any) { // If upload fails, remove the pending placeholder setTourImages((prev) => prev.filter((item) => item.localId !== pendingItem.localId)) setImageUploadError(err?.message || `خطا در آپلود تصویر «${file.name}». لطفاً مجدداً تلاش کنید.`) } }) ) setIsUploadingImage(false) if (fileInputRef.current) { fileInputRef.current.value = '' } } // Delete image from tour draft const handleDeleteImage = (index: number) => { const target = tourImages[index] if (!target) return if (target.id) { setDeletedTourImageIds((prev) => [...prev, target.id!]) } if (target.isNew && target.filer_id) { setNewFilerImageIds((prev) => prev.filter((id) => id !== target.filer_id)) } setTourImages((prev) => prev.filter((_, i) => i !== index)) setImageUploadError(null) } // Create a new itinerary step const handleCreateItineraryStep = async () => { if (!tourId || !newItinTitle.trim()) return setIsAddingItin(true) setError(null) setSuccessMessage(null) try { const res = await createTourItineraryStep(tourId, { title: newItinTitle.trim(), summary: newItinSummary.trim(), lang_code: selectedLang, }) const updatedItinTrans = await fetchTourItineraryTranslations(tourId) setItinTransList(updatedItinTrans) const updatedItins = await fetchTourItineraries(tourId) setItineraries(updatedItins.results || []) setAllItinDrafts((prev) => { const itEntry = prev[res.itinerary_id] || ({} as Record) return { ...prev, [res.itinerary_id]: { ...itEntry, [selectedLang]: { title: newItinTitle.trim(), summary: newItinSummary.trim(), }, }, } }) setNewItinTitle('') setNewItinSummary('') setShowAddItinForm(false) setSuccessMessage(`مرحله جدید با موفقیت به برنامه سفر (${activeLangConfig.label}) افزوده شد.`) if (onUpdated) onUpdated() } catch (err: any) { setError(err?.message || 'خطا در افزودن مرحله جدید به برنامه سفر') } finally { setIsAddingItin(false) } } // Delete an itinerary step const handleDeleteItineraryStep = async (itId: number) => { if (!tourId) return if (!confirm('آیا از حذف این مرحله از برنامه سفر اطمینان دارید؟')) return setIsSaving(true) setError(null) try { await deleteTourItineraryStep(tourId, itId) const updatedItinTrans = await fetchTourItineraryTranslations(tourId) setItinTransList(updatedItinTrans) const updatedItins = await fetchTourItineraries(tourId) setItineraries(updatedItins.results || []) setAllItinDrafts((prev) => { const next = { ...prev } delete next[itId] return next }) setInitialItinDrafts((prev) => { const next = { ...prev } delete next[itId] return next }) setSuccessMessage('مرحله مورد نظر از برنامه سفر حذف شد.') if (onUpdated) onUpdated() } catch (err: any) { setError(err?.message || 'خطا در حذف مرحله برنامه سفر') } finally { setIsSaving(false) } } // Global One-Click Save: Saves all 5 languages, core fields, images, and itinerary steps in batch const handleSaveAllChanges = async () => { if (!tourId) return if (hasNoImages) { setError('وجود حداقل یک تصویر برای تور الزامی است. لطفاً حداقل یک تصویر آپلود نمایید.') return } setIsSaving(true) setError(null) setSuccessMessage(null) try { // 1. Prepare multi-language translations payload const translationsPayload: Record = {} SUPPORTED_LANGUAGES.forEach((lang) => { const draft = allTranslations[lang.code] if (draft) { const travel_tips_obj: Record = {} draft.tips.forEach((t) => { if (t.title.trim()) { travel_tips_obj[t.title.trim()] = t.desc.trim() } }) translationsPayload[lang.code] = { title: draft.title.trim(), description: draft.description.trim(), tour_feature: draft.features.map((f) => ({ title: f.trim() })).filter((f) => f.title), travel_tips: travel_tips_obj, } } }) // 2. Prepare core details payload including image updates const corePayload: any = { started_at: coreFields.started_at, ended_at: coreFields.ended_at, capacity: Number(coreFields.capacity) || 0, status: coreFields.status, trip_status: coreFields.trip_status, slug: coreFields.slug.trim(), price: coreFields.price, price_child: coreFields.price_child, price_infant: coreFields.price_infant, percent_off: Number(coreFields.percent_off) || 0, visibility: coreFields.visibility, } if (deletedTourImageIds.length > 0) { corePayload.deleted_image_ids = deletedTourImageIds } if (newFilerImageIds.length > 0) { corePayload.image_ids = newFilerImageIds } // 3. Prepare itinerary batch payload const itineraryBatch: Array<{ itinerary_id: number lang_code: string title: string summary: string }> = [] const itIds = Object.keys(allItinDrafts).map(Number) itIds.forEach((itId) => { SUPPORTED_LANGUAGES.forEach((lang) => { const item = allItinDrafts[itId]?.[lang.code] if (item) { itineraryBatch.push({ itinerary_id: itId, lang_code: lang.code, title: item.title.trim(), summary: item.summary.trim(), }) } }) }) // Execute all updates concurrently const promises: Promise[] = [ updateTour(tourId, corePayload), saveTourTranslations(tourId, { translations: translationsPayload }), ] if (itineraryBatch.length > 0) { promises.push(saveTourItineraryTranslationsBatch(tourId, itineraryBatch)) } await Promise.all(promises) // Refresh full tour details, translations, and itinerary data from backend const [freshDetail, freshTrans, freshItinTrans] = await Promise.all([ fetchTourDetail(tourId), fetchTourTranslations(tourId).catch(() => null), fetchTourItineraryTranslations(tourId).catch(() => []), ]) setTour(freshDetail) setTransData(freshTrans) setItinTransList(freshItinTrans || []) // Re-initialize images state from server response const updatedImgs: TourImageDraftItem[] = (freshDetail.images || []).map((img) => ({ id: img.id, url: img.image_url?.md || img.image_url?.medium || img.image_url?.lg || img.image_url?.large || img.image_url?.original || '/placeholder.jpg', })) setTourImages(updatedImgs) setInitialTourImages(JSON.parse(JSON.stringify(updatedImgs))) setDeletedTourImageIds([]) setNewFilerImageIds([]) // Update baseline states to reset isDirty setInitialCoreFields({ ...coreFields }) setInitialTranslations(JSON.parse(JSON.stringify(allTranslations))) setInitialItinDrafts(JSON.parse(JSON.stringify(allItinDrafts))) setSuccessMessage('تمامی تغییرات مشخصات تور، تصاویر، ترجمه‌های 5 زبانه و برنامه سفر با موفقیت ذخیره شدند.') if (onUpdated) onUpdated() } catch (err: any) { setError(err?.message || 'خطا در ذخیره یکپارچه اطلاعات تور') } finally { setIsSaving(false) } } // Delete Tour Handler const handleConfirmDeleteTour = async () => { if (!tourId) return setIsDeletingTour(true) setError(null) try { await deleteTour(tourId) setShowDeleteDialog(false) onClose() if (onUpdated) onUpdated() } catch (err: any) { setError(err?.message || 'خطا در حذف تور') setShowDeleteDialog(false) } finally { setIsDeletingTour(false) } } // Guard exit / close against unsaved changes const handleRequestClose = () => { if (isDirty) { setPendingAction('close') setShowUnsavedDialog(true) } else { onClose() } } const handleToggleEditMode = () => { if (isEditMode) { if (isDirty) { setPendingAction('toggle_edit') setShowUnsavedDialog(true) } else { setIsEditMode(false) setShowAddItinForm(false) } } else { setIsEditMode(true) } } const handleDiscardAndProceed = () => { // Revert form state back to initial setCoreFields({ ...initialCoreFields }) setAllTranslations(JSON.parse(JSON.stringify(initialTranslations))) setAllItinDrafts(JSON.parse(JSON.stringify(initialItinDrafts))) setTourImages(JSON.parse(JSON.stringify(initialTourImages))) setDeletedTourImageIds([]) setNewFilerImageIds([]) setImageUploadError(null) setShowUnsavedDialog(false) if (pendingAction === 'close') { onClose() } else if (pendingAction === 'toggle_edit') { setIsEditMode(false) setShowAddItinForm(false) } setPendingAction(null) } const handleSaveAndProceed = async () => { if (hasNoImages) { setError('وجود حداقل یک تصویر برای تور الزامی است. لطفاً حداقل یک تصویر آپلود نمایید.') setShowUnsavedDialog(false) return } await handleSaveAllChanges() setShowUnsavedDialog(false) if (pendingAction === 'close') { onClose() } else if (pendingAction === 'toggle_edit') { setIsEditMode(false) setShowAddItinForm(false) } setPendingAction(null) } const activeLangConfig = SUPPORTED_LANGUAGES.find((l) => l.code === selectedLang) || SUPPORTED_LANGUAGES[0] // Calculated discounted adult price for live preview const adultNum = parseFloat(coreFields.price) || 0 const discNum = parseFloat(String(coreFields.percent_off)) || 0 const discountedAdult = Math.max(0, adultNum * (1 - discNum / 100)) return ( <> !open && handleRequestClose()}> {/* Header Modal */}
{/* بخش راست: آیکون، عنوان تور، وضعیت سفر و تاگل روشن/خاموش */}
{isLoading && !tour ? ( ) : ( tour?.title || 'جزئیات و مشخصات تور' )} {tour && (
{/* بج وضعیت برگزاری سفر */} {/* تاگل تعاملی روشن/خاموش وضعیت فعال/غیرفعال بودن تور */}
e.stopPropagation()} className="flex items-center gap-2 rounded-xl border border-border-soft bg-card/60 px-3 py-1.5 shadow-2xs transition" > نمایش در اپ و وب: {coreFields.visibility ? 'روشن (فعال)' : 'خاموش (مخفی)'}
)}
{/* دکمه‌های عملیاتی ثابت و بدون شکستن خط (Action Buttons) */}
{isLoading && !tour ? (
) : tour ? ( <> {/* دکمه حذف تور در حالت ویرایش */} {isEditMode && ( )} {/* دکمه سراسری ثبت نهایی تغییرات (در حالت ویرایش) */} {isEditMode && ( )} {/* دکمه سوییچ حالت فقط‌خواندنی / ویرایش */} ) : null} {/* دکمه بستن پنجره */}
{/* Integrated 5-Language Selector Bar with dirty indicators */}
زبان فعال محتوا:
{SUPPORTED_LANGUAGES.map((lang) => { const isSelected = lang.code === selectedLang const isPrimary = transData?.primary_language === lang.code const hasDraftData = Boolean( allTranslations[lang.code]?.title || allTranslations[lang.code]?.description || allTranslations[lang.code]?.features.length > 0 || allTranslations[lang.code]?.tips.length > 0 ) const dirty = isLanguageDirty(lang.code) return ( ) })}
{/* Main Navigation Tabs */} setActiveTab(val)} className="mt-3 w-full" > مشخصات و متون برنامه سفر امکانات و نکات مسافران نظرات
{/* Modal Scrollable Body */}
{successMessage && (
{successMessage}
)} {isLoading ? ( ) : error ? (

{error}

) : tour ? ( <> {/* تب ۱: مشخصات و متون تور */} {activeTab === 'overview' && (
{/* بخش ترجمه پویا برای عنوان و توضیحات تور */}
{activeLangConfig.flag} محتوای متنی تور به زبان {activeLangConfig.label} ({activeLangConfig.code})
جهت متن: {activeLangConfig.dir.toUpperCase()}
handleTitleChange(e.target.value)} placeholder={`عنوان تور به زبان ${activeLangConfig.label}...`} dir={activeLangConfig.dir} className="bg-card font-medium text-xs rounded-xl border-border-soft disabled:opacity-85 disabled:cursor-default" />