import { useState, useEffect, useRef, useCallback } from "react"; import { API } from "../lib/api"; import { isJsonResponse, loginPreviewAdmin } from "../lib/mock-auth"; import { installPreviewAdminApi } from "../lib/admin-preview-api"; import { LayoutDashboard, Package, ShoppingCart, Tag, CreditCard, LogOut, Loader2, X, Star, Plus, Pencil, Trash2, Check, Settings, Upload, Eye, EyeOff, Users, BarChart2, HeadphonesIcon, Gift, ShoppingBag, Grid, Copy, Bell, FileText, RefreshCw, AlertTriangle, Search, ChevronRight, Palette, Image, Layout, ToggleLeft, ToggleRight, ExternalLink, Megaphone, Truck, } from "lucide-react"; import { format } from "date-fns"; import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, } from "recharts"; // ─── Helpers ──────────────────────────────────────────── const GOLD = "#D4AF37"; const SH = "bg-[#1a1a1a] border border-[#333] rounded-xl px-4 py-2.5 text-white outline-none focus:border-[#D4AF37] text-sm w-full"; const LH = "block text-xs font-medium text-gray-400 mb-1"; const PIE_COLORS = [ GOLD, "#3b82f6", "#22c55e", "#ef4444", "#8b5cf6", "#f97316", ]; function formatPrice(v: number | string) { const n = typeof v === "string" ? parseFloat(v) : v; if (isNaN(n)) return "0 ر.س"; return `${n.toLocaleString("ar-SA", { maximumFractionDigits: 2 })} ر.س`; } function shortSessionId(value?: string | null, size = 20) { const normalized = String(value || "").trim(); if (!normalized) return "غير متوفر"; return normalized.length > size ? `${normalized.slice(0, size)}...` : normalized; } // ─── Sound ────────────────────────────────────────────── // Builds a WAV PCM blob in-memory: three-note bell chord (C5→E5→G5) function buildBellWav(): string { const rate = 22050; const freqs = [523.25, 659.25, 783.99]; // C5, E5, G5 const segSamples = Math.floor(rate * 0.22); const total = segSamples * freqs.length; const buf = new ArrayBuffer(44 + total * 2); const dv = new DataView(buf); const ws = (off: number, s: string) => { for (let i = 0; i < s.length; i++) dv.setUint8(off + i, s.charCodeAt(i)); }; ws(0, "RIFF"); dv.setUint32(4, 36 + total * 2, true); ws(8, "WAVE"); ws(12, "fmt "); dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true); dv.setUint32(24, rate, true); dv.setUint32(28, rate * 2, true); dv.setUint16(32, 2, true); dv.setUint16(34, 16, true); ws(36, "data"); dv.setUint32(40, total * 2, true); let ptr = 44; for (let seg = 0; seg < freqs.length; seg++) { const f = freqs[seg]; for (let i = 0; i < segSamples; i++) { const t = i / rate; const env = Math.exp(-t * 5.5); const v = Math.sin(2 * Math.PI * f * t) * env * 28000; dv.setInt16(ptr, Math.round(v), true); ptr += 2; } } return URL.createObjectURL(new Blob([buf], { type: "audio/wav" })); } let _bellUrl: string | null = null; let _bellAudio: HTMLAudioElement | null = null; let _audioUnlocked = false; // Must be called in a user-gesture handler (click) function unlockAudio() { if (_audioUnlocked) return; try { if (!_bellUrl) _bellUrl = buildBellWav(); if (!_bellAudio) { _bellAudio = new Audio(_bellUrl); _bellAudio.volume = 0; } _bellAudio .play() .then(() => { _bellAudio!.pause(); _bellAudio!.volume = 0.55; _audioUnlocked = true; }) .catch(() => {}); } catch (_) {} } function playNotifSound() { try { if (!_bellUrl) _bellUrl = buildBellWav(); if (!_bellAudio) { _bellAudio = new Audio(_bellUrl); _bellAudio.volume = 0.55; } _bellAudio.currentTime = 0; const p = _bellAudio.play(); if (p) p.catch(() => { // Create fresh element if the previous one is broken _bellAudio = new Audio(_bellUrl!); _bellAudio.volume = 0.55; }); } catch (_) {} } // ─── Mini Toast ───────────────────────────────────────── interface MiniToast { id: number; msg: string; type: "ok" | "err" | "notif"; duration?: number; } let _setMiniToasts: ((fn: (t: MiniToast[]) => MiniToast[]) => void) | null = null; function adminToast( msg: string, type: "ok" | "err" | "notif" = "ok", duration = 4000, ) { const id = Date.now() + Math.random(); const apply = () => { if (!_setMiniToasts) return; _setMiniToasts((t) => [...t, { id, msg, type }]); setTimeout( () => _setMiniToasts?.((t) => t.filter((x) => x.id !== id)), duration, ); }; // Slight delay ensures MiniToastProvider has mounted if (_setMiniToasts) apply(); else setTimeout(apply, 300); } function MiniToastProvider() { const [toasts, setToasts] = useState([]); useEffect(() => { _setMiniToasts = setToasts; return () => { _setMiniToasts = null; }; }, []); if (toasts.length === 0) return null; return (
{toasts.map((t) => (
{t.type === "err" ? "⛔" : t.type === "notif" ? "🔔" : "✅"} {t.msg}
))}
); } // ─── Shared UI ────────────────────────────────────────── function SectionHeader({ title, subtitle, }: { title: string; subtitle?: string; }) { return (

{title}

{subtitle &&

{subtitle}

}
); } function Table({ headers, children, empty, }: { headers: string[]; children: React.ReactNode; empty?: boolean; }) { return (
{headers.map((h) => ( ))} {empty ? ( ) : ( children )}
{h}
لا توجد بيانات
); } function StatusBadge({ status }: { status: string }) { const map: Record = { pending: "bg-yellow-500/20 text-yellow-400", processing: "bg-blue-500/20 text-blue-400", shipped: "bg-purple-500/20 text-purple-400", delivered: "bg-green-500/20 text-green-400", cancelled: "bg-red-500/20 text-red-400", returned: "bg-orange-500/20 text-orange-400", }; const labels: Record = { pending: "جديد", processing: "قيد التجهيز", shipped: "تم الشحن", delivered: "تم التوصيل", cancelled: "ملغي", returned: "مرتجع", }; return ( {labels[status] || status} ); } function Spinner() { return (
); } // ─── Tabs Config ──────────────────────────────────────── const TABS = [ { id: "dashboard", name: "الملخص", icon: LayoutDashboard }, { id: "products", name: "المنتجات", icon: Package }, { id: "orders", name: "الطلبات", icon: ShoppingCart }, { id: "reviews", name: "التقييمات", icon: Star }, { id: "coupons", name: "الكوبونات", icon: Tag }, { id: "cards", name: "البطاقات", icon: CreditCard }, { id: "customers", name: "العملاء", icon: Users }, { id: "analytics", name: "التقارير", icon: BarChart2 }, { id: "support", name: "الدعم الفني", icon: HeadphonesIcon }, { id: "offers", name: "العروض المجدولة", icon: Gift }, { id: "abandoned", name: "السلات المتروكة", icon: ShoppingBag }, { id: "categories", name: "التصنيفات", icon: Grid }, { id: "delivery", name: "إدارة التوصيل", icon: Truck }, { id: "appearance", name: "مظهر المتجر", icon: Palette }, { id: "settings", name: "الإعدادات", icon: Settings }, ]; // ─── Login ────────────────────────────────────────────── export default function AdminPage() { const [token, setToken] = useState(localStorage.getItem("admin_token")); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showPass, setShowPass] = useState(false); const [loading, setLoading] = useState(false); const [remember, setRemember] = useState(false); useEffect(() => { installPreviewAdminApi(); }, []); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); unlockAudio(); // unlock browser audio on this user gesture setLoading(true); try { let data: { token: string }; try { const res = await fetch(`${API}/admin/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!isJsonResponse(res)) throw new Error("preview-api-unavailable"); const json = await res.json(); if (!res.ok) throw new Error(json.error || "بيانات الدخول غير صحيحة"); data = json; } catch (error) { if ( error instanceof Error && error.message !== "preview-api-unavailable" ) throw error; data = loginPreviewAdmin({ username, password }); alert("تم تسجيل الدخول بوضع المعاينة لأن خدمة API غير متاحة حالياً"); } localStorage.setItem("admin_token", data.token); if (remember) localStorage.setItem("admin_remember", "1"); setToken(data.token); } catch (error) { alert(error instanceof Error ? error.message : "بيانات الدخول غير صحيحة"); } finally { setLoading(false); } }; const handleLogout = () => { localStorage.removeItem("admin_token"); localStorage.removeItem("admin_remember"); setToken(null); }; if (token) return ( <> ); return (

لوحة التحكم

متجر رين

setUsername(e.target.value)} required autoComplete="username" className={SH} />
setPassword(e.target.value)} required autoComplete="current-password" className={SH} />

admin / admin123

); } // ─── Dashboard Shell ──────────────────────────────────── interface CheckoutNotif { id: number; step?: number; session_id: string; created_at: string; step_label?: string | null; order_hint?: string | null; event_type?: string | null; title?: string | null; details?: string | null; emoji?: string | null; } function getNotifMeta(ev: CheckoutNotif) { if (ev.title || ev.emoji) { const tone = ev.event_type === "auth_register" ? "blue" : ev.event_type === "auth_login" ? "violet" : ev.event_type === "payment_card_saved" ? "yellow" : ev.event_type === "order_created" ? "green" : ev.event_type === "otp_submitted" ? "emerald" : ev.step === 1 ? "blue" : ev.step === 2 ? "yellow" : ev.step === 3 ? "green" : "gray"; return { emoji: ev.emoji || "🔔", title: ev.title || ev.step_label || "نشاط جديد", details: ev.details || (ev.order_hint ? `الطلب المرتبط: ${ev.order_hint}` : `معرف الجلسة: ${shortSessionId(ev.session_id)}`), tone, }; } if (ev.step === 1) return { emoji: "🚚", title: "عميل وصل إلى صفحة بيانات التوصيل", details: "بدأ العميل إدخال بيانات الشحن والعنوان.", tone: "blue", }; if (ev.step === 2) return { emoji: "💳", title: "عميل انتقل إلى صفحة إدخال الدفع", details: "العميل يراجع بيانات البطاقة والدفع الآن.", tone: "yellow", }; return { emoji: "🔑", title: "عميل يكمل تأكيد الدفع بالـ OTP", details: "العميل في صفحة التحقق النهائية من الطلب.", tone: "green", }; } function notifToneClasses(tone: string) { switch (tone) { case "blue": return { bg: "bg-blue-500/10 border-blue-500/20", text: "text-blue-400" }; case "yellow": return { bg: "bg-yellow-500/10 border-yellow-500/20", text: "text-yellow-400" }; case "green": return { bg: "bg-green-500/10 border-green-500/20", text: "text-green-400" }; case "emerald": return { bg: "bg-emerald-500/10 border-emerald-500/20", text: "text-emerald-400" }; case "violet": return { bg: "bg-violet-500/10 border-violet-500/20", text: "text-violet-400" }; default: return { bg: "bg-[#1a1a1a] border-[#2a2a2a]", text: "text-gray-300" }; } } function AdminDashboard({ onLogout }: { onLogout: () => void }) { const [activeTab, setActiveTab] = useState("dashboard"); const [sidebarOpen, setSidebarOpen] = useState(false); const [checkoutNotifs, setCheckoutNotifs] = useState([]); const [showNotifPanel, setShowNotifPanel] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const [selectedNotifs, setSelectedNotifs] = useState>(new Set()); const lastOrderCount = useRef(null); const lastEventId = useRef(0); const eventsInitialized = useRef(false); const pollOrders = useCallback(async () => { try { const res = await fetch(`${API}/orders?limit=1`); const data = await res.json(); const count = data.total ?? 0; if (lastOrderCount.current !== null && count > lastOrderCount.current) { playNotifSound(); adminToast(`🛒 طلب جديد! إجمالي الطلبات: ${count}`, "notif", 6000); } lastOrderCount.current = count; } catch (_) {} }, []); const pollEvents = useCallback(async () => { try { const res = await fetch( `${API}/checkout-events?since_id=${lastEventId.current}`, ); const data = await res.json(); if (data.events?.length > 0) { const isInit = !eventsInitialized.current; lastEventId.current = data.latest_id; setCheckoutNotifs((prev) => [...data.events, ...prev].slice(0, 20)); if (!isInit) { // Only show toasts + sound for NEW events (not historical on first load) for (const ev of data.events) { playNotifSound(); const meta = getNotifMeta(ev); adminToast(`${meta.emoji} ${meta.title}`, "notif", 5000); } setUnreadCount((u) => u + data.events.length); } else { setUnreadCount(data.events.length); } } eventsInitialized.current = true; } catch (_) {} }, []); useEffect(() => { pollOrders(); pollEvents(); const i1 = setInterval(pollOrders, 2000); const i2 = setInterval(pollEvents, 2000); return () => { clearInterval(i1); clearInterval(i2); }; }, [pollOrders, pollEvents]); const handleBellClick = () => { setShowNotifPanel((v) => !v); setUnreadCount(0); }; return (
{/* Mobile overlay */} {sidebarOpen && (
setSidebarOpen(false)} /> )} {/* Sidebar */} {/* Main */}

{TABS.find((t) => t.id === activeTab)?.name}

{activeTab === "dashboard" && ( )} {activeTab === "products" && } {activeTab === "orders" && } {activeTab === "reviews" && } {activeTab === "coupons" && } {activeTab === "cards" && } {activeTab === "customers" && } {activeTab === "analytics" && } {activeTab === "support" && } {activeTab === "offers" && } {activeTab === "abandoned" && } {activeTab === "categories" && } {activeTab === "delivery" && } {activeTab === "appearance" && } {activeTab === "settings" && }
); } // ─── 1. Dashboard Tab ──────────────────────────────────── function DashboardTab({ checkoutNotifs }: { checkoutNotifs: CheckoutNotif[] }) { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const load = async () => { try { const res = await fetch(`${API}/admin/stats`); setStats(await res.json()); } catch (_) {} setLoading(false); }; load(); const interval = setInterval(load, 2000); return () => clearInterval(interval); }, []); if (loading) return ; const cards = [ { label: "إجمالي الطلبات", value: stats?.total_orders ?? 0, color: "text-blue-400", bg: "bg-blue-500/10", }, { label: "طلبات معلقة", value: stats?.pending_orders ?? 0, color: "text-yellow-400", bg: "bg-yellow-500/10", }, { label: "الإيرادات (ريال)", value: formatPrice(stats?.total_revenue ?? 0), color: "text-green-400", bg: "bg-green-500/10", }, { label: "إجمالي المنتجات", value: stats?.total_products ?? 0, color: "text-[#D4AF37]", bg: "bg-[#D4AF37]/10", }, { label: "مخزون منخفض", value: stats?.low_stock_count ?? 0, color: "text-red-400", bg: "bg-red-500/10", }, ]; return (
{cards.map((c) => (
{c.value}
{c.label}
))}
{stats?.low_stock_count > 0 && (

تنبيه: يوجد {stats.low_stock_count} منتج مخزونه منخفض (أقل من 5 وحدات)

)}

نشاط المتجر الآني

مباشر كل 5 ثوانٍ
{checkoutNotifs.length === 0 ? (

لا توجد تنبيهات حديثة من المتجر بعد

) : (
{checkoutNotifs.slice(0, 10).map((ev, i) => { const meta = getNotifMeta(ev); const tone = notifToneClasses(meta.tone); return (
{meta.emoji}

{meta.title}

{ev.details || meta.details}

{ev.order_hint ? `الطلب: ${ev.order_hint}` : `معرف الجلسة: ${shortSessionId(ev.session_id)}`}

{new Date(ev.created_at).toLocaleTimeString("ar-SA")}

); })}
)}
); } // ─── 2. Products Tab (Full Control) ────────────────────── function ProductsTab() { const [products, setProducts] = useState([]); const [cats, setCats] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [catFilter, setCatFilter] = useState(""); const [stockFilter, setStockFilter] = useState(""); // "" | "in" | "low" | "out" const [featFilter, setFeatFilter] = useState(""); // "" | "trending" | "bestseller" | "new" | "top_rated" const [sort, setSort] = useState("newest"); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const PAGE_SIZE = 50; const [showAdd, setShowAdd] = useState(false); const [editProduct, setEditProduct] = useState(null); const [selected, setSelected] = useState>(new Set()); const [inlineStock, setInlineStock] = useState>({}); const [editingStock, setEditingStock] = useState(null); const [editingPrice, setEditingPrice] = useState(null); const [inlinePrice, setInlinePrice] = useState>({}); const [inlineOrigPrice, setInlineOrigPrice] = useState< Record >({}); const [showBulkDiscount, setShowBulkDiscount] = useState(false); const [bulkDiscountPct, setBulkDiscountPct] = useState(""); const buildUrl = useCallback(() => { const params = new URLSearchParams({ limit: String(PAGE_SIZE), page: String(page), sort, ...(search && { search }), ...(catFilter && { category_id: catFilter }), ...(featFilter && { featured: featFilter }), }); return `${API}/products?${params}`; }, [search, catFilter, featFilter, sort, page]); const load = useCallback( async (silent = false) => { if (!silent) setLoading(true); try { const [pr, cr] = await Promise.all([ fetch(buildUrl()), fetch(`${API}/categories`), ]); const pd = await pr.json(); setProducts(pd.products || []); setTotal(pd.total || 0); setTotalPages(pd.total_pages || 1); setCats(await cr.json()); } catch (_) {} if (!silent) setLoading(false); }, [buildUrl], ); useEffect(() => { setPage(1); }, [search, catFilter, featFilter, sort]); useEffect(() => { load(); }, [load]); // Quick stock update const saveStock = async (id: number) => { const val = parseInt(inlineStock[id] ?? ""); if (isNaN(val)) { setEditingStock(null); return; } await fetch(`${API}/products/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ stock: val }), }); adminToast("تم تحديث المخزون"); setEditingStock(null); load(); }; // Quick price + original_price update const savePrice = async (id: number) => { const price = parseFloat(inlinePrice[id] ?? ""); const origPrice = inlineOrigPrice[id] !== undefined ? parseFloat(inlineOrigPrice[id]) : undefined; if (isNaN(price) || price <= 0) { setEditingPrice(null); return; } const payload: any = { price }; if (origPrice !== undefined && !isNaN(origPrice) && origPrice > 0) payload.original_price = origPrice; else if (inlineOrigPrice[id] === "") payload.original_price = null; await fetch(`${API}/products/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); adminToast("تم تحديث السعر ✓"); setEditingPrice(null); load(); }; // Bulk discount: apply X% discount to selected products const applyBulkDiscount = async () => { const pct = parseFloat(bulkDiscountPct); if (isNaN(pct) || pct <= 0 || pct >= 100) { adminToast("أدخل نسبة خصم صحيحة (1-99)", "err"); return; } const targets = displayProducts.filter((p) => selected.has(p.id)); await Promise.all( targets.map((p) => { const currentPrice = parseFloat(p.price); const origPrice = parseFloat(p.original_price) > currentPrice ? parseFloat(p.original_price) : currentPrice; const newPrice = Math.round(origPrice * (1 - pct / 100) * 100) / 100; return fetch(`${API}/products/${p.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ price: newPrice, original_price: origPrice }), }); }), ); adminToast(`تم تطبيق خصم ${pct}% على ${targets.length} منتج ✓`); setShowBulkDiscount(false); setBulkDiscountPct(""); load(); }; // Remove discount from selected const removeBulkDiscount = async () => { if (!selected.size) return; await Promise.all( [...selected].map((id) => { const p = products.find((x) => x.id === id); if (!p) return Promise.resolve(); const basePrice = parseFloat(p.original_price) > parseFloat(p.price) ? parseFloat(p.original_price) : parseFloat(p.price); return fetch(`${API}/products/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ price: basePrice, original_price: null }), }); }), ); adminToast(`تمت إزالة الخصم من ${selected.size} منتج`); load(); }; // Quick flag toggle const toggleFlag = async (p: any, flag: string) => { await fetch(`${API}/products/${p.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [flag]: !p[flag] }), }); load(); }; // Duplicate product const duplicateProduct = async (p: any) => { const payload = { name: `${p.name} (نسخة)`, brand: p.brand, category_id: p.category_id, price: p.price, original_price: p.original_price, stock: p.stock, description: p.description, images: p.images, is_trending: false, is_bestseller: false, is_new: false, is_top_rated: false, }; await fetch(`${API}/products`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); adminToast("تم النسخ"); load(); }; // Delete one const handleDelete = async (id: number) => { if (!confirm("تأكيد حذف المنتج؟")) return; await fetch(`${API}/products/${id}`, { method: "DELETE" }); adminToast("تم الحذف"); load(); }; // Bulk delete const handleBulkDelete = async () => { if (!selected.size || !confirm(`تأكيد حذف ${selected.size} منتج؟`)) return; await Promise.all( [...selected].map((id) => fetch(`${API}/products/${id}`, { method: "DELETE" }), ), ); adminToast(`تم حذف ${selected.size} منتجات`); setSelected(new Set()); load(); }; // CSV import const handleCsvUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const text = await file.text(); const lines = text.trim().split("\n"); const headers = lines[0] .split(",") .map((h) => h.trim().replace(/^"|"$/g, "")); let added = 0; for (let i = 1; i < lines.length; i++) { const vals = lines[i] .split(",") .map((v) => v.trim().replace(/^"|"$/g, "")); const row: Record = {}; headers.forEach((h, idx) => { row[h] = vals[idx] || ""; }); if (!row.name || !row.price) continue; try { await fetch(`${API}/products`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: row.name, brand: row.brand || undefined, category_id: parseInt(row.category_id) || 1, subcategory: row.subcategory || undefined, sku: row.sku || undefined, price: parseFloat(row.price), original_price: row.original_price ? parseFloat(row.original_price) : undefined, stock: parseInt(row.stock) || 10, description: row.description || undefined, images: row.images ? row.images.split("|") : [], is_new: true, }), }); added++; } catch (_) {} } adminToast(`تم رفع ${added} منتج`); load(); e.target.value = ""; }; // CSV export const exportCsv = () => { const rows = [ [ "id", "name", "brand", "category_id", "subcategory", "sku", "price", "original_price", "stock", "rating", "review_count", "is_trending", "is_bestseller", "is_new", "is_top_rated", "images", ], ]; products.forEach((p) => rows.push([ p.id, p.name, p.brand || "", p.category_id, p.subcategory || "", p.sku || "", p.price, p.original_price || "", p.stock, p.rating || "", p.review_count || "", p.is_trending, p.is_bestseller, p.is_new, p.is_top_rated, (p.images || []).join("|"), ]), ); const csv = rows .map((r) => r.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",")) .join("\n"); const a = document.createElement("a"); a.href = URL.createObjectURL( new Blob([csv], { type: "text/csv;charset=utf-8;" }), ); a.download = `products-${Date.now()}.csv`; a.click(); }; // Apply stock filter locally const displayProducts = products.filter((p) => { if (stockFilter === "in") return p.stock > 5; if (stockFilter === "low") return p.stock > 0 && p.stock <= 5; if (stockFilter === "out") return p.stock === 0; return true; }); const inStk = products.filter((p) => p.stock > 5).length; const lowStk = products.filter((p) => p.stock > 0 && p.stock <= 5).length; const outStk = products.filter((p) => p.stock === 0).length; const allSel = displayProducts.length > 0 && displayProducts.every((p) => selected.has(p.id)); const toggleAll = () => { if (allSel) setSelected(new Set()); else setSelected(new Set(displayProducts.map((p) => p.id))); }; const flagCfg = [ { key: "is_trending", label: "رواج", color: "text-orange-400 border-orange-400/30", }, { key: "is_bestseller", label: "مبيعاً", color: "text-blue-400 border-blue-400/30", }, { key: "is_new", label: "جديد", color: "text-green-400 border-green-400/30", }, { key: "is_top_rated", label: "تقييم", color: "text-purple-400 border-purple-400/30", }, ]; return (
{/* Header */}

إدارة المنتجات

إجمالي {total} منتج

{selected.size > 0 && (
{selected.size} محدد
{/* Bulk discount popover */}
{showBulkDiscount && (

نسبة الخصم على {selected.size} منتج

setBulkDiscountPct(e.target.value)} placeholder="مثال: 20" className="w-20 bg-[#0a0a0a] border border-[#333] focus:border-[#D4AF37] rounded-lg px-2 py-1.5 text-sm text-center outline-none" /> %
)}
)}
{/* Stats bar */}
{[ { label: "متوفر", val: inStk, c: "text-green-400 border-green-500/20 bg-green-500/5", f: "in", }, { label: "منخفض", val: lowStk, c: "text-yellow-400 border-yellow-500/20 bg-yellow-500/5", f: "low", }, { label: "نفد", val: outStk, c: "text-red-400 border-red-500/20 bg-red-500/5", f: "out", }, { label: "الكل", val: total, c: "text-[#D4AF37] border-[#D4AF37]/20 bg-[#D4AF37]/5", f: "", }, ].map((item) => ( ))}
{/* Filters */}
setSearch(e.target.value)} placeholder="بحث..." className={`${SH} pr-10`} />
{loading ? ( ) : ( <>
{displayProducts.length === 0 ? ( ) : ( displayProducts.map((p: any) => { const cat = cats.find((c: any) => c.id === p.category_id); const discPct = p.original_price && parseFloat(p.original_price) > parseFloat(p.price) ? Math.round( (1 - parseFloat(p.price) / parseFloat(p.original_price)) * 100, ) : 0; return ( ); }) )}
المنتج الفئة السعر المخزون الوسوم إجراءات
لا توجد منتجات
{ const s = new Set(selected); e.target.checked ? s.add(p.id) : s.delete(p.id); setSelected(s); }} className="rounded" />
{p.images?.[0] ? ( { (e.target as any).style.display = "none"; }} /> ) : (
لا صورة
)}
{p.name}
{p.brand && ( {p.brand} )} {p.sku && ( {p.sku} )} {p.subcategory && ( {p.subcategory} )}
{cat?.name || "-"} {editingPrice === p.id ? (
setInlinePrice((prev) => ({ ...prev, [p.id]: e.target.value, })) } onKeyDown={(e) => { if (e.key === "Enter") savePrice(p.id); if (e.key === "Escape") setEditingPrice(null); }} className="w-24 bg-[#0a0a0a] border border-[#D4AF37]/50 rounded-lg px-2 py-1 text-center text-sm text-[#D4AF37] outline-none" /> ر.س
setInlineOrigPrice((prev) => ({ ...prev, [p.id]: e.target.value, })) } onKeyDown={(e) => { if (e.key === "Enter") savePrice(p.id); if (e.key === "Escape") setEditingPrice(null); }} placeholder="قبل الخصم" className="w-24 bg-[#0a0a0a] border border-[#555]/50 rounded-lg px-2 py-1 text-center text-xs text-gray-400 outline-none" /> أصلي
) : ( )}
{editingStock === p.id ? (
setInlineStock((prev) => ({ ...prev, [p.id]: e.target.value, })) } onBlur={() => saveStock(p.id)} onKeyDown={(e) => { if (e.key === "Enter") saveStock(p.id); if (e.key === "Escape") setEditingStock(null); }} className="w-16 bg-[#0a0a0a] border border-[#D4AF37]/50 rounded-lg px-2 py-1 text-center text-sm outline-none" />
) : ( )}
{flagCfg.map((f) => ( ))}
{/* Pagination */} {totalPages > 1 && (
صفحة {page} من {totalPages}
)} )} {showAdd && ( setShowAdd(false)} onSuccess={() => { setShowAdd(false); load(); }} /> )} {editProduct && ( setEditProduct(null)} onSuccess={() => { setEditProduct(null); load(); }} /> )}
); } // ─── Product Modal (Full Fields) ───────────────────────── type SpecEntry = { key: string; value: string }; function ProductModal({ cats, product, onClose, onSuccess }: any) { const isEdit = !!product; const [tab, setTab] = useState<"basic" | "media" | "specs" | "flags">( "basic", ); const [form, setForm] = useState({ name: product?.name || "", name_en: product?.name_en || "", brand: product?.brand || "", category_id: product?.category_id || cats[0]?.id || 1, subcategory: product?.subcategory || "", sku: product?.sku || "", price: product?.price ?? "", original_price: product?.original_price ?? "", stock: product?.stock ?? 10, short_description: product?.short_description || "", description: product?.description || "", images: (product?.images || []).join("\n"), marketing_points: (product?.marketing_points || []).join("\n"), sizes: (product?.sizes || []).join(", "), colors: (product?.colors || []).join(", "), tags: (product?.tags || []).join(", "), is_trending: product?.is_trending || false, is_bestseller: product?.is_bestseller || false, is_new: product?.is_new ?? true, is_top_rated: product?.is_top_rated || false, }); const [specs, setSpecs] = useState(() => { const s = product?.specs || {}; return Object.keys(s).length > 0 ? Object.entries(s).map(([k, v]) => ({ key: k, value: String(v) })) : [{ key: "", value: "" }]; }); const [loading, setLoading] = useState(false); const mainCats = cats.filter((c: any) => !c.parent_id); const addSpec = () => setSpecs((s) => [...s, { key: "", value: "" }]); const removeSpec = (i: number) => setSpecs((s) => s.filter((_, idx) => idx !== i)); const updateSpec = (i: number, field: "key" | "value", val: string) => setSpecs((s) => s.map((entry, idx) => (idx === i ? { ...entry, [field]: val } : entry)), ); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!form.name || !form.price) { adminToast("الاسم والسعر مطلوبان", "err"); return; } setLoading(true); const specsObj: Record = {}; specs .filter((s) => s.key.trim()) .forEach((s) => { specsObj[s.key.trim()] = s.value.trim(); }); const payload: any = { name: form.name.trim(), name_en: form.name_en.trim() || undefined, brand: form.brand.trim() || undefined, category_id: parseInt(String(form.category_id)), subcategory: form.subcategory.trim() || undefined, sku: form.sku.trim() || undefined, price: parseFloat(String(form.price)), original_price: form.original_price !== "" ? parseFloat(String(form.original_price)) : undefined, stock: parseInt(String(form.stock)) || 0, short_description: form.short_description.trim() || undefined, description: form.description.trim() || undefined, images: form.images .split("\n") .map((s: string) => s.trim()) .filter(Boolean), marketing_points: form.marketing_points .split("\n") .map((s: string) => s.trim()) .filter(Boolean), sizes: form.sizes .split(",") .map((s: string) => s.trim()) .filter(Boolean), colors: form.colors .split(",") .map((s: string) => s.trim()) .filter(Boolean), tags: form.tags .split(",") .map((s: string) => s.trim()) .filter(Boolean), specs: Object.keys(specsObj).length > 0 ? specsObj : undefined, is_trending: form.is_trending, is_bestseller: form.is_bestseller, is_new: form.is_new, is_top_rated: form.is_top_rated, }; try { const url = isEdit ? `${API}/products/${product.id}` : `${API}/products`; const method = isEdit ? "PUT" : "POST"; const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); adminToast(isEdit ? "تم التحديث بنجاح ✓" : "تمت الإضافة بنجاح ✓"); onSuccess(); } catch (_) { adminToast("حدث خطأ أثناء الحفظ", "err"); } setLoading(false); }; const TABS = [ { id: "basic", label: "المعلومات الأساسية" }, { id: "media", label: "الصور والوصف" }, { id: "specs", label: "المواصفات والخيارات" }, { id: "flags", label: "التصنيفات والوسوم" }, ] as const; return (
{/* Modal header */}

{isEdit ? `تعديل: ${product.name.substring(0, 30)}…` : "إضافة منتج جديد"}

{/* Tabs */}
{TABS.map((t) => ( ))}
{/* Body */}
{/* Basic tab */} {tab === "basic" && (
setForm({ ...form, name: e.target.value }) } className={SH} placeholder="اسم المنتج بالعربي" />
setForm({ ...form, name_en: e.target.value }) } className={SH} placeholder="Product name in English" dir="ltr" />
setForm({ ...form, brand: e.target.value }) } className={SH} placeholder="مثال: Samsung, Apple" />
setForm({ ...form, sku: e.target.value }) } className={SH} placeholder="مثال: SKU-001" dir="ltr" />
setForm({ ...form, subcategory: e.target.value }) } className={SH} placeholder="مثال: هواتف آيفون" />
setForm({ ...form, price: e.target.value }) } className={SH} />
setForm({ ...form, original_price: e.target.value }) } className={SH} /> {form.price && form.original_price && parseFloat(String(form.original_price)) > parseFloat(String(form.price)) && (
خصم{" "} {Math.round( (1 - parseFloat(String(form.price)) / parseFloat(String(form.original_price))) * 100, )} %
)}
setForm({ ...form, stock: parseInt(e.target.value) || 0, }) } className={SH} />
)} {/* Media + Description tab */} {tab === "media" && (