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.
 
 
 

441 lines
18 KiB

import React, { useState, useEffect, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { Ic } from '@/icons'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { ShimmerBox, ShimmerLine } from '@/components/shared/shimmer_frontend'
import { toFa } from '@/lib/utils'
import { toast } from '@/stores/toast'
import type { ServiceMeta } from '../ServicesPage'
import {
fetchSimCards,
fetchSimCardOrdersCount,
createSimCard,
updateSimCard,
deleteSimCard,
type SimCardItem,
type CreateSimCardInput,
} from '../services/simcard-api'
import { SimcardFormDialog } from './SimcardFormDialog'
const COUNTRY_OPTIONS = [
{ code: 'ALL', labelFa: 'همه کشورها', labelEn: 'All Countries', flag: '🌍' },
{ code: 'IR', labelFa: 'ایران', labelEn: 'Iran', flag: '🇮🇷' },
{ code: 'IQ', labelFa: 'عراق', labelEn: 'Iraq', flag: '🇮🇶' },
{ code: 'SA', labelFa: 'عربستان سعودی', labelEn: 'Saudi Arabia', flag: '🇸🇦' },
] as const
const COUNTRY_FLAGS: Record<string, string> = {
IR: '🇮🇷',
IQ: '🇮🇶',
SA: '🇸🇦',
SY: '🇸🇾',
OM: '🇴🇲',
OMN: '🇴🇲',
KW: '🇰🇼',
KWT: '🇰🇼',
BH: '🇧🇭',
BHR: '🇧🇭',
}
const COUNTRY_NAMES_FA: Record<string, string> = {
IR: 'ایران',
IQ: 'عراق',
SA: 'عربستان سعودی',
SY: 'سوریه',
OM: 'عمان',
OMN: 'عمان',
KW: 'کویت',
KWT: 'کویت',
BH: 'بحرین',
BHR: 'بحرین',
}
export function SimcardServiceView({ service }: { service: ServiceMeta }) {
const [selectedCountry, setSelectedCountry] = useState<string>('ALL')
const [searchTerm, setSearchTerm] = useState('')
const [simcards, setSimcards] = useState<SimCardItem[]>([])
const [ordersCount, setOrdersCount] = useState<number>(0)
const [isLoading, setIsLoading] = useState<boolean>(true)
// فرم ایجاد و ویرایش
const [isFormOpen, setIsFormOpen] = useState(false)
const [editingItem, setEditingItem] = useState<SimCardItem | null>(null)
// بارگذاری داده‌ها از دیتابیس (یک‌بار در ابتدا برای دریافت همه بسته‌ها)
const loadData = async () => {
setIsLoading(true)
try {
const [cardsData, ordersTotal] = await Promise.all([
fetchSimCards(),
fetchSimCardOrdersCount(),
])
setSimcards(cardsData)
setOrdersCount(ordersTotal)
} catch (err) {
console.error('Error loading simcards:', err)
toast.error('خطا در دریافت لیست سیم‌کارت‌ها از سرور', 'خطا')
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadData()
}, [])
// ذخیره (ایجاد یا ویرایش)
const handleSave = async (data: CreateSimCardInput, id?: number) => {
if (id) {
await updateSimCard(id, data)
toast.success(`بسته سیم‌کارت «${data.title}» با موفقیت ویرایش شد.`, 'ویرایش موفق')
} else {
await createSimCard(data)
toast.success(
`بسته سیم‌کارت «${data.title}» با موفقیت ثبت شد و در اپلیکیشن قرار گرفت.`,
'ثبت موفق'
)
}
setIsFormOpen(false)
loadData()
}
// حذف سیم‌کارت از دیتابیس
const handleDelete = async (id: number) => {
try {
await deleteSimCard(id)
toast.success('بسته سیم‌کارت با موفقیت از دیتابیس حذف شد.', 'حذف موفق')
setIsFormOpen(false)
loadData()
} catch (err: any) {
toast.error(err?.message || 'خطایی در حذف سیم‌کارت رخ داد.', 'خطا')
throw err
}
}
// فیلتر آنی کلاینت: بر اساس کشور انتخابی و عبارت جستجو (بدون لودینگ مجدد)
const filtered = useMemo(() => {
return simcards.filter((item) => {
// فیلتر بر اساس کشور
if (selectedCountry !== 'ALL' && item.country?.code !== selectedCountry) {
return false
}
// فیلتر بر اساس عبارت جستجو
if (searchTerm.trim()) {
const term = searchTerm.toLowerCase().trim()
const matchTitle = item.title?.toLowerCase().includes(term)
const matchCountry =
item.country?.name?.toLowerCase().includes(term) ||
(COUNTRY_NAMES_FA[item.country?.code] || '').includes(term)
const matchDetails = item.simcard_detail?.some((d) =>
d.toLowerCase().includes(term)
)
if (!matchTitle && !matchCountry && !matchDetails) {
return false
}
}
return true
})
}, [simcards, selectedCountry, searchTerm])
// آمار محاسباتی زنده
const averagePrice = useMemo(() => {
if (!simcards.length) return '0'
const total = simcards.reduce((sum, item) => sum + parseFloat(item.price || '0'), 0)
return (total / simcards.length).toFixed(1)
}, [simcards])
return (
<div className="space-y-6">
{/* کارت‌های آمار واقعی منطبق با دیتابیس */}
<div className="grid grid-cols-2 gap-3.5 sm:grid-cols-4">
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 shadow-xs">
<div className="flex items-center justify-between">
<span className="text-xs text-grey-3 font-medium">بسته‌های فعال سیم‌کارت</span>
<span className="text-xs text-emerald-500 font-bold">در اپلیکیشن</span>
</div>
<div className="mt-2 text-2xl font-black text-foreground font-mono tabular-nums">
{isLoading ? <ShimmerBox className="h-8 w-16" rounded="lg" /> : toFa(simcards.length)}
</div>
</div>
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 shadow-xs">
<span className="text-xs text-grey-3 font-medium">کشورهای تحت پوشش</span>
<div className="mt-2 text-2xl font-black text-foreground font-mono tabular-nums">
{toFa(3)}{' '}
<span className="text-xs font-normal text-grey-3">
(🇮🇷 ایران، 🇮🇶 عراق، 🇸🇦 عربستان)
</span>
</div>
</div>
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 shadow-xs">
<span className="text-xs text-grey-3 font-medium">میانگین قیمت بسته‌ها</span>
<div className="mt-2 text-2xl font-black text-foreground font-mono tabular-nums">
{isLoading ? (
<ShimmerBox className="h-8 w-20" rounded="lg" />
) : (
`${toFa(averagePrice)}$`
)}
</div>
</div>
<div className="rounded-2xl border border-border-soft bg-card/40 p-4 shadow-xs flex flex-col justify-between">
<div>
<div className="flex items-center justify-between">
<span className="text-xs text-grey-3 font-medium">سفارش‌های ثبت‌شده</span>
<Link
to="/billing?service=simcard"
className="text-[11px] text-primary hover:underline font-bold inline-flex items-center gap-1"
>
<span>مشاهده</span>
<Ic name="arrow-left" className="size-3" />
</Link>
</div>
<div className="mt-2 text-2xl font-black text-primary font-mono tabular-nums">
{isLoading ? (
<ShimmerBox className="h-8 w-16" rounded="lg" />
) : (
toFa(ordersCount)
)}
</div>
</div>
</div>
</div>
{/* ابزارها، تب کشورها، جستجو و دکمه افزودن */}
<div className="flex flex-col gap-3 rounded-2xl border border-border-soft bg-card/40 p-4 sm:flex-row sm:items-center sm:justify-between shadow-xs">
<div className="flex items-center gap-2 flex-wrap">
{COUNTRY_OPTIONS.map((opt) => {
const isSelected = selectedCountry === opt.code
return (
<Button
key={opt.code}
size="sm"
variant={isSelected ? 'secondary' : 'ghost'}
onClick={() => setSelectedCountry(opt.code)}
className={`text-xs h-9 rounded-xl font-bold transition ${
isSelected
? 'bg-primary/10 text-primary border border-primary/20 shadow-xs'
: 'text-grey-3 hover:text-foreground'
}`}
>
<span className="me-1.5">{opt.flag}</span>
<span>{opt.labelFa}</span>
</Button>
)
})}
</div>
<div className="flex items-center gap-2 flex-wrap">
<div className="relative min-w-[200px]">
<Ic
name="search"
className="absolute start-3 top-1/2 -translate-y-1/2 size-3.5 text-grey-3"
/>
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="جستجوی بسته یا کشور..."
className="h-9 ps-9 text-xs bg-surface-base rounded-xl border-border-soft"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute end-2.5 top-1/2 -translate-y-1/2 text-grey-3 hover:text-foreground p-0.5"
>
<Ic name="close" className="size-3" />
</button>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={loadData}
title="بروزرسانی داده‌ها"
className="size-9 p-0 rounded-xl text-grey-3 hover:text-foreground"
>
<Ic name="refresh" className={`size-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
<Button
size="sm"
onClick={() => {
setEditingItem(null)
setIsFormOpen(true)
}}
className="h-9 px-3.5 rounded-xl text-xs font-bold bg-primary text-primary-foreground hover:bg-primary/90 gap-1.5 shadow-xs"
>
<Ic name="plus" className="size-3.5" />
<span>افزودن سیم‌کارت</span>
</Button>
</div>
</div>
{/* جدول بسته‌های سیم‌کارت مطابق با دیتابیس */}
<div className="overflow-hidden rounded-2xl border border-border-soft bg-card/40 shadow-xs">
<div className="overflow-x-auto">
<table className="w-full text-start text-xs">
<thead className="border-b border-border-soft bg-card/60 text-grey-3">
<tr>
<th className="p-3.5 text-start font-bold w-16">شناسه</th>
<th className="p-3.5 text-start font-bold w-40">کشور</th>
<th className="p-3.5 text-start font-bold">عنوان بسته اینترنت و سیم‌کارت</th>
<th className="p-3.5 text-start font-bold w-28">قیمت رسمی</th>
<th className="p-3.5 text-start font-bold w-32">وضعیت</th>
<th className="p-3.5 text-center font-bold w-20">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-border-soft">
{isLoading ? (
Array.from({ length: 5 }).map((_, idx) => (
<tr key={idx}>
<td className="p-3.5">
<ShimmerBox className="h-4 w-8" rounded="sm" />
</td>
<td className="p-3.5">
<div className="flex items-center gap-2">
<ShimmerBox className="size-5" rounded="full" />
<ShimmerBox className="h-4 w-20" rounded="sm" />
</div>
</td>
<td className="p-3.5">
<ShimmerLine width="75%" height={14} rounded="sm" />
</td>
<td className="p-3.5">
<ShimmerBox className="h-4 w-14" rounded="sm" />
</td>
<td className="p-3.5">
<ShimmerBox className="h-5 w-20" rounded="full" />
</td>
<td className="p-3.5 text-center">
<div className="flex items-center justify-center">
<ShimmerBox className="size-7" rounded="lg" />
</div>
</td>
</tr>
))
) : filtered.length === 0 ? (
<tr>
<td colSpan={6} className="p-12 text-center text-grey-3">
<div className="flex flex-col items-center justify-center gap-2">
<Ic name="simcard" className="size-8 text-grey-3/50" />
<span className="font-bold text-sm">هیچ بسته‌ای یافت نشد</span>
<span className="text-xs text-grey-3">
{searchTerm
? 'عبارت جستجو شده با هیچ‌یک از بسته‌های سیم‌کارت همخوانی ندارد.'
: 'برای کشور انتخاب شده سیم‌کارتی در دیتابیس ثبت نشده است.'}
</span>
</div>
</td>
</tr>
) : (
filtered.map((item) => {
const countryCode = item.country?.code || ''
const flag =
COUNTRY_FLAGS[countryCode] || item.country?.icon || '🌐'
const countryNameFa =
COUNTRY_NAMES_FA[countryCode] || item.country?.name || countryCode
return (
<tr
key={item.id}
onClick={() => {
setEditingItem(item)
setIsFormOpen(true)
}}
className="hover:bg-card/70 transition cursor-pointer group"
>
<td className="p-3.5 font-mono font-bold text-grey-3">
#{toFa(item.id)}
</td>
<td className="p-3.5">
<div className="flex items-center gap-2">
<span className="text-base">{flag}</span>
<span className="font-bold text-foreground">{countryNameFa}</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono">
{countryCode}
</Badge>
</div>
</td>
<td className="p-3.5">
<div className="flex flex-col gap-1.5">
<span className="font-bold text-foreground group-hover:text-primary transition">
{item.title}
</span>
<div className="flex items-center gap-1.5 flex-wrap">
{['fa', 'ar', 'en', 'ru', 'id'].map((lc) => (
<span
key={lc}
className="text-[9px] font-mono px-1.5 py-0.5 rounded-md font-bold uppercase bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"
title={`پشتیبانی و ترجمه فعال: ${lc.toUpperCase()}`}
>
{lc}
</span>
))}
{item.simcard_detail && item.simcard_detail.length > 0 && (
<span className="text-[11px] text-grey-3 ms-1">
• {toFa(item.simcard_detail.length)} ویژگی ثبت‌شده
</span>
)}
</div>
</div>
</td>
<td className="p-3.5 font-mono font-bold text-foreground tabular-nums">
{toFa(item.price)}$
</td>
<td className="p-3.5">
<Badge
className={`text-[10px] rounded-full border ${
item.status === 2
? 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/20'
: item.status === 3
? 'bg-zinc-500/15 text-zinc-500 border-zinc-500/20'
: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/20'
}`}
>
{item.status === 2
? 'ناموجود'
: item.status === 3
? 'غیرفعال'
: 'فعال در اپلیکیشن'}
</Badge>
</td>
<td className="p-3.5 text-center" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-center">
<Button
size="sm"
variant="ghost"
onClick={() => {
setEditingItem(item)
setIsFormOpen(true)
}}
title="ویرایش بسته"
className="size-7 p-0 text-primary hover:text-primary hover:bg-primary/10 rounded-lg"
>
<Ic name="edit" className="size-3.5" />
</Button>
</div>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
</div>
{/* فرم ایجاد و ویرایش سیم‌کارت */}
<SimcardFormDialog
open={isFormOpen}
onOpenChange={setIsFormOpen}
item={editingItem}
onSave={handleSave}
onDelete={handleDelete}
/>
</div>
)
}