import * as React from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, } from '@/components/ui/dialog' import { Button, ButtonSpinner } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Ic } from '@/icons' import { createTour, uploadTourImage, type CreateTourPayload } from '../services/tour-api' import type { TourTranslationData } from '../types' interface CreateTourDialogProps { open: boolean onOpenChange: (open: boolean) => void onSuccess?: () => 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'] export function CreateTourDialog({ open, onOpenChange, onSuccess, }: CreateTourDialogProps) { const [activeTab, setActiveTab] = React.useState<'basic' | 'features_tips' | 'translations'>('basic') const [selectedTransLang, setSelectedTransLang] = React.useState('en') const [isSubmitting, setIsSubmitting] = React.useState(false) const [errorMessage, setErrorMessage] = React.useState(null) const [successMessage, setSuccessMessage] = React.useState(null) // Basic info states const [title, setTitle] = React.useState('') const [description, setDescription] = React.useState('') const [startedAt, setStartedAt] = React.useState('') const [endedAt, setEndedAt] = React.useState('') const [capacity, setCapacity] = React.useState('40') const [primaryLang, setPrimaryLang] = React.useState('ar') const [status, setStatus] = React.useState('AVAILABLE') const [visibility, setVisibility] = React.useState(true) // Image states (mandatory cover photo) const [imageId, setImageId] = React.useState(null) const [imageUrl, setImageUrl] = React.useState(null) const [imageName, setImageName] = React.useState(null) const [isUploadingImage, setIsUploadingImage] = React.useState(false) const [imageUploadError, setImageUploadError] = React.useState(null) const fileInputRef = React.useRef(null) // Pricing states const [price, setPrice] = React.useState('') const [priceChild, setPriceChild] = React.useState('') const [priceInfant, setPriceInfant] = React.useState('') const [percentOff, setPercentOff] = React.useState('0') // Base features & tips const [features, setFeatures] = React.useState([]) const [newFeature, setNewFeature] = React.useState('') const [tips, setTips] = React.useState<{ title: string; desc: string }[]>([]) const [newTipTitle, setNewTipTitle] = React.useState('') const [newTipDesc, setNewTipDesc] = React.useState('') // Multi-language translation state const [translations, setTranslations] = React.useState>({ fa: { title: '', description: '', tour_feature: [], travel_tips: {} }, ar: { title: '', description: '', tour_feature: [], travel_tips: {} }, en: { title: '', description: '', tour_feature: [], travel_tips: {} }, ru: { title: '', description: '', tour_feature: [], travel_tips: {} }, id: { title: '', description: '', tour_feature: [], travel_tips: {} }, }) // Reset form when opened React.useEffect(() => { if (open) { setErrorMessage(null) setSuccessMessage(null) setActiveTab('basic') setImageId(null) setImageUrl(null) setImageName(null) setImageUploadError(null) } }, [open]) const processImageUpload = async (file: File) => { if (!file.type.startsWith('image/')) { setImageUploadError('لطفاً فقط فایل تصویری (JPG، PNG، WEBP) انتخاب نمایید.') return } if (file.size > 15 * 1024 * 1024) { setImageUploadError('حجم تصویر نباید بیشتر از 15 مگابایت باشد.') return } setImageUploadError(null) setIsUploadingImage(true) try { const res = await uploadTourImage(file) setImageId(res.id) setImageUrl(res.url || URL.createObjectURL(file)) setImageName(file.name) } catch (err: any) { setImageUploadError(err?.message || 'خطا در آپلود تصویر در سرور.') } finally { setIsUploadingImage(false) } } const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return await processImageUpload(file) } const handleRemoveImage = () => { setImageId(null) setImageUrl(null) setImageName(null) setImageUploadError(null) if (fileInputRef.current) { fileInputRef.current.value = '' } } const handleAddFeature = () => { if (!newFeature.trim()) return setFeatures((prev) => [...prev, newFeature.trim()]) setNewFeature('') } const handleRemoveFeature = (idx: number) => { setFeatures((prev) => prev.filter((_, i) => i !== idx)) } const handleAddTip = () => { if (!newTipTitle.trim() || !newTipDesc.trim()) return setTips((prev) => [...prev, { title: newTipTitle.trim(), desc: newTipDesc.trim() }]) setNewTipTitle('') setNewTipDesc('') } const handleRemoveTip = (idx: number) => { setTips((prev) => prev.filter((_, i) => i !== idx)) } // Helper for updating translation fields for selectedTransLang const handleTransFieldChange = (field: 'title' | 'description', val: string) => { setTranslations((prev) => ({ ...prev, [selectedTransLang]: { ...prev[selectedTransLang], [field]: val, }, })) } const handleAddTransFeature = (text: string) => { if (!text.trim()) return setTranslations((prev) => { const current = prev[selectedTransLang] || { tour_feature: [] } const currFeats = current.tour_feature || [] return { ...prev, [selectedTransLang]: { ...current, tour_feature: [...currFeats, { title: text.trim() }], }, } }) } const handleRemoveTransFeature = (idx: number) => { setTranslations((prev) => { const current = prev[selectedTransLang] || { tour_feature: [] } const currFeats = current.tour_feature || [] return { ...prev, [selectedTransLang]: { ...current, tour_feature: currFeats.filter((_, i) => i !== idx), }, } }) } const handleAddTransTip = (tipTitle: string, tipDesc: string) => { if (!tipTitle.trim() || !tipDesc.trim()) return setTranslations((prev) => { const current = prev[selectedTransLang] || { travel_tips: {} } const currTips = current.travel_tips || {} return { ...prev, [selectedTransLang]: { ...current, travel_tips: { ...currTips, [tipTitle.trim()]: tipDesc.trim(), }, }, } }) } const handleRemoveTransTip = (tipTitle: string) => { setTranslations((prev) => { const current = prev[selectedTransLang] || { travel_tips: {} } const currTips = { ...(current.travel_tips || {}) } delete currTips[tipTitle] return { ...prev, [selectedTransLang]: { ...current, travel_tips: currTips, }, } }) } const [newTransFeatText, setNewTransFeatText] = React.useState('') const [newTransTipTitle, setNewTransTipTitle] = React.useState('') const [newTransTipDesc, setNewTransTipDesc] = React.useState('') const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setErrorMessage(null) setSuccessMessage(null) // Validation if (!title.trim()) { setErrorMessage('لطفاً عنوان اصلی تور را وارد نمایید.') setActiveTab('basic') return } if (!imageId) { setErrorMessage('بارگذاری تصویر شاخص برای ایجاد تور الزامی است.') setActiveTab('basic') return } if (!startedAt) { setErrorMessage('لطفاً تاریخ شروع تور را مشخص نمایید.') setActiveTab('basic') return } if (!endedAt) { setErrorMessage('لطفاً تاریخ پایان تور را مشخص نمایید.') setActiveTab('basic') return } if (new Date(endedAt) < new Date(startedAt)) { setErrorMessage('تاریخ پایان تور نمی‌تواند قبل از تاریخ شروع باشد.') setActiveTab('basic') return } if (!price || isNaN(Number(price)) || Number(price) <= 0) { setErrorMessage('لطفاً قیمت معتبر برای بزرگسال وارد نمایید.') setActiveTab('basic') return } if (!capacity || isNaN(Number(capacity)) || Number(capacity) <= 0) { setErrorMessage('لطفاً ظرفیت معتبر برای تور وارد نمایید.') setActiveTab('basic') return } setIsSubmitting(true) try { const tour_feature = features.map((f) => ({ title: f })) const travel_tips: Record = {} tips.forEach((t) => { travel_tips[t.title] = t.desc }) // Clean translations payload: include any language that has data const cleanedTranslations: Record = {} Object.entries(translations).forEach(([code, data]) => { const hasTitle = Boolean(data.title?.trim()) const hasDesc = Boolean(data.description?.trim()) const hasFeats = Boolean(data.tour_feature && data.tour_feature.length > 0) const hasTips = Boolean(data.travel_tips && Object.keys(data.travel_tips).length > 0) if (hasTitle || hasDesc || hasFeats || hasTips) { cleanedTranslations[code] = { title: data.title?.trim() || (code === primaryLang ? title.trim() : ''), description: data.description?.trim() || '', tour_feature: data.tour_feature || [], travel_tips: data.travel_tips || {}, } } }) // Ensure primary language translation is also present if not defined if (!cleanedTranslations[primaryLang]) { cleanedTranslations[primaryLang] = { title: title.trim(), description: description.trim(), tour_feature, travel_tips, } } const payload: CreateTourPayload = { title: title.trim(), description: description.trim(), started_at: startedAt, ended_at: endedAt, capacity: Number(capacity), price: String(price), price_child: priceChild ? String(priceChild) : '0', price_infant: priceInfant ? String(priceInfant) : '0', percent_off: percentOff ? Number(percentOff) : 0, language_code: primaryLang, status, tour_feature, travel_tips, visibility, translations: cleanedTranslations, image_id: imageId, image_ids: [imageId], } const createdTour = await createTour(payload) setSuccessMessage(`تور "${createdTour.title}" با موفقیت ایجاد و ترجمه‌ها در دیتابیس ثبت گردید.`) setTimeout(() => { if (onSuccess) onSuccess() onOpenChange(false) }, 900) } catch (err: any) { setErrorMessage(err?.message || 'خطا در ثبت تور جدید در سرور.') } finally { setIsSubmitting(false) } } const activeTransConfig = SUPPORTED_LANGUAGES.find((l) => l.code === selectedTransLang) const currentTransData = translations[selectedTransLang] || { title: '', description: '', tour_feature: [], travel_tips: {}, } return ( e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} className="max-w-5xl xl:max-w-6xl w-[95vw] p-0 overflow-hidden h-[90vh] min-h-[600px] max-h-[92vh] flex flex-col border border-border-soft bg-background rounded-2xl shadow-2xl" > {/* Header */}
افزودن تور زیارتی و گردشگری جدید ثبت مشخصات پایه، هزینه‌ها، ظرفیت، تصویر شاخص و ورودی‌های ترجمه 5 زبانه
{/* Step/Tab buttons */}
{/* دکمه علامت ضربدر بالای مودال */}
{/* Form Body */}
{errorMessage && (
{errorMessage}
)} {successMessage && (
{successMessage}
)} {/* TAB 1: Basic Info & Pricing */} {activeTab === 'basic' && (
setTitle(e.target.value)} placeholder="مثال: تور زیارتی عتبات عالیات نجف و کربلا (ویژه اربعین)..." className="bg-surface-2 border-border/50 text-sm font-medium" required />
{/* بخش آپلود تصویر شاخص تور (الزامی) */}
{imageId && ( تصویر بارگذاری شد (کد: #{imageId}) )}
{imageUrl ? (
پیش‌نمایش کاور تور

{imageName || 'تصویر تور'}

#{imageId}

تصویر با موفقیت در سرور ذخیره شد و به عنوان تصویر شاخص تور نمایش داده می‌شود.

) : (
!isUploadingImage && fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault() e.stopPropagation() }} onDrop={(e) => { e.preventDefault() e.stopPropagation() const file = e.dataTransfer.files?.[0] if (file) processImageUpload(file) }} className={`relative flex flex-col items-center justify-center rounded-2xl border-2 border-dashed p-6 text-center transition-all cursor-pointer ${ imageUploadError ? 'border-rose-500/40 bg-rose-500/5' : 'border-border-soft hover:border-primary/50 bg-surface-2/60 hover:bg-surface-2' }`} > {isUploadingImage ? (

در حال بارگذاری و بهینه‌سازی تصویر در سرور...

) : ( <>

برای انتخاب تصویر شاخص کلیک کنید یا فایل را اینجا رها کنید (Drag & Drop)

فرمت‌های پشتیبانی‌شده: JPG، PNG، WEBP — حداکثر حجم: 15 مگابایت

{imageUploadError && (

{imageUploadError}

)} )}
)}
setStartedAt(e.target.value)} className="bg-surface-2 border-border/50 text-sm font-mono" required />
setEndedAt(e.target.value)} className="bg-surface-2 border-border/50 text-sm font-mono" required />
setCapacity(e.target.value)} placeholder="40" className="bg-surface-2 border-border/50 text-sm font-mono" required />
{/* Pricing Sub-card */}

قیمت‌گذاری و تخفیفات (دلار)

setPrice(e.target.value)} placeholder="520" className="bg-surface-3 text-xs font-mono" required />
setPriceChild(e.target.value)} placeholder="280" className="bg-surface-3 text-xs font-mono" />
setPriceInfant(e.target.value)} placeholder="80" className="bg-surface-3 text-xs font-mono" />
setPercentOff(e.target.value)} placeholder="0" className="bg-surface-3 text-xs font-mono" />
{/* Description */}