You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

662 lines
27 KiB

import React, { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Ic } from '@/icons'
import {
ServiceTranslation,
SERVICE_LANGUAGES,
type ServiceLanguageCode,
} from '@/components/shared/ServiceTranslation'
import type { SimCardItem, CreateSimCardInput } from '../services/simcard-api'
export type SupportedLanguage = ServiceLanguageCode
interface SimcardFormDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
item?: SimCardItem | null
onSave: (data: CreateSimCardInput, id?: number) => Promise<void>
onDelete?: (id: number) => Promise<void>
}
const COUNTRIES = [
{ id: 1, code: 'IR', name: 'ایران', flag: '🇮🇷' },
{ id: 2, code: 'IQ', name: 'عراق', flag: '🇮🇶' },
{ id: 4, code: 'SA', name: 'عربستان سعودی', flag: '🇸🇦' },
]
export const LANGUAGES = SERVICE_LANGUAGES
const createEmptyTitles = (): Record<SupportedLanguage, string> => ({
ar: '',
fa: '',
en: '',
ru: '',
id: '',
})
const createEmptyDetails = (): Record<SupportedLanguage, string[]> => ({
ar: [],
fa: [],
en: [],
ru: [],
id: [],
})
export function SimcardFormDialog({
open,
onOpenChange,
item,
onSave,
onDelete,
}: SimcardFormDialogProps) {
const isEditing = Boolean(item && item.id)
const [activeLangTab, setActiveLangTab] = useState<SupportedLanguage>('ar')
const [countryId, setCountryId] = useState<number>(1)
const [price, setPrice] = useState<string>('10.00')
const [status, setStatus] = useState<number>(1)
// عناوین چندزبانه
const [titles, setTitles] = useState<Record<SupportedLanguage, string>>(createEmptyTitles)
// ویژگی‌های چندزبانه
const [details, setDetails] = useState<Record<SupportedLanguage, string[]>>(createEmptyDetails)
// افزودن ویژگی جدید در زبان فعال
const [newDetailText, setNewDetailText] = useState('')
// ویرایش درون‌خطی ویژگی در زبان فعال
const [editingDetailIndex, setEditingDetailIndex] = useState<number | null>(null)
const [editingDetailText, setEditingDetailText] = useState<string>('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
useEffect(() => {
if (!open) return
setActiveLangTab('ar')
if (item) {
const cId = item.country?.id || 1
setCountryId(cId)
setPrice(item.price ? String(item.price) : '10.00')
setStatus(item.status ?? 1)
// استخراج عناوین ۵ زبانه از دیتابیس
const parsedTitles = createEmptyTitles()
if (Array.isArray(item.raw_title)) {
for (const t of item.raw_title) {
if (t && typeof t === 'object' && t.language_code && t.title) {
const lang = t.language_code as SupportedLanguage
if (lang in parsedTitles) {
parsedTitles[lang] = t.title
}
}
}
} else if (item.raw_title && typeof item.raw_title === 'object') {
for (const lang of ['ar', 'fa', 'en', 'ru', 'id'] as SupportedLanguage[]) {
if (item.raw_title[lang]) parsedTitles[lang] = String(item.raw_title[lang])
}
}
if (!parsedTitles.ar && !parsedTitles.fa && item.title) {
parsedTitles.ar = item.title
parsedTitles.fa = item.title
}
setTitles(parsedTitles)
// استخراج ویژگی‌های ۵ زبانه از دیتابیس (آرایه‌های کاملاً مستقل جهت جلوگیری از Mutation)
const parsedDetails = createEmptyDetails()
if (Array.isArray(item.raw_detail)) {
for (const d of item.raw_detail) {
if (d && typeof d === 'object' && d.language_code && d.detail) {
const lang = d.language_code as SupportedLanguage
if (lang in parsedDetails) {
const text = String(d.detail).trim()
// جلوگیری از موارد تکراری ناشی از ثبت‌های قبلی
if (text && !parsedDetails[lang].includes(text)) {
parsedDetails[lang].push(text)
}
}
}
}
} else if (item.raw_detail && typeof item.raw_detail === 'object') {
for (const lang of ['ar', 'fa', 'en', 'ru', 'id'] as SupportedLanguage[]) {
if (Array.isArray(item.raw_detail[lang])) {
const uniqueList: string[] = []
for (const x of item.raw_detail[lang]) {
const text = String(x || '').trim()
if (text && !uniqueList.includes(text)) {
uniqueList.push(text)
}
}
parsedDetails[lang] = uniqueList
}
}
}
if (parsedDetails.ar.length === 0 && parsedDetails.fa.length === 0 && Array.isArray(item.simcard_detail)) {
const uniqueList: string[] = []
for (const x of item.simcard_detail) {
const text = String(x || '').trim()
if (text && !uniqueList.includes(text)) {
uniqueList.push(text)
}
}
parsedDetails.ar = [...uniqueList]
parsedDetails.fa = [...uniqueList]
}
setDetails(parsedDetails)
} else {
setCountryId(1)
setPrice('10.00')
setStatus(1)
setTitles(createEmptyTitles())
setDetails(createEmptyDetails())
}
setNewDetailText('')
setEditingDetailIndex(null)
setEditingDetailText('')
setErrorMsg(null)
setShowDeleteConfirm(false)
}, [item, open])
// کار با ویژگی‌های زبان فعال
const activeDetails = details[activeLangTab] || []
const handleTabChange = (langCode: SupportedLanguage) => {
setActiveLangTab(langCode)
}
const handleTitleChange = (val: string) => {
setTitles((prev) => ({
...prev,
[activeLangTab]: val,
}))
}
const handleCountryChange = (cId: number) => {
setCountryId(cId)
}
const handleAddDetail = () => {
if (!newDetailText.trim()) return
setDetails((prev) => ({
...prev,
[activeLangTab]: [...(prev[activeLangTab] || []), newDetailText.trim()],
}))
setNewDetailText('')
}
const handleRemoveDetail = (index: number) => {
setDetails((prev) => ({
...prev,
[activeLangTab]: (prev[activeLangTab] || []).filter((_, i) => i !== index),
}))
if (editingDetailIndex === index) {
setEditingDetailIndex(null)
setEditingDetailText('')
}
}
const handleStartEditDetail = (index: number, text: string) => {
setEditingDetailIndex(index)
setEditingDetailText(text)
}
const handleSaveEditDetail = (index: number) => {
if (!editingDetailText.trim()) return
setDetails((prev) => ({
...prev,
[activeLangTab]: (prev[activeLangTab] || []).map((d, i) =>
i === index ? editingDetailText.trim() : d
),
}))
setEditingDetailIndex(null)
setEditingDetailText('')
}
const handleCancelEditDetail = () => {
setEditingDetailIndex(null)
setEditingDetailText('')
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const mainTitle =
titles.ar.trim() ||
titles.fa.trim() ||
titles[activeLangTab].trim() ||
titles.en.trim() ||
titles.ru.trim() ||
titles.id.trim()
if (!mainTitle) {
setErrorMsg('لطفاً عنوان بسته سیم‌کارت را وارد نمایید.')
return
}
const numPrice = parseFloat(price)
if (isNaN(numPrice) || numPrice <= 0) {
setErrorMsg('لطفاً قیمت معتبری وارد نمایید.')
return
}
// عناوین ۵ زبانه
const finalTitles: Record<SupportedLanguage, string> = {
ar: titles.ar.trim() || mainTitle,
fa: titles.fa.trim() || mainTitle,
en: titles.en.trim() || mainTitle,
ru: titles.ru.trim() || mainTitle,
id: titles.id.trim() || mainTitle,
}
// ویژگی‌های ۵ زبانه
const finalDetails: Record<SupportedLanguage, string[]> = {
ar: (details.ar || []).filter((d) => d.trim()),
fa: (details.fa || []).filter((d) => d.trim()),
en: (details.en || []).filter((d) => d.trim()),
ru: (details.ru || []).filter((d) => d.trim()),
id: (details.id || []).filter((d) => d.trim()),
}
// فال‌بک ویژگی‌ها در صورت خالی بودن
const fallbackDetails =
finalDetails.ar.length > 0
? finalDetails.ar
: finalDetails.fa.length > 0
? finalDetails.fa
: []
for (const lang of ['ar', 'fa', 'en', 'ru', 'id'] as SupportedLanguage[]) {
if (finalDetails[lang].length === 0 && fallbackDetails.length > 0) {
finalDetails[lang] = [...fallbackDetails]
}
}
setErrorMsg(null)
setIsSubmitting(true)
try {
await onSave(
{
title: finalTitles.fa || finalTitles.ar,
titles: finalTitles,
country_id: countryId,
price: numPrice.toFixed(2),
status,
simcard_detail: finalDetails.fa.length > 0 ? finalDetails.fa : finalDetails.ar,
details: finalDetails,
},
item?.id
)
onOpenChange(false)
} catch (err: any) {
setErrorMsg(err?.message || 'خطایی در ثبت اطلاعات رخ داد.')
} finally {
setIsSubmitting(false)
}
}
const handleConfirmDelete = async () => {
if (!item?.id || !onDelete) return
setIsDeleting(true)
try {
await onDelete(item.id)
setShowDeleteConfirm(false)
onOpenChange(false)
} catch (err: any) {
setErrorMsg(err?.message || 'خطایی در حذف سیم‌کارت رخ داد.')
} finally {
setIsDeleting(false)
}
}
const currentLang = LANGUAGES.find((l) => l.code === activeLangTab) || LANGUAGES[0]
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl lg:max-w-3xl w-full rounded-2xl bg-card border border-border-soft p-0 max-h-[85vh] flex flex-col overflow-hidden shadow-2xl">
{/* هدر ثابت در بالای مدال */}
<DialogHeader className="p-6 pb-4 border-b border-border-soft shrink-0 text-start">
<DialogTitle className="text-lg font-bold text-foreground">
{isEditing ? 'ویرایش بسته سیم‌کارت' : 'افزودن سیم‌کارت جدید'}
</DialogTitle>
<DialogDescription className="text-xs text-grey-3">
{isEditing
? 'تغییرات شما در تمام زبان‌ها در دیتابیس ثبت شده و در اپلیکیشن زائرین بر اساس زبان انتخابی کاربر نمایش داده می‌شود.'
: 'سیم‌کارت جدید در دیتابیس ذخیره و در اپلیکیشن فعال خواهد شد.'}
</DialogDescription>
</DialogHeader>
{/* فرم: بدنه اسکرول‌شونده و فوتر فیکس‌شده */}
<form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-hidden min-h-0">
{/* بدنه محتوا (تنها بخشی که اسکرول می‌شود) */}
<div className="flex-1 overflow-y-auto p-6 space-y-5">
{errorMsg && (
<div className="p-3.5 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-500 text-xs flex items-center gap-2">
<Ic name="alert" className="size-4 shrink-0" />
<span>{errorMsg}</span>
</div>
)}
{/* ۱. انتخاب کشور مقصد */}
<div className="space-y-2">
<label className="text-xs font-bold text-foreground block">
کشور مقصد <span className="text-rose-500">*</span>
</label>
<div className="grid grid-cols-3 gap-3">
{COUNTRIES.map((c) => {
const isSelected = countryId === c.id
return (
<button
key={c.id}
type="button"
onClick={() => handleCountryChange(c.id)}
className={`h-12 px-4 rounded-xl border text-xs font-bold transition flex items-center justify-center gap-2.5 ${
isSelected
? 'border-primary bg-primary/10 text-primary shadow-xs'
: 'border-border-soft bg-surface-base/50 text-grey-3 hover:text-foreground hover:bg-card'
}`}
>
<span className="text-lg">{c.flag}</span>
<span className="text-xs">{c.name}</span>
</button>
)
})}
</div>
</div>
{/* ۲. تب‌های انتخاب ۵ زبان با کامپوننت مشترک ServiceTranslation */}
<ServiceTranslation
activeLang={activeLangTab}
onLangChange={(lang) => handleTabChange(lang)}
completedLangs={Object.fromEntries(
Object.entries(titles).map(([k, v]) => [k, Boolean(v?.trim())])
)}
primaryLang="ar"
title="زبان ترجمه و محتوا"
required
variant="grid"
/>
{/* ۳. عنوان بسته سیم‌کارت به زبان فعال */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-foreground flex items-center gap-1.5">
<span>عنوان بسته سیم‌کارت به زبان {currentLang.name}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono font-bold bg-primary/10 text-primary border border-primary/20">
{currentLang.badge}
</span>
<span className="text-rose-500">*</span>
</label>
<span className="text-[11px] text-grey-3 font-mono">
{currentLang.dir === 'rtl' ? 'RTL' : 'LTR'}
</span>
</div>
<Input
value={titles[activeLangTab] || ''}
onChange={(e) => handleTitleChange(e.target.value)}
dir={currentLang.dir}
placeholder={`عنوان بسته به زبان ${currentLang.name}...`}
className="h-11 text-xs bg-surface-base rounded-xl border-border-soft"
required={activeLangTab === 'fa'}
/>
</div>
{/* ۴. قیمت و وضعیت در اپلیکیشن */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-xs font-bold text-foreground block">
قیمت به دلار ($) <span className="text-rose-500">*</span>
</label>
<div className="relative">
<Input
type="number"
step="0.01"
min="0"
value={price}
onChange={(e) => setPrice(e.target.value)}
placeholder="10.00"
className="h-11 text-xs bg-surface-base rounded-xl border-border-soft font-mono ps-8"
required
/>
<span className="absolute start-3 top-1/2 -translate-y-1/2 text-grey-3 font-mono text-xs">
$
</span>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-foreground block">وضعیت در اپلیکیشن</label>
<select
value={status}
onChange={(e) => setStatus(parseInt(e.target.value, 10))}
className="w-full h-11 text-xs bg-surface-base rounded-xl border border-border-soft px-3 text-foreground font-medium focus:outline-none focus:ring-1 focus:ring-primary"
>
<option value={1}>فعال در اپلیکیشن</option>
<option value={2}>ناموجود (Out of Stock)</option>
<option value={3}>غیرفعال</option>
</select>
</div>
</div>
{/* ۵. ویژگی‌ها و جزئیات بسته به زبان فعال */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-foreground flex items-center gap-1.5">
<span>ویژگی‌ها و جزئیات بسته به زبان {currentLang.name}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono font-bold bg-primary/10 text-primary border border-primary/20">
{currentLang.badge}
</span>
</label>
<span className="text-[11px] text-grey-3">
{activeDetails.length} ویژگی ثبت‌شده برای {currentLang.name}
</span>
</div>
{/* ورودی افزودن ویژگی جدید */}
<div className="flex items-center gap-2">
<Input
value={newDetailText}
onChange={(e) => setNewDetailText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddDetail()
}
}}
dir={currentLang.dir}
placeholder={`افزودن ویژگی به زبان ${currentLang.name} (مثلاً حجم اینترنت، دقایق مکالمه، فعال‌سازی)...`}
className="h-10 text-xs bg-surface-base rounded-xl border-border-soft flex-1"
/>
<Button
type="button"
onClick={handleAddDetail}
disabled={!newDetailText.trim()}
className="h-10 px-4 rounded-xl text-xs font-bold bg-primary text-primary-foreground hover:bg-primary/90 gap-1.5 shrink-0"
>
<Ic name="plus" className="size-3.5" />
<span>افزودن</span>
</Button>
</div>
{/* لیست ویژگی‌های ثبت‌شده در زبان فعال */}
{activeDetails.length === 0 ? (
<div className="p-4 rounded-xl border border-dashed border-border-soft text-center text-grey-3 text-xs">
هنوز ویژگی خاصی به زبان {currentLang.name} اضافه نشده است. می‌توانید با فیلد بالا ویژگی جدید ثبت کنید.
</div>
) : (
<div className="space-y-2">
{activeDetails.map((itemText, idx) => (
<div
key={idx}
className="p-3 rounded-xl border border-border-soft bg-surface-base/60 text-xs transition"
>
{editingDetailIndex === idx ? (
<div className="flex items-center gap-2">
<Input
value={editingDetailText}
onChange={(e) => setEditingDetailText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveEditDetail(idx)
}
}}
dir={currentLang.dir}
className="h-9 text-xs bg-card rounded-lg flex-1"
autoFocus
/>
<Button
type="button"
size="sm"
onClick={() => handleSaveEditDetail(idx)}
className="h-9 px-3 text-xs bg-primary text-white rounded-lg gap-1 shrink-0"
>
<Ic name="check" className="size-3.5" />
<span>ذخیره</span>
</Button>
<Button
type="button"
size="sm"
variant="ghost"
onClick={handleCancelEditDetail}
className="h-9 px-2 text-xs text-grey-3 hover:text-foreground shrink-0"
title="انصراف"
>
<Ic name="close" className="size-3.5" />
</Button>
</div>
) : (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 truncate" dir={currentLang.dir}>
<span className="size-1.5 rounded-full bg-primary shrink-0" />
<span className="text-foreground font-medium leading-relaxed truncate">
{itemText}
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={() => handleStartEditDetail(idx, itemText)}
className="text-grey-3 hover:text-primary p-1.5 rounded-lg hover:bg-primary/10 transition"
title="ویرایش ویژگی"
>
<Ic name="edit" className="size-3.5" />
</button>
<button
type="button"
onClick={() => handleRemoveDetail(idx)}
className="text-grey-3 hover:text-rose-500 p-1.5 rounded-lg hover:bg-rose-500/10 transition"
title="حذف ویژگی"
>
<Ic name="trash" className="size-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* فوتر فیکس‌شده در پایین */}
<DialogFooter className="p-4 sm:px-6 bg-card border-t border-border-soft shrink-0 flex flex-row items-center justify-between w-full gap-3">
{/* دکمه حذف در سمت راست (Start در RTL) */}
<div>
{isEditing && onDelete && (
<Button
type="button"
variant="ghost"
disabled={isSubmitting || isDeleting}
onClick={() => setShowDeleteConfirm(true)}
className="rounded-xl text-xs h-10 px-3.5 text-rose-500 hover:text-rose-600 hover:bg-rose-500/10 gap-1.5"
title="حذف بسته سیم‌کارت"
>
<Ic name="trash" className="size-4" />
<span>حذف بسته</span>
</Button>
)}
</div>
{/* دکمه‌های انصراف و ذخیره در سمت چپ (End در RTL) */}
<div className="flex items-center gap-2">
<DialogClose asChild>
<Button
type="button"
variant="ghost"
disabled={isSubmitting || isDeleting}
className="rounded-xl text-xs h-10 px-4 text-grey-3 hover:text-foreground"
>
انصراف
</Button>
</DialogClose>
<Button
type="submit"
disabled={isSubmitting || isDeleting}
className="rounded-xl text-xs font-bold h-10 px-6 bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs"
>
{isSubmitting ? 'در حال ذخیره...' : 'ذخیره'}
</Button>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* دیالوگ اعتبارسنجی و تایید حذف برای جلوگیری از حذف تصادفی */}
{showDeleteConfirm && (
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent className="sm:max-w-md rounded-2xl bg-card border border-border-soft p-6 shadow-2xl">
<DialogHeader className="space-y-2 text-start">
<div className="size-11 rounded-2xl bg-rose-500/10 text-rose-500 flex items-center justify-center mb-1">
<Ic name="trash" className="size-5" />
</div>
<DialogTitle className="text-base font-bold text-foreground">
حذف بسته سیم‌کارت
</DialogTitle>
<DialogDescription className="text-xs text-grey-3 leading-relaxed">
آیا از حذف بسته سیم‌کارت «{item?.title}» از پایگاه‌داده اطمینان دارید؟ با حذف این مورد، دیگر در اپلیکیشن زائرین نمایش داده نخواهد شد و این عملیات غیرقابل بازگشت است.
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-5 flex flex-row items-center gap-2 sm:justify-start">
<Button
type="button"
disabled={isDeleting}
onClick={handleConfirmDelete}
className="rounded-xl text-xs font-bold h-9 px-4 bg-rose-500 hover:bg-rose-600 text-white flex-1"
>
{isDeleting ? 'در حال حذف...' : 'تایید و حذف'}
</Button>
<DialogClose asChild>
<Button
type="button"
variant="ghost"
disabled={isDeleting}
className="rounded-xl text-xs h-9 px-4 flex-1"
>
انصراف
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
)
}