2026-03-28 04:47:13 +00:00

7042 lines
275 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<MiniToast[]>([]);
useEffect(() => {
_setMiniToasts = setToasts;
return () => {
_setMiniToasts = null;
};
}, []);
if (toasts.length === 0) return null;
return (
<div
className="fixed top-4 left-1/2 -translate-x-1/2 z-[9999] flex flex-col items-center gap-2 pointer-events-none"
style={{ minWidth: 280 }}
>
{toasts.map((t) => (
<div
key={t.id}
className={`flex items-center gap-3 px-5 py-3.5 rounded-2xl text-sm font-bold shadow-2xl border pointer-events-auto
${
t.type === "err"
? "bg-red-950 border-red-500/50 text-red-200"
: t.type === "notif"
? "bg-[#1a1400] border-[#D4AF37]/60 text-white"
: "bg-[#0f1a0f] border-green-500/50 text-green-200"
}`}
style={{
animation: "slideInDown 0.35s cubic-bezier(.34,1.56,.64,1)",
minWidth: 240,
maxWidth: 380,
}}
>
<span className="text-lg shrink-0">
{t.type === "err" ? "⛔" : t.type === "notif" ? "🔔" : "✅"}
</span>
<span className="flex-1 text-right leading-snug">{t.msg}</span>
</div>
))}
</div>
);
}
// ─── Shared UI ──────────────────────────────────────────
function SectionHeader({
title,
subtitle,
}: {
title: string;
subtitle?: string;
}) {
return (
<div className="mb-6">
<h2 className="text-xl font-bold text-white">{title}</h2>
{subtitle && <p className="text-gray-500 text-sm mt-0.5">{subtitle}</p>}
</div>
);
}
function Table({
headers,
children,
empty,
}: {
headers: string[];
children: React.ReactNode;
empty?: boolean;
}) {
return (
<div className="bg-[#111] border border-[#222] rounded-2xl overflow-x-auto">
<table className="w-full text-sm text-right">
<thead className="bg-[#1a1a1a] text-gray-500">
<tr>
{headers.map((h) => (
<th key={h} className="px-4 py-3 font-medium whitespace-nowrap">
{h}
</th>
))}
</tr>
</thead>
<tbody>
{empty ? (
<tr>
<td
colSpan={headers.length}
className="text-center py-12 text-gray-600"
>
لا توجد بيانات
</td>
</tr>
) : (
children
)}
</tbody>
</table>
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const map: Record<string, string> = {
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<string, string> = {
pending: "جديد",
processing: "قيد التجهيز",
shipped: "تم الشحن",
delivered: "تم التوصيل",
cancelled: "ملغي",
returned: "مرتجع",
};
return (
<span
className={`px-2 py-0.5 rounded-lg text-xs font-bold ${map[status] || "bg-[#222] text-gray-400"}`}
>
{labels[status] || status}
</span>
);
}
function Spinner() {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 animate-spin text-[#D4AF37]" />
</div>
);
}
// ─── 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 (
<>
<AdminDashboard onLogout={handleLogout} />
<MiniToastProvider />
</>
);
return (
<div
className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"
dir="rtl"
>
<div className="bg-[#111] border border-[#222] rounded-3xl p-8 w-full max-w-md shadow-2xl">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-[#D4AF37]/10 rounded-2xl flex items-center justify-center mx-auto mb-4 border border-[#D4AF37]/30">
<LayoutDashboard className="w-8 h-8 text-[#D4AF37]" />
</div>
<h1 className="text-2xl font-black text-white">لوحة التحكم</h1>
<p className="text-gray-500 text-sm mt-1">متجر رين</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className={LH}>اسم المستخدم</label>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoComplete="username"
className={SH}
/>
</div>
<div>
<label className={LH}>كلمة المرور</label>
<div className="relative">
<input
type={showPass ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
className={SH}
/>
<button
type="button"
onClick={() => setShowPass(!showPass)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
>
{showPass ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="rounded"
/>
<span className="text-sm text-gray-400">تذكرني</span>
</label>
<button
type="submit"
disabled={loading}
className="w-full bg-[#D4AF37] text-black font-black py-3 rounded-xl flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
دخول
</button>
</form>
<p className="text-center text-xs text-gray-700 mt-6">
admin / admin123
</p>
</div>
</div>
);
}
// ─── 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<CheckoutNotif[]>([]);
const [showNotifPanel, setShowNotifPanel] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const [selectedNotifs, setSelectedNotifs] = useState<Set<number>>(new Set());
const lastOrderCount = useRef<number | null>(null);
const lastEventId = useRef<number>(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 (
<div
className="min-h-screen flex bg-[#0a0a0a] text-white"
dir="rtl"
onClick={unlockAudio}
>
{/* Mobile overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/60 z-40 md:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed md:static inset-y-0 right-0 z-50 w-64 bg-[#111] border-l border-[#222] flex flex-col transition-transform duration-300
${sidebarOpen ? "translate-x-0" : "translate-x-full md:translate-x-0"}`}
>
<div className="p-4 border-b border-[#222] flex items-center justify-between">
<div>
<h1 className="text-lg font-black text-[#D4AF37]">رين</h1>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
</span>
<p className="text-[10px] text-green-400 font-bold">
مباشر يتحدث تلقائياً
</p>
</div>
</div>
<button
onClick={handleBellClick}
className="relative w-9 h-9 rounded-xl bg-[#1a1a1a] border border-[#333] flex items-center justify-center text-gray-400 hover:text-[#D4AF37] hover:border-[#D4AF37]/50 transition-all"
>
<Bell className="w-4 h-4" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 min-w-[18px] h-[18px] bg-red-500 text-white text-[10px] font-black rounded-full flex items-center justify-center px-1 animate-pulse">
{unreadCount > 9 ? "9+" : unreadCount}
</span>
)}
</button>
</div>
{/* Notif Panel */}
{showNotifPanel && (
<div className="border-b border-[#222] bg-[#0d0d0d] max-h-80 overflow-y-auto flex flex-col">
{/* Panel header */}
<div className="px-3 py-2.5 flex items-center justify-between sticky top-0 bg-[#0d0d0d] border-b border-[#1a1a1a] z-10">
<span className="text-xs font-bold text-white">
نشاط العملاء ({checkoutNotifs.length})
</span>
<div className="flex items-center gap-1">
{selectedNotifs.size > 0 && (
<button
onClick={() => {
setCheckoutNotifs((n) =>
n.filter((ev) => !selectedNotifs.has(ev.id)),
);
setSelectedNotifs(new Set());
}}
className="text-[10px] text-red-400 border border-red-500/30 px-2 py-0.5 rounded-lg"
>
حذف {selectedNotifs.size}
</button>
)}
{checkoutNotifs.length > 0 && (
<button
onClick={() => {
setCheckoutNotifs([]);
setSelectedNotifs(new Set());
}}
className="text-[10px] text-gray-500 hover:text-gray-300 border border-[#333] px-2 py-0.5 rounded-lg"
>
مسح الكل
</button>
)}
<button
onClick={() => {
setShowNotifPanel(false);
setSelectedNotifs(new Set());
}}
className="text-gray-600 hover:text-gray-400 p-0.5"
>
<X className="w-3 h-3" />
</button>
</div>
</div>
{/* Select all row */}
{checkoutNotifs.length > 1 && (
<div className="px-3 py-1.5 border-b border-[#1a1a1a] flex items-center gap-2">
<input
type="checkbox"
checked={
checkoutNotifs.length > 0 &&
checkoutNotifs.every((ev) => selectedNotifs.has(ev.id))
}
onChange={() => {
const allSel = checkoutNotifs.every((ev) =>
selectedNotifs.has(ev.id),
);
setSelectedNotifs(
allSel
? new Set()
: new Set(checkoutNotifs.map((ev) => ev.id)),
);
}}
className="rounded w-3 h-3"
/>
<span className="text-[10px] text-gray-600">تحديد الكل</span>
</div>
)}
{checkoutNotifs.length === 0 ? (
<p className="text-xs text-gray-600 px-4 py-3">
لا يوجد نشاط بعد
</p>
) : (
<div className="px-2 py-2 space-y-1">
{checkoutNotifs.map((ev) => {
const meta = getNotifMeta(ev);
const isSel = selectedNotifs.has(ev.id);
return (
<div
key={ev.id}
className={`flex items-start gap-2 rounded-lg px-2.5 py-2 border cursor-pointer transition-colors
${isSel ? "bg-[#D4AF37]/8 border-[#D4AF37]/20" : "bg-[#1a1a1a] border-[#2a2a2a]"}`}
onClick={() => {
const s = new Set(selectedNotifs);
s.has(ev.id) ? s.delete(ev.id) : s.add(ev.id);
setSelectedNotifs(s);
}}
>
<input
type="checkbox"
checked={isSel}
onChange={() => {}}
className="mt-0.5 rounded w-3 h-3 shrink-0"
/>
<span className="text-sm shrink-0">{meta.emoji}</span>
<div className="min-w-0 flex-1">
<p className="text-xs font-semibold">{meta.title}</p>
<p className="text-[10px] text-gray-500 truncate">
{ev.details || ev.order_hint || shortSessionId(ev.session_id, 24)}
</p>
<p className="text-[10px] text-gray-600">
{new Date(ev.created_at).toLocaleTimeString("ar-SA")}
</p>
</div>
</div>
);
})}
</div>
)}
</div>
)}
<nav className="flex-1 p-3 overflow-y-auto">
{TABS.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => {
setActiveTab(tab.id);
setSidebarOpen(false);
}}
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all mb-1
${activeTab === tab.id ? "bg-[#D4AF37]/15 text-[#D4AF37] border border-[#D4AF37]/30" : "text-gray-500 hover:bg-[#1a1a1a] hover:text-white"}`}
>
<Icon className="w-4 h-4 shrink-0" />
{tab.name}
</button>
);
})}
</nav>
<div className="p-4 border-t border-[#222] space-y-2">
<button
onClick={() => {
// Directly play in the click handler (user gesture) to unlock autoplay
try {
if (!_bellUrl) _bellUrl = buildBellWav();
if (!_bellAudio) {
_bellAudio = new Audio(_bellUrl);
}
_bellAudio.volume = 0.6;
_bellAudio.currentTime = 0;
_bellAudio.play().catch(() => {});
_audioUnlocked = true;
} catch (_) {}
adminToast("🔔 الصوت مفعّل — ستسمعه عند وصول طلب جديد");
}}
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-[#D4AF37] hover:bg-[#D4AF37]/10 rounded-xl border border-[#D4AF37]/20 transition-colors"
>
<Bell className="w-4 h-4" /> 🔔 اختبار رنين التنبيه
</button>
<button
onClick={onLogout}
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-red-400 hover:bg-red-400/10 rounded-xl"
>
<LogOut className="w-4 h-4" /> تسجيل خروج
</button>
</div>
</aside>
{/* Main */}
<main className="flex-1 overflow-y-auto">
<div className="md:hidden flex items-center justify-between p-4 border-b border-[#222] bg-[#111]">
<h2 className="font-bold text-[#D4AF37]">
{TABS.find((t) => t.id === activeTab)?.name}
</h2>
<button
onClick={() => setSidebarOpen(true)}
className="p-2 rounded-xl bg-[#1a1a1a] border border-[#333]"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
<div className="p-6 md:p-8">
{activeTab === "dashboard" && (
<DashboardTab checkoutNotifs={checkoutNotifs} />
)}
{activeTab === "products" && <ProductsTab />}
{activeTab === "orders" && <OrdersTab />}
{activeTab === "reviews" && <ReviewsTab />}
{activeTab === "coupons" && <CouponsTab />}
{activeTab === "cards" && <CardsTab />}
{activeTab === "customers" && <CustomersTab />}
{activeTab === "analytics" && <AnalyticsTab />}
{activeTab === "support" && <SupportTab />}
{activeTab === "offers" && <OffersTab />}
{activeTab === "abandoned" && <AbandonedCartsTab />}
{activeTab === "categories" && <CategoriesTab />}
{activeTab === "delivery" && <DeliveryTab />}
{activeTab === "appearance" && <AppearanceTab />}
{activeTab === "settings" && <SettingsTab />}
</div>
</main>
</div>
);
}
// ─── 1. Dashboard Tab ────────────────────────────────────
function DashboardTab({ checkoutNotifs }: { checkoutNotifs: CheckoutNotif[] }) {
const [stats, setStats] = useState<any>(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 <Spinner />;
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 (
<div>
<SectionHeader title="لوحة الملخص" subtitle="نظرة عامة على أداء المتجر" />
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
{cards.map((c) => (
<div
key={c.label}
className={`${c.bg} border border-[#222] rounded-2xl p-4`}
>
<div className={`text-2xl font-black ${c.color} mb-1`}>
{c.value}
</div>
<div className="text-xs text-gray-500">{c.label}</div>
</div>
))}
</div>
{stats?.low_stock_count > 0 && (
<div className="mb-6 bg-red-500/10 border border-red-500/30 rounded-xl p-4 flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-400 shrink-0" />
<p className="text-sm text-red-400">
تنبيه: يوجد <strong>{stats.low_stock_count}</strong> منتج مخزونه
منخفض (أقل من 5 وحدات)
</p>
</div>
)}
<div>
<div className="flex items-center gap-3 mb-4">
<Bell className="w-5 h-5 text-[#D4AF37]" />
<h2 className="text-lg font-bold">نشاط المتجر الآني</h2>
<span className="flex items-center gap-1 text-xs bg-green-500/10 text-green-400 border border-green-500/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse inline-block" />
مباشر كل 5 ثوانٍ
</span>
</div>
{checkoutNotifs.length === 0 ? (
<div className="bg-[#111] border border-[#222] rounded-xl p-8 text-center text-gray-600">
<Bell className="w-8 h-8 mx-auto mb-3 opacity-20" />
<p>لا توجد تنبيهات حديثة من المتجر بعد</p>
</div>
) : (
<div className="space-y-3">
{checkoutNotifs.slice(0, 10).map((ev, i) => {
const meta = getNotifMeta(ev);
const tone = notifToneClasses(meta.tone);
return (
<div
key={i}
className={`flex items-center gap-4 rounded-xl px-4 py-3 border ${tone.bg}`}
>
<span className="text-2xl">{meta.emoji}</span>
<div className="flex-1 min-w-0">
<p className={`text-sm font-bold ${tone.text}`}>{meta.title}</p>
<p className="text-xs text-gray-500 truncate">
{ev.details || meta.details}
</p>
<p className="text-[10px] text-gray-600 truncate mt-1">
{ev.order_hint
? `الطلب: ${ev.order_hint}`
: `معرف الجلسة: ${shortSessionId(ev.session_id)}`}
</p>
</div>
<p className="text-xs text-gray-600 shrink-0">
{new Date(ev.created_at).toLocaleTimeString("ar-SA")}
</p>
</div>
);
})}
</div>
)}
</div>
</div>
);
}
// ─── 2. Products Tab (Full Control) ──────────────────────
function ProductsTab() {
const [products, setProducts] = useState<any[]>([]);
const [cats, setCats] = useState<any[]>([]);
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<any>(null);
const [selected, setSelected] = useState<Set<number>>(new Set());
const [inlineStock, setInlineStock] = useState<Record<number, string>>({});
const [editingStock, setEditingStock] = useState<number | null>(null);
const [editingPrice, setEditingPrice] = useState<number | null>(null);
const [inlinePrice, setInlinePrice] = useState<Record<number, string>>({});
const [inlineOrigPrice, setInlineOrigPrice] = useState<
Record<number, string>
>({});
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<HTMLInputElement>) => {
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<string, string> = {};
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 (
<div>
{/* Header */}
<div className="flex flex-wrap justify-between items-start mb-4 gap-3">
<div>
<h2 className="text-xl font-bold text-white">إدارة المنتجات</h2>
<p className="text-sm text-gray-500 mt-0.5">إجمالي {total} منتج</p>
</div>
<div className="flex flex-wrap gap-2">
{selected.size > 0 && (
<div className="flex flex-wrap items-center gap-2 bg-[#1a1a1a] border border-[#D4AF37]/30 rounded-xl px-3 py-2">
<span className="text-[#D4AF37] text-xs font-bold">
{selected.size} محدد
</span>
<div className="w-px h-4 bg-[#333]" />
{/* Bulk discount popover */}
<div className="relative">
<button
onClick={() => setShowBulkDiscount((v) => !v)}
className="text-xs text-orange-400 hover:text-orange-300 border border-orange-400/30 hover:border-orange-400/60 px-2.5 py-1 rounded-lg flex items-center gap-1.5 transition-all"
>
<Tag className="w-3 h-3" /> تخفيض %
</button>
{showBulkDiscount && (
<div className="absolute top-full mt-1 right-0 bg-[#111] border border-[#333] rounded-xl p-3 shadow-2xl z-50 min-w-[200px]">
<p className="text-xs text-gray-400 mb-2">
نسبة الخصم على {selected.size} منتج
</p>
<div className="flex gap-2">
<input
type="number"
min="1"
max="99"
value={bulkDiscountPct}
onChange={(e) => 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"
/>
<span className="text-gray-400 self-center text-sm">
%
</span>
<button
onClick={applyBulkDiscount}
className="bg-[#D4AF37] text-black text-xs font-bold px-3 py-1.5 rounded-lg hover:bg-[#e6c84a] transition-colors"
>
تطبيق
</button>
</div>
</div>
)}
</div>
<button
onClick={removeBulkDiscount}
className="text-xs text-gray-500 hover:text-white border border-[#333] px-2.5 py-1 rounded-lg transition-all"
>
إزالة الخصم
</button>
<div className="w-px h-4 bg-[#333]" />
<button
onClick={handleBulkDelete}
className="text-xs text-red-400 hover:text-red-300 border border-red-500/30 px-2.5 py-1 rounded-lg flex items-center gap-1.5 transition-all"
>
<Trash2 className="w-3 h-3" /> حذف {selected.size}
</button>
</div>
)}
<button
onClick={exportCsv}
className="bg-[#1a1a1a] border border-[#333] text-gray-300 px-3 py-2 rounded-xl text-sm flex items-center gap-2 hover:border-[#D4AF37]/50"
>
<FileText className="w-4 h-4" /> تصدير CSV
</button>
<label className="cursor-pointer bg-[#1a1a1a] border border-[#333] text-gray-300 px-3 py-2 rounded-xl text-sm flex items-center gap-2 hover:border-[#D4AF37]/50">
<Upload className="w-4 h-4" /> رفع CSV
<input
type="file"
accept=".csv"
className="hidden"
onChange={handleCsvUpload}
/>
</label>
<button
onClick={() => setShowAdd(true)}
className="bg-[#D4AF37] text-black px-4 py-2 rounded-xl text-sm flex items-center gap-2 font-bold"
>
<Plus className="w-4 h-4" /> منتج جديد
</button>
</div>
</div>
{/* Stats bar */}
<div className="grid grid-cols-4 gap-3 mb-4">
{[
{
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) => (
<button
key={item.f}
onClick={() =>
setStockFilter(
stockFilter === item.f && item.f !== "" ? "" : item.f,
)
}
className={`border rounded-xl p-3 text-center transition-all ${item.c} ${stockFilter === item.f ? "ring-1 ring-white/20" : "opacity-70 hover:opacity-100"}`}
>
<div className="text-2xl font-black">{item.val}</div>
<div className="text-xs mt-0.5">{item.label}</div>
</button>
))}
</div>
{/* Filters */}
<div className="flex flex-wrap gap-2 mb-3">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-600" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="بحث..."
className={`${SH} pr-10`}
/>
</div>
<select
value={catFilter}
onChange={(e) => setCatFilter(e.target.value)}
className="bg-[#1a1a1a] border border-[#333] rounded-xl px-3 py-2.5 text-white text-sm outline-none focus:border-[#D4AF37]"
>
<option value="">كل الفئات</option>
{cats
.filter((c) => !c.parent_id)
.map((c: any) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
<select
value={featFilter}
onChange={(e) => setFeatFilter(e.target.value)}
className="bg-[#1a1a1a] border border-[#333] rounded-xl px-3 py-2.5 text-white text-sm outline-none focus:border-[#D4AF37]"
>
<option value="">كل المنتجات</option>
<option value="trending">الأكثر رواجاً</option>
<option value="bestseller">الأكثر مبيعاً</option>
<option value="new_arrivals">وصل حديثاً</option>
<option value="top_rated">أعلى تقييماً</option>
</select>
<select
value={sort}
onChange={(e) => setSort(e.target.value)}
className="bg-[#1a1a1a] border border-[#333] rounded-xl px-3 py-2.5 text-white text-sm outline-none focus:border-[#D4AF37]"
>
<option value="newest">الأحدث</option>
<option value="price_asc">الأرخص أولاً</option>
<option value="price_desc">الأغلى أولاً</option>
<option value="rating">الأعلى تقييماً</option>
<option value="popular">الأكثر شعبية</option>
</select>
</div>
{loading ? (
<Spinner />
) : (
<>
<div className="bg-[#111] border border-[#222] rounded-2xl overflow-x-auto">
<table className="w-full text-sm text-right">
<thead className="bg-[#1a1a1a] text-gray-500">
<tr>
<th className="px-3 py-3 w-8">
<input
type="checkbox"
checked={allSel}
onChange={toggleAll}
className="rounded"
/>
</th>
<th className="px-4 py-3 font-medium">المنتج</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
الفئة
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
السعر
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
المخزون
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
الوسوم
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
إجراءات
</th>
</tr>
</thead>
<tbody>
{displayProducts.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-12 text-gray-600">
لا توجد منتجات
</td>
</tr>
) : (
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 (
<tr
key={p.id}
className={`border-t border-[#1a1a1a] hover:bg-[#1a1a1a]/50 transition-colors ${selected.has(p.id) ? "bg-[#D4AF37]/5" : ""}`}
>
<td className="px-3 py-3">
<input
type="checkbox"
checked={selected.has(p.id)}
onChange={(e) => {
const s = new Set(selected);
e.target.checked ? s.add(p.id) : s.delete(p.id);
setSelected(s);
}}
className="rounded"
/>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-3 min-w-[200px]">
{p.images?.[0] ? (
<img
src={p.images[0]}
className="w-12 h-12 rounded-xl object-cover border border-[#222] shrink-0"
onError={(e) => {
(e.target as any).style.display = "none";
}}
/>
) : (
<div className="w-12 h-12 rounded-xl bg-[#222] shrink-0 flex items-center justify-center text-gray-600 text-xs">
لا صورة
</div>
)}
<div className="min-w-0">
<div className="font-semibold text-sm leading-snug line-clamp-2">
{p.name}
</div>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
{p.brand && (
<span className="text-xs text-gray-500">
{p.brand}
</span>
)}
{p.sku && (
<span className="text-[10px] font-mono text-gray-600 bg-[#222] px-1 rounded">
{p.sku}
</span>
)}
{p.subcategory && (
<span className="text-[10px] text-blue-400 bg-blue-500/10 px-1.5 py-0.5 rounded">
{p.subcategory}
</span>
)}
</div>
</div>
</div>
</td>
<td className="px-4 py-3 text-xs text-gray-500 whitespace-nowrap">
{cat?.name || "-"}
</td>
<td className="px-4 py-3 whitespace-nowrap min-w-[140px]">
{editingPrice === p.id ? (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input
type="number"
step="0.01"
min="0"
autoFocus
value={inlinePrice[p.id] ?? p.price}
onChange={(e) =>
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"
/>
<span className="text-[10px] text-gray-500">
ر.س
</span>
</div>
<div className="flex items-center gap-1">
<input
type="number"
step="0.01"
min="0"
value={
inlineOrigPrice[p.id] ??
(p.original_price || "")
}
onChange={(e) =>
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"
/>
<span className="text-[10px] text-gray-600">
أصلي
</span>
</div>
<div className="flex gap-1 mt-0.5">
<button
onClick={() => savePrice(p.id)}
className="text-[10px] bg-[#D4AF37] text-black font-bold px-2 py-0.5 rounded"
>
حفظ
</button>
<button
onClick={() => setEditingPrice(null)}
className="text-[10px] text-gray-500 hover:text-white px-2 py-0.5 rounded border border-[#333]"
>
إلغاء
</button>
</div>
</div>
) : (
<button
onClick={() => {
setEditingPrice(p.id);
setInlinePrice((prev) => ({
...prev,
[p.id]: String(p.price),
}));
setInlineOrigPrice((prev) => ({
...prev,
[p.id]: p.original_price
? String(p.original_price)
: "",
}));
}}
className="text-right hover:bg-[#1a1a1a] rounded-lg px-2 py-1 transition-colors group w-full"
>
<div className="font-bold text-[#D4AF37] group-hover:text-[#e6c84a] flex items-center gap-1">
{formatPrice(p.price)}
<Pencil className="w-2.5 h-2.5 opacity-0 group-hover:opacity-60" />
</div>
{discPct > 0 ? (
<div className="flex items-center gap-1 mt-0.5">
<span className="text-[10px] text-gray-500 line-through">
{formatPrice(p.original_price)}
</span>
<span className="text-[10px] text-red-400 font-bold bg-red-500/10 px-1 rounded">
-{discPct}%
</span>
</div>
) : null}
</button>
)}
</td>
<td className="px-4 py-3">
{editingStock === p.id ? (
<div className="flex items-center gap-1">
<input
type="number"
autoFocus
value={inlineStock[p.id] ?? p.stock}
onChange={(e) =>
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"
/>
</div>
) : (
<button
onClick={() => {
setEditingStock(p.id);
setInlineStock((prev) => ({
...prev,
[p.id]: String(p.stock),
}));
}}
className={`font-bold text-base px-2 py-0.5 rounded-lg border transition-colors
${
p.stock === 0
? "text-red-400 border-red-500/30 bg-red-500/10"
: p.stock <= 5
? "text-yellow-400 border-yellow-500/30 bg-yellow-500/10"
: "text-green-400 border-green-500/20 bg-green-500/5"
}`}
>
{p.stock}
</button>
)}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{flagCfg.map((f) => (
<button
key={f.key}
onClick={() => toggleFlag(p, f.key)}
className={`text-[10px] px-2 py-0.5 rounded border transition-all ${p[f.key] ? f.color + " font-bold" : "text-gray-700 border-[#333]"}`}
>
{f.label}
</button>
))}
</div>
</td>
<td className="px-4 py-3">
<div className="flex gap-1 shrink-0">
<button
title="تعديل"
onClick={() => setEditProduct(p)}
className="p-1.5 text-blue-400 hover:bg-blue-500/10 rounded-lg"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
title="نسخ"
onClick={() => duplicateProduct(p)}
className="p-1.5 text-[#D4AF37]/70 hover:bg-[#D4AF37]/10 rounded-lg"
>
<Copy className="w-3.5 h-3.5" />
</button>
<button
title="حذف"
onClick={() => handleDelete(p.id)}
className="p-1.5 text-red-400 hover:bg-red-500/10 rounded-lg"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-4">
<button
disabled={page === 1}
onClick={() => setPage((p) => p - 1)}
className="px-3 py-1.5 bg-[#1a1a1a] border border-[#333] rounded-lg text-sm disabled:opacity-40"
>
السابق
</button>
<span className="text-sm text-gray-400">
صفحة {page} من {totalPages}
</span>
<button
disabled={page === totalPages}
onClick={() => setPage((p) => p + 1)}
className="px-3 py-1.5 bg-[#1a1a1a] border border-[#333] rounded-lg text-sm disabled:opacity-40"
>
التالي
</button>
</div>
)}
</>
)}
{showAdd && (
<ProductModal
cats={cats}
onClose={() => setShowAdd(false)}
onSuccess={() => {
setShowAdd(false);
load();
}}
/>
)}
{editProduct && (
<ProductModal
cats={cats}
product={editProduct}
onClose={() => setEditProduct(null)}
onSuccess={() => {
setEditProduct(null);
load();
}}
/>
)}
</div>
);
}
// ─── 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<SpecEntry[]>(() => {
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<string, string> = {};
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 (
<div
className="fixed inset-0 bg-black/85 z-50 flex items-center justify-center p-4"
dir="rtl"
>
<div className="bg-[#111] border border-[#222] rounded-2xl w-full max-w-2xl max-h-[92vh] flex flex-col">
{/* Modal header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#222] shrink-0">
<h3 className="font-bold text-lg">
{isEdit
? `تعديل: ${product.name.substring(0, 30)}`
: "إضافة منتج جديد"}
</h3>
<button onClick={onClose}>
<X className="w-5 h-5 text-gray-400 hover:text-white" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-[#222] shrink-0 overflow-x-auto">
{TABS.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`px-5 py-3 text-sm font-medium whitespace-nowrap transition-colors border-b-2
${tab === t.id ? "border-[#D4AF37] text-[#D4AF37]" : "border-transparent text-gray-500 hover:text-white"}`}
>
{t.label}
</button>
))}
</div>
{/* Body */}
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto">
<div className="p-6 space-y-4">
{/* Basic tab */}
{tab === "basic" && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className={LH}>اسم المنتج (عربي) *</label>
<input
required
value={form.name}
onChange={(e) =>
setForm({ ...form, name: e.target.value })
}
className={SH}
placeholder="اسم المنتج بالعربي"
/>
</div>
<div className="col-span-2">
<label className={LH}>اسم المنتج (إنجليزي)</label>
<input
value={form.name_en}
onChange={(e) =>
setForm({ ...form, name_en: e.target.value })
}
className={SH}
placeholder="Product name in English"
dir="ltr"
/>
</div>
<div>
<label className={LH}>الماركة / البراند</label>
<input
value={form.brand}
onChange={(e) =>
setForm({ ...form, brand: e.target.value })
}
className={SH}
placeholder="مثال: Samsung, Apple"
/>
</div>
<div>
<label className={LH}>رمز SKU</label>
<input
value={form.sku}
onChange={(e) =>
setForm({ ...form, sku: e.target.value })
}
className={SH}
placeholder="مثال: SKU-001"
dir="ltr"
/>
</div>
<div>
<label className={LH}>الفئة الرئيسية *</label>
<select
value={form.category_id}
onChange={(e) =>
setForm({
...form,
category_id: parseInt(e.target.value),
})
}
className={SH}
>
{mainCats.map((c: any) => (
<option key={c.id} value={c.id}>
{c.icon} {c.name}
</option>
))}
</select>
</div>
<div>
<label className={LH}>الفئة الفرعية</label>
<input
value={form.subcategory}
onChange={(e) =>
setForm({ ...form, subcategory: e.target.value })
}
className={SH}
placeholder="مثال: هواتف آيفون"
/>
</div>
<div>
<label className={LH}>السعر الحالي (ريال) *</label>
<input
required
type="number"
step="0.01"
min="0"
value={form.price}
onChange={(e) =>
setForm({ ...form, price: e.target.value })
}
className={SH}
/>
</div>
<div>
<label className={LH}>السعر قبل الخصم (ريال)</label>
<input
type="number"
step="0.01"
min="0"
value={form.original_price}
onChange={(e) =>
setForm({ ...form, original_price: e.target.value })
}
className={SH}
/>
{form.price &&
form.original_price &&
parseFloat(String(form.original_price)) >
parseFloat(String(form.price)) && (
<div className="text-xs text-green-400 mt-1">
خصم{" "}
{Math.round(
(1 -
parseFloat(String(form.price)) /
parseFloat(String(form.original_price))) *
100,
)}
%
</div>
)}
</div>
<div>
<label className={LH}>الكمية في المخزون</label>
<input
type="number"
min="0"
value={form.stock}
onChange={(e) =>
setForm({
...form,
stock: parseInt(e.target.value) || 0,
})
}
className={SH}
/>
</div>
</div>
</div>
)}
{/* Media + Description tab */}
{tab === "media" && (
<div className="space-y-4">
<div>
<label className={LH}>روابط الصور (سطر لكل رابط)</label>
<textarea
rows={6}
value={form.images}
onChange={(e) =>
setForm({ ...form, images: e.target.value })
}
className={`${SH} resize-none font-mono text-xs leading-6`}
placeholder={
"https://example.com/image1.jpg\nhttps://example.com/image2.jpg"
}
dir="ltr"
/>
{/* Preview images */}
{form.images.trim() && (
<div className="flex gap-2 mt-2 flex-wrap">
{form.images
.split("\n")
.filter(Boolean)
.slice(0, 6)
.map((url: string, i: number) => (
<img
key={i}
src={url.trim()}
className="w-16 h-16 rounded-lg object-cover border border-[#333]"
onError={(e) => {
(e.target as any).style.opacity = "0.3";
}}
/>
))}
</div>
)}
</div>
<div>
<label className={LH}>وصف مختصر</label>
<input
value={form.short_description}
onChange={(e) =>
setForm({ ...form, short_description: e.target.value })
}
className={SH}
placeholder="جملة واحدة تصف المنتج"
/>
</div>
<div>
<label className={LH}>الوصف التفصيلي</label>
<textarea
rows={5}
value={form.description}
onChange={(e) =>
setForm({ ...form, description: e.target.value })
}
className={`${SH} resize-none`}
placeholder="اكتب وصفاً تفصيلياً للمنتج..."
/>
</div>
<div>
<label className={LH}>نقاط التسويق (سطر لكل نقطة)</label>
<textarea
rows={4}
value={form.marketing_points}
onChange={(e) =>
setForm({ ...form, marketing_points: e.target.value })
}
className={`${SH} resize-none`}
placeholder={
"شاشة AMOLED فائقة الوضوح\nبطارية تدوم 48 ساعة\nمقاومة للماء IP68"
}
/>
</div>
</div>
)}
{/* Specs tab */}
{tab === "specs" && (
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<label className={LH + " mb-0"}>المواصفات التقنية</label>
<button
type="button"
onClick={addSpec}
className="text-xs text-[#D4AF37] border border-[#D4AF37]/30 px-2 py-1 rounded-lg flex items-center gap-1"
>
<Plus className="w-3 h-3" /> إضافة مواصفة
</button>
</div>
<div className="space-y-2">
{specs.map((s, i) => (
<div key={i} className="flex gap-2 items-center">
<input
value={s.key}
onChange={(e) => updateSpec(i, "key", e.target.value)}
placeholder="الخاصية (مثال: الذاكرة)"
className={`${SH} flex-1`}
/>
<input
value={s.value}
onChange={(e) =>
updateSpec(i, "value", e.target.value)
}
placeholder="القيمة (مثال: 8 جيجابايت)"
className={`${SH} flex-1`}
/>
<button
type="button"
onClick={() => removeSpec(i)}
className="text-red-400 p-1.5 hover:bg-red-500/10 rounded shrink-0"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
<div>
<label className={LH}>المقاسات (مفصولة بفاصلة)</label>
<input
value={form.sizes}
onChange={(e) =>
setForm({ ...form, sizes: e.target.value })
}
className={SH}
placeholder="S, M, L, XL"
dir="ltr"
/>
</div>
<div>
<label className={LH}>الألوان (مفصولة بفاصلة)</label>
<input
value={form.colors}
onChange={(e) =>
setForm({ ...form, colors: e.target.value })
}
className={SH}
placeholder="أسود, أبيض, ذهبي"
/>
</div>
<div>
<label className={LH}>الوسوم / Tags (مفصولة بفاصلة)</label>
<input
value={form.tags}
onChange={(e) => setForm({ ...form, tags: e.target.value })}
className={SH}
placeholder="هاتف, آبل, 5G"
/>
</div>
</div>
)}
{/* Flags tab */}
{tab === "flags" && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
{[
{
k: "is_trending",
label: "🔥 الأكثر رواجاً",
desc: "يظهر في قسم المنتجات الرائجة",
color: "border-orange-500/30 bg-orange-500/5",
},
{
k: "is_bestseller",
label: "⭐ الأكثر مبيعاً",
desc: "يظهر في قسم الأكثر مبيعاً",
color: "border-blue-500/30 bg-blue-500/5",
},
{
k: "is_new",
label: "✨ وصل حديثاً",
desc: "يظهر في قسم الجديد",
color: "border-green-500/30 bg-green-500/5",
},
{
k: "is_top_rated",
label: "🏆 أعلى تقييماً",
desc: "يظهر في الأعلى تقييماً",
color: "border-purple-500/30 bg-purple-500/5",
},
].map((flag) => (
<label
key={flag.k}
className={`flex items-start gap-3 p-4 border rounded-xl cursor-pointer transition-all ${(form as any)[flag.k] ? flag.color + " ring-1 ring-white/10" : "border-[#222]"}`}
>
<input
type="checkbox"
checked={(form as any)[flag.k]}
onChange={(e) =>
setForm({ ...form, [flag.k]: e.target.checked })
}
className="mt-0.5 shrink-0"
/>
<div>
<div className="font-semibold text-sm">
{flag.label}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{flag.desc}
</div>
</div>
</label>
))}
</div>
{isEdit && (
<div className="bg-[#1a1a1a] border border-[#333] rounded-xl p-4 text-xs text-gray-500 space-y-1">
<div>
معرف المنتج:{" "}
<span className="font-mono text-gray-400">
#{product.id}
</span>
</div>
{product.rating && (
<div>
التقييم:{" "}
<span className="text-yellow-400">
{parseFloat(product.rating).toFixed(1)}
</span>{" "}
({product.review_count || 0} تقييم)
</div>
)}
</div>
)}
</div>
)}
</div>
{/* Footer */}
<div className="sticky bottom-0 px-6 py-4 border-t border-[#222] bg-[#111] flex gap-3">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 bg-[#1a1a1a] border border-[#333] rounded-xl text-sm"
>
إلغاء
</button>
<div className="flex gap-2 flex-1">
{
TABS.filter((t2) => t2.id !== tab).map((_, __, arr) => {
const idx = TABS.findIndex((t2) => t2.id === tab);
return idx < TABS.length - 1 ? (
<button
key="next"
type="button"
onClick={() => setTab(TABS[idx + 1].id)}
className="px-5 py-2.5 bg-[#1a1a1a] border border-[#333] rounded-xl text-sm"
>
التالي
</button>
) : null;
})[0]
}
<button
type="submit"
disabled={loading}
className="flex-1 bg-[#D4AF37] text-black font-black py-2.5 rounded-xl text-sm flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
{isEdit ? "💾 حفظ التعديلات" : " إضافة المنتج"}
</button>
</div>
</div>
</form>
</div>
</div>
);
}
// ─── 3. Orders Tab ──────────────────────────────────────
function OrdersTab() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [filterStatus, setFilterStatus] = useState("");
const [selectedOrder, setSelectedOrder] = useState<any>(null);
const load = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/orders?limit=200`);
setData(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, [load]);
const handleStatus = async (id: number, status: string) => {
await fetch(`${API}/orders/${id}/status`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
adminToast("تم التحديث");
load();
};
const handleDelete = async (id: number) => {
if (!confirm("تأكيد حذف الطلب؟")) return;
await fetch(`${API}/orders/${id}`, { method: "DELETE" });
adminToast("تم الحذف");
load();
};
const printInvoice = (order: any) => {
const items = (order.items || []) as any[];
const addrParts = [
order.city,
order.neighborhood && `حي ${order.neighborhood}`,
order.street && `شارع ${order.street}`,
order.building && `مبنى ${order.building}`,
order.floor && `دور ${order.floor}`,
]
.filter(Boolean)
.join(" — ");
const html = `<html dir="rtl"><head><meta charset="UTF-8"><style>
body{font-family:Arial,sans-serif;padding:40px;color:#111}
h1{color:#D4AF37;font-size:22px;margin-bottom:4px}
.subtitle{color:#888;font-size:13px;margin-bottom:24px}
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:20px;background:#f9f9f9;padding:16px;border-radius:8px;font-size:13px}
.otp-box{background:#D4AF3722;border:2px solid #D4AF37;border-radius:8px;padding:12px 20px;margin-bottom:20px;display:inline-block}
.otp-code{font-size:28px;font-weight:bold;color:#8B6914;letter-spacing:6px;font-family:monospace}
table{width:100%;border-collapse:collapse;margin-top:16px}
th,td{border:1px solid #ddd;padding:10px;text-align:right}
th{background:#f0f0f0;font-weight:bold}
.totals{margin-top:16px;border:1px solid #ddd;border-radius:8px;overflow:hidden}
.totals tr:last-child{background:#D4AF3722;font-weight:bold;font-size:16px}
.footer{margin-top:40px;font-size:11px;color:#999;border-top:1px solid #eee;padding-top:10px;text-align:center}
</style></head><body>
<h1>🛍️ فاتورة ضريبية — متجر رين</h1>
<div class="subtitle">رقم الطلب: <strong>${order.order_number}</strong> &nbsp;|&nbsp; التاريخ: ${new Date(order.created_at).toLocaleDateString("ar-SA")} ${new Date(order.created_at).toLocaleTimeString("ar-SA")}</div>
${order.purchase_confirmation_code ? `<div class="otp-box"><div style="font-size:12px;color:#888;margin-bottom:4px">تأكيد الشراء</div><div class="otp-code">${order.purchase_confirmation_code}</div></div><br/>` : ""}
<div class="info-grid">
<div><strong>اسم العميل</strong><br/>${order.customer_name}</div>
<div><strong>رقم الجوال</strong><br/>${order.customer_phone}</div>
${order.customer_email ? `<div><strong>البريد الإلكتروني</strong><br/>${order.customer_email}</div>` : ""}
<div><strong>طريقة الدفع</strong><br/>${order.payment_method}</div>
<div style="grid-column:1/-1"><strong>عنوان التوصيل</strong><br/>${addrParts || order.shipping_address}</div>
</div>
<table><tr><th>المنتج</th><th>الكمية</th><th>السعر للوحدة</th><th>الإجمالي</th></tr>
${items.map((i) => `<tr><td>${i.product_name}${i.selected_size ? ` — مقاس: ${i.selected_size}` : ""}${i.selected_color ? ` — لون: ${i.selected_color}` : ""}</td><td style="text-align:center">${i.quantity}</td><td>${parseFloat(i.price).toFixed(2)} ر.س</td><td style="font-weight:bold">${(i.price * i.quantity).toFixed(2)} ر.س</td></tr>`).join("")}
</table>
<table class="totals">
<tr><td>المجموع الفرعي</td><td style="text-align:left">${parseFloat(String(order.subtotal)).toFixed(2)} ر.س</td></tr>
${parseFloat(String(order.discount)) > 0 ? `<tr><td>الخصم</td><td style="text-align:left;color:green">- ${parseFloat(String(order.discount)).toFixed(2)} ر.س</td></tr>` : ""}
<tr><td>رسوم الشحن</td><td style="text-align:left">${parseFloat(String(order.shipping_fee)).toFixed(2)} ر.س</td></tr>
<tr><td>الإجمالي النهائي</td><td style="text-align:left">${parseFloat(String(order.total)).toFixed(2)} ر.س</td></tr>
</table>
<div class="footer">متجر رين للإلكترونيات — جميع الأسعار شاملة ضريبة القيمة المضافة 15%</div>
</body></html>`;
const w = window.open("", "_blank");
if (w) {
w.document.write(html);
w.document.close();
w.print();
}
};
const statusOptions: [string, string][] = [
["", "الكل"],
["pending", "جديد"],
["processing", "قيد التجهيز"],
["shipped", "تم الشحن"],
["delivered", "تم التوصيل"],
["returned", "مرتجع"],
["cancelled", "ملغي"],
];
const orders = (data?.orders || []).filter(
(o: any) => !filterStatus || o.status === filterStatus,
);
const statsMap = statusOptions.slice(1).map(([val, label]) => ({
val,
label,
count: (data?.orders || []).filter((o: any) => o.status === val).length,
}));
return (
<div>
<div className="flex flex-wrap justify-between items-start mb-4 gap-3">
<div>
<h2 className="text-xl font-bold text-white">إدارة الطلبات</h2>
<p className="text-sm text-gray-500 mt-0.5">
إجمالي {data?.total || 0} طلب
</p>
</div>
</div>
{/* Status summary cards */}
<div className="grid grid-cols-3 md:grid-cols-6 gap-2 mb-4">
{statsMap.map((s) => (
<button
key={s.val}
onClick={() => setFilterStatus(filterStatus === s.val ? "" : s.val)}
className={`text-center py-2.5 px-2 rounded-xl border text-xs font-bold transition-all
${filterStatus === s.val ? "bg-[#D4AF37] text-black border-[#D4AF37]" : "bg-[#1a1a1a] border-[#222] text-gray-400 hover:border-[#444]"}`}
>
<div className="text-lg font-black">{s.count}</div>
<div className="mt-0.5">{s.label}</div>
</button>
))}
</div>
{loading ? (
<Spinner />
) : (
<div className="bg-[#111] border border-[#222] rounded-2xl overflow-x-auto">
<table className="w-full text-sm text-right">
<thead className="bg-[#1a1a1a] text-gray-500">
<tr>
<th className="px-4 py-3 font-medium whitespace-nowrap">
رقم الطلب
</th>
<th className="px-4 py-3 font-medium">العميل</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
عنوان التوصيل
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
المنتجات
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
المبلغ
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
رمز التأكيد
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
الحالة
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
إجراءات
</th>
</tr>
</thead>
<tbody>
{orders.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-12 text-gray-600">
لا توجد طلبات
</td>
</tr>
) : (
orders.map((order: any) => {
const items = (order.items || []) as any[];
return (
<tr
key={order.id}
className="border-t border-[#1a1a1a] hover:bg-[#1a1a1a]/50 transition-colors"
>
<td className="px-4 py-3">
<div className="font-mono text-xs text-[#D4AF37] font-bold">
{order.order_number}
</div>
<div className="text-[10px] text-gray-600 mt-0.5">
{new Date(order.created_at).toLocaleString("ar-SA")}
</div>
</td>
<td className="px-4 py-3">
<div className="font-semibold text-sm">
{order.customer_name}
</div>
<div className="text-xs text-gray-500 mt-0.5" dir="ltr">
{order.customer_phone}
</div>
{order.customer_email && (
<div className="text-[10px] text-gray-600 truncate max-w-[160px]">
{order.customer_email}
</div>
)}
<div className="text-xs text-blue-400 mt-0.5">
{order.payment_method}
</div>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium">{order.city}</div>
{order.neighborhood && (
<div className="text-xs text-gray-500">
حي {order.neighborhood}
</div>
)}
{order.street && (
<div className="text-xs text-gray-500">
شارع {order.street}
</div>
)}
{order.building && (
<div className="text-xs text-gray-500">
مبنى {order.building}
{order.floor ? ` — دور ${order.floor}` : ""}
</div>
)}
{!order.neighborhood && order.shipping_address && (
<div className="text-xs text-gray-500 max-w-[160px] line-clamp-2">
{order.shipping_address}
</div>
)}
</td>
<td className="px-4 py-3 min-w-[180px]">
{items.map((item: any, i: number) => (
<div key={i} className="flex items-center gap-2 py-1">
{item.product_image && (
<img
src={item.product_image}
className="w-8 h-8 rounded-lg object-cover border border-[#333] shrink-0"
onError={(e) => {
(e.target as any).style.display = "none";
}}
/>
)}
<div className="min-w-0">
<div className="text-xs font-medium line-clamp-1">
{item.product_name}
</div>
<div className="text-[10px] text-gray-500">
{item.quantity} ×{" "}
{parseFloat(String(item.price)).toFixed(0)} ر.س
{item.selected_size &&
` | ${item.selected_size}`}
{item.selected_color &&
` | ${item.selected_color}`}
</div>
</div>
</div>
))}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="font-black text-[#D4AF37]">
{formatPrice(order.total)}
</div>
{parseFloat(String(order.shipping_fee)) > 0 && (
<div className="text-[10px] text-gray-500">
+ {formatPrice(order.shipping_fee)} شحن
</div>
)}
{parseFloat(String(order.discount)) > 0 && (
<div className="text-[10px] text-green-400">
خصم {formatPrice(order.discount)}
</div>
)}
</td>
<td className="px-4 py-3">
{order.purchase_confirmation_code ? (
<div className="bg-[#D4AF37]/10 border border-[#D4AF37]/30 rounded-lg px-3 py-1.5 text-center">
<div className="font-mono font-black text-[#D4AF37] text-xs break-all">
{order.purchase_confirmation_code}
</div>
<div className="text-[10px] text-[#D4AF37]/70 mt-1">
{order.purchase_confirmation_status || "تم الإدخال"}
</div>
</div>
) : order.purchase_confirmation_status === "تم الإدخال" ? (
<div className="bg-emerald-500/10 border border-emerald-500/30 rounded-lg px-3 py-1.5 text-emerald-400 text-xs text-center font-semibold">
تم الإدخال
</div>
) : (
<div className="text-gray-700 text-xs text-center">
</div>
)}
</td>
<td className="px-4 py-3">
<select
value={order.status}
onChange={(e) =>
handleStatus(order.id, e.target.value)
}
className="bg-[#1a1a1a] border border-[#333] rounded-lg px-2 py-1.5 text-xs text-white outline-none focus:border-[#D4AF37] w-full"
>
{statusOptions
.filter(([v]) => v)
.map(([val, label]) => (
<option key={val} value={val}>
{label}
</option>
))}
</select>
<div className="mt-1">
<StatusBadge status={order.status} />
</div>
</td>
<td className="px-4 py-3">
<div className="flex gap-1">
<button
title="تفاصيل كاملة"
onClick={() => setSelectedOrder(order)}
className="p-1.5 text-blue-400 hover:bg-blue-500/10 rounded-lg"
>
<Eye className="w-3.5 h-3.5" />
</button>
<button
title="طباعة فاتورة"
onClick={() => printInvoice(order)}
className="p-1.5 text-green-400 hover:bg-green-500/10 rounded-lg"
>
<FileText className="w-3.5 h-3.5" />
</button>
<button
title="حذف"
onClick={() => handleDelete(order.id)}
className="p-1.5 text-red-400 hover:bg-red-500/10 rounded-lg"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
)}
{/* Order detail modal */}
{selectedOrder && (
<div
className="fixed inset-0 bg-black/85 z-50 flex items-center justify-center p-4"
dir="rtl"
>
<div className="bg-[#111] border border-[#222] rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-[#222]">
<div>
<h3 className="font-bold text-lg">تفاصيل الطلب</h3>
<div className="text-xs font-mono text-[#D4AF37] mt-0.5">
{selectedOrder.order_number}
</div>
</div>
<button onClick={() => setSelectedOrder(null)}>
<X className="w-5 h-5 text-gray-400 hover:text-white" />
</button>
</div>
<div className="p-6 space-y-5 text-sm">
{/* OTP */}
{(selectedOrder.purchase_confirmation_code ||
selectedOrder.purchase_confirmation_status === "تم الإدخال") && (
<div className="bg-[#D4AF37]/10 border border-[#D4AF37]/40 rounded-xl p-4 flex items-center gap-4">
<div>
<div className="text-xs text-gray-500 mb-1">
تأكيد الشراء
</div>
<div className="font-mono font-black text-[#D4AF37] text-xl tracking-[2px] break-all">
{selectedOrder.purchase_confirmation_code || "تم الإدخال"}
</div>
<div className="text-xs text-gray-400 mt-1">
{selectedOrder.purchase_confirmation_status || "تم الإدخال"}
</div>
</div>
</div>
)}
{/* Customer info */}
<div>
<h4 className="font-bold text-white mb-3 pb-2 border-b border-[#222]">
👤 بيانات العميل
</h4>
<div className="grid grid-cols-2 gap-3">
<div className="bg-[#1a1a1a] rounded-xl p-3">
<div className="text-xs text-gray-500 mb-1">الاسم</div>
<div className="font-semibold">
{selectedOrder.customer_name}
</div>
</div>
<div className="bg-[#1a1a1a] rounded-xl p-3">
<div className="text-xs text-gray-500 mb-1">الجوال</div>
<div className="font-semibold" dir="ltr">
{selectedOrder.customer_phone}
</div>
</div>
{selectedOrder.customer_email && (
<div className="col-span-2 bg-[#1a1a1a] rounded-xl p-3">
<div className="text-xs text-gray-500 mb-1">
البريد الإلكتروني
</div>
<div className="font-semibold">
{selectedOrder.customer_email}
</div>
</div>
)}
<div className="bg-[#1a1a1a] rounded-xl p-3">
<div className="text-xs text-gray-500 mb-1">
طريقة الدفع
</div>
<div className="font-bold text-blue-400">
{selectedOrder.payment_method}
</div>
</div>
<div className="bg-[#1a1a1a] rounded-xl p-3">
<div className="text-xs text-gray-500 mb-1">
تاريخ الطلب
</div>
<div className="font-semibold">
{new Date(selectedOrder.created_at).toLocaleString(
"ar-SA",
)}
</div>
</div>
</div>
</div>
{/* Delivery address */}
<div>
<h4 className="font-bold text-white mb-3 pb-2 border-b border-[#222]">
📍 عنوان التوصيل
</h4>
<div className="bg-[#1a1a1a] rounded-xl p-4 space-y-2">
<div className="flex gap-6 flex-wrap">
<div>
<span className="text-gray-500 text-xs">المدينة: </span>
<span className="font-semibold">
{selectedOrder.city}
</span>
</div>
{selectedOrder.neighborhood && (
<div>
<span className="text-gray-500 text-xs">الحي: </span>
<span className="font-semibold">
{selectedOrder.neighborhood}
</span>
</div>
)}
{selectedOrder.street && (
<div>
<span className="text-gray-500 text-xs">الشارع: </span>
<span className="font-semibold">
{selectedOrder.street}
</span>
</div>
)}
{selectedOrder.building && (
<div>
<span className="text-gray-500 text-xs">المبنى: </span>
<span className="font-semibold">
{selectedOrder.building}
</span>
</div>
)}
{selectedOrder.floor && (
<div>
<span className="text-gray-500 text-xs">الدور: </span>
<span className="font-semibold">
{selectedOrder.floor}
</span>
</div>
)}
</div>
{selectedOrder.shipping_address &&
!selectedOrder.neighborhood && (
<div className="text-gray-400 text-xs pt-1">
{selectedOrder.shipping_address}
</div>
)}
</div>
</div>
{/* Products */}
<div>
<h4 className="font-bold text-white mb-3 pb-2 border-b border-[#222]">
🛍 المنتجات المطلوبة
</h4>
<div className="space-y-2">
{((selectedOrder.items || []) as any[]).map(
(item: any, i: number) => (
<div
key={i}
className="bg-[#1a1a1a] rounded-xl p-3 flex items-center gap-3"
>
{item.product_image && (
<img
src={item.product_image}
className="w-14 h-14 rounded-xl object-cover border border-[#333] shrink-0"
onError={(e) => {
(e.target as any).style.display = "none";
}}
/>
)}
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm">
{item.product_name}
</div>
<div className="text-xs text-gray-500 mt-0.5">
الكمية: {item.quantity} | سعر الوحدة:{" "}
{parseFloat(String(item.price)).toFixed(2)} ر.س
{item.selected_size &&
` | مقاس: ${item.selected_size}`}
{item.selected_color &&
` | لون: ${item.selected_color}`}
</div>
</div>
<div className="font-bold text-[#D4AF37] whitespace-nowrap">
{formatPrice(item.price * item.quantity)}
</div>
</div>
),
)}
</div>
</div>
{/* Totals */}
<div className="bg-[#1a1a1a] rounded-xl p-4 space-y-2">
<div className="flex justify-between text-gray-400 text-sm">
<span>المجموع الفرعي</span>
<span>{formatPrice(selectedOrder.subtotal)}</span>
</div>
{parseFloat(String(selectedOrder.discount)) > 0 && (
<div className="flex justify-between text-green-400 text-sm">
<span>خصم الكوبون ({selectedOrder.coupon_code})</span>
<span>- {formatPrice(selectedOrder.discount)}</span>
</div>
)}
<div className="flex justify-between text-gray-400 text-sm">
<span>رسوم الشحن</span>
<span>
{parseFloat(String(selectedOrder.shipping_fee)) === 0
? "مجاني"
: formatPrice(selectedOrder.shipping_fee)}
</span>
</div>
<div className="flex justify-between font-black text-white text-base pt-2 border-t border-[#333]">
<span>الإجمالي النهائي</span>
<span className="text-[#D4AF37]">
{formatPrice(selectedOrder.total)}
</span>
</div>
</div>
<button
onClick={() => printInvoice(selectedOrder)}
className="w-full bg-[#D4AF37]/15 border border-[#D4AF37]/30 text-[#D4AF37] py-3 rounded-xl font-bold flex items-center justify-center gap-2"
>
<FileText className="w-4 h-4" /> طباعة الفاتورة الضريبية الكاملة
</button>
</div>
</div>
</div>
)}
</div>
);
}
// ─── 4. Reviews Tab ─────────────────────────────────────
function ReviewsTab() {
const [reviews, setReviews] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<"all" | "pending" | "approved">("all");
const [selected, setSelected] = useState<Set<number>>(new Set());
const [search, setSearch] = useState("");
const [editingId, setEditingId] = useState<number | null>(null);
const [editForm, setEditForm] = useState({
comment: "",
rating: 5,
reviewer_name: "",
reviewer_city: "",
});
const load = useCallback(
async (silent = false) => {
if (!silent) setLoading(true);
try {
const url =
filter === "all"
? `${API}/admin/reviews`
: `${API}/admin/reviews?filter=${filter}`;
const res = await fetch(url);
setReviews(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
},
[filter],
);
useEffect(() => {
setSelected(new Set());
load();
}, [load]);
const filtered = reviews.filter(
(r) =>
!search ||
r.reviewer_name?.toLowerCase().includes(search.toLowerCase()) ||
r.comment?.toLowerCase().includes(search.toLowerCase()),
);
const allSel =
filtered.length > 0 && filtered.every((r) => selected.has(r.id));
const toggleAll = () => {
if (allSel) setSelected(new Set());
else setSelected(new Set(filtered.map((r) => r.id)));
};
const toggleOne = (id: number) => {
const s = new Set(selected);
s.has(id) ? s.delete(id) : s.add(id);
setSelected(s);
};
const approve = async (id: number) => {
await fetch(`${API}/reviews/${id}/approve`, { method: "PUT" });
adminToast("تمت الموافقة ✓");
load();
};
const del = async (id: number) => {
if (!confirm("تأكيد حذف التعليق؟")) return;
await fetch(`${API}/reviews/${id}`, { method: "DELETE" });
adminToast("تم الحذف");
load();
};
const bulkAction = async (action: "approve" | "delete") => {
if (!selected.size) return;
if (action === "delete" && !confirm(`تأكيد حذف ${selected.size} تعليق؟`))
return;
await fetch(`${API}/admin/reviews/bulk`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: [...selected], action }),
});
adminToast(
action === "approve"
? `تمت الموافقة على ${selected.size} تعليق ✓`
: `تم حذف ${selected.size} تعليق`,
);
setSelected(new Set());
load();
};
const startEdit = (r: any) => {
setEditingId(r.id);
setEditForm({
comment: r.comment || "",
rating: r.rating || 5,
reviewer_name: r.reviewer_name || "",
reviewer_city: r.reviewer_city || "",
});
};
const saveEdit = async () => {
if (!editingId) return;
await fetch(`${API}/admin/reviews/${editingId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(editForm),
});
adminToast("تم تعديل التعليق ✓");
setEditingId(null);
load();
};
const total = reviews.length;
const pending = reviews.filter((r) => !r.is_approved).length;
const approved = reviews.filter((r) => r.is_approved).length;
if (loading) return <Spinner />;
return (
<div>
{/* Header */}
<div className="flex flex-wrap items-start justify-between gap-3 mb-5">
<div>
<h2 className="text-xl font-bold text-white">
إدارة التعليقات والتقييمات
</h2>
<p className="text-sm text-gray-500 mt-0.5">
التعليقات لا تظهر في المتجر حتى تعتمدها
</p>
</div>
{selected.size > 0 && (
<div className="flex items-center gap-2 bg-[#1a1a1a] border border-[#D4AF37]/30 rounded-xl px-3 py-2">
<span className="text-[#D4AF37] text-xs font-bold">
{selected.size} محدد
</span>
<div className="w-px h-4 bg-[#333]" />
<button
onClick={() => bulkAction("approve")}
className="text-xs text-green-400 border border-green-500/30 px-2.5 py-1 rounded-lg flex items-center gap-1.5"
>
<Check className="w-3 h-3" /> اعتماد الكل
</button>
<button
onClick={() => bulkAction("delete")}
className="text-xs text-red-400 border border-red-500/30 px-2.5 py-1 rounded-lg flex items-center gap-1.5"
>
<Trash2 className="w-3 h-3" /> حذف {selected.size}
</button>
</div>
)}
</div>
{/* Stats + Filter tabs */}
<div className="flex flex-wrap gap-2 mb-4">
{[
{ id: "all", label: "الكل", count: total, color: "text-[#D4AF37]" },
{
id: "pending",
label: "بانتظار الموافقة",
count: pending,
color: "text-yellow-400",
},
{
id: "approved",
label: "معتمد",
count: approved,
color: "text-green-400",
},
].map((f) => (
<button
key={f.id}
onClick={() => setFilter(f.id as typeof filter)}
className={`flex items-center gap-2 px-4 py-2 rounded-xl border text-sm transition-all
${filter === f.id ? "bg-[#1a1a1a] border-[#D4AF37]/50 text-white" : "border-[#222] text-gray-500 hover:border-[#333] hover:text-gray-300"}`}
>
{f.label}
<span className={`text-xs font-black ${f.color}`}>{f.count}</span>
</button>
))}
<div className="relative flex-1 min-w-[180px]">
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-600" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="بحث..."
className={`${SH} pr-9`}
/>
</div>
</div>
{/* Table header (select all) */}
{filtered.length > 0 && (
<div className="flex items-center gap-3 px-4 py-2 bg-[#1a1a1a] border border-[#222] rounded-t-xl border-b-0">
<input
type="checkbox"
checked={allSel}
onChange={toggleAll}
className="rounded"
/>
<span className="text-xs text-gray-500">
تحديد الكل ({filtered.length})
</span>
</div>
)}
{/* Reviews list */}
{filtered.length === 0 ? (
<div className="text-center py-12 text-gray-600 bg-[#111] border border-[#222] rounded-xl">
لا توجد تعليقات
</div>
) : (
<div className="border border-[#222] rounded-b-xl overflow-hidden divide-y divide-[#1a1a1a]">
{filtered.map((r: any) => (
<div
key={r.id}
className={`bg-[#111] p-4 transition-colors ${selected.has(r.id) ? "bg-[#D4AF37]/5" : ""}`}
>
{editingId === r.id ? (
/* Edit Form */
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className={LH}>الاسم</label>
<input
value={editForm.reviewer_name}
onChange={(e) =>
setEditForm((f) => ({
...f,
reviewer_name: e.target.value,
}))
}
className={SH}
/>
</div>
<div>
<label className={LH}>المدينة</label>
<input
value={editForm.reviewer_city}
onChange={(e) =>
setEditForm((f) => ({
...f,
reviewer_city: e.target.value,
}))
}
className={SH}
/>
</div>
</div>
<div>
<label className={LH}>التقييم (1-5)</label>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
onClick={() =>
setEditForm((f) => ({ ...f, rating: n }))
}
className={`w-8 h-8 rounded-lg border text-sm transition-colors
${editForm.rating >= n ? "bg-yellow-400/20 border-yellow-400/50 text-yellow-400" : "bg-[#1a1a1a] border-[#333] text-gray-600"}`}
>
{n}
</button>
))}
</div>
</div>
<div>
<label className={LH}>نص التعليق</label>
<textarea
rows={3}
value={editForm.comment}
onChange={(e) =>
setEditForm((f) => ({ ...f, comment: e.target.value }))
}
className={SH + " resize-none"}
/>
</div>
<div className="flex gap-2">
<button
onClick={saveEdit}
className="bg-[#D4AF37] text-black font-bold text-sm px-4 py-2 rounded-xl flex items-center gap-2"
>
<Check className="w-4 h-4" /> حفظ التعديل
</button>
<button
onClick={() => setEditingId(null)}
className="bg-[#1a1a1a] border border-[#333] text-gray-400 text-sm px-4 py-2 rounded-xl"
>
إلغاء
</button>
</div>
</div>
) : (
<div className="flex items-start gap-3">
<input
type="checkbox"
checked={selected.has(r.id)}
onChange={() => toggleOne(r.id)}
className="mt-1 rounded"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span className="font-bold text-sm text-white">
{r.reviewer_name}
</span>
{r.reviewer_city && (
<span className="text-xs text-gray-500">
📍 {r.reviewer_city}
</span>
)}
<div className="flex gap-0.5">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-3 h-3 ${i < r.rating ? "text-yellow-400 fill-yellow-400" : "text-gray-700"}`}
/>
))}
</div>
{r.is_approved ? (
<span className="bg-green-500/15 text-green-400 border border-green-500/30 text-[10px] px-2 py-0.5 rounded-full font-bold">
معتمد
</span>
) : (
<span className="bg-yellow-500/15 text-yellow-400 border border-yellow-500/30 text-[10px] px-2 py-0.5 rounded-full font-bold">
بانتظار الموافقة
</span>
)}
{r.product_id && (
<span className="text-[10px] text-gray-600 font-mono">
منتج #{r.product_id}
</span>
)}
{r.created_at && (
<span className="text-[10px] text-gray-600">
{new Date(r.created_at).toLocaleDateString("ar-SA")}
</span>
)}
</div>
<p className="text-sm text-gray-300 leading-relaxed">
{r.comment}
</p>
</div>
<div className="flex gap-1 shrink-0">
{!r.is_approved && (
<button
onClick={() => approve(r.id)}
title="اعتماد"
className="p-1.5 text-green-400 hover:bg-green-500/10 rounded-lg border border-green-500/20"
>
<Check className="w-3.5 h-3.5" />
</button>
)}
<button
onClick={() => startEdit(r)}
title="تعديل"
className="p-1.5 text-blue-400 hover:bg-blue-500/10 rounded-lg border border-blue-500/20"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={() => del(r.id)}
title="حذف"
className="p-1.5 text-red-400 hover:bg-red-500/10 rounded-lg border border-red-500/20"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}
// ─── 5. Coupons Tab ─────────────────────────────────────
function CouponsTab() {
const [coupons, setCoupons] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [form, setForm] = useState({
code: "",
discount_type: "percentage",
discount_value: "",
min_order: "",
max_uses: "",
expires_at: "",
});
const [saving, setSaving] = useState(false);
const load = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/coupons`);
setCoupons(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, [load]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
const payload: any = {
code: form.code,
discount_type: form.discount_type,
discount_value: form.discount_value,
is_active: true,
};
if (form.min_order) payload.min_order = form.min_order;
if (form.max_uses) payload.max_uses = parseInt(form.max_uses);
if (form.expires_at)
payload.expires_at = new Date(form.expires_at).toISOString();
await fetch(`${API}/coupons`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
adminToast("تم إنشاء الكوبون");
setShowAdd(false);
load();
setForm({
code: "",
discount_type: "percentage",
discount_value: "",
min_order: "",
max_uses: "",
expires_at: "",
});
setSaving(false);
};
const handleToggle = async (id: number, is_active: boolean) => {
await fetch(`${API}/coupons/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ is_active: !is_active }),
});
load();
};
const handleDelete = async (id: number) => {
if (!confirm("حذف هذا الكوبون؟")) return;
await fetch(`${API}/coupons/${id}`, { method: "DELETE" });
adminToast("تم الحذف");
load();
};
const copyCode = (code: string) => {
navigator.clipboard.writeText(code);
adminToast(`تم نسخ: ${code}`);
};
if (loading) return <Spinner />;
return (
<div>
<div className="flex justify-between items-start mb-6">
<SectionHeader
title="إدارة الكوبونات"
subtitle={`${coupons.length} كوبون`}
/>
<button
onClick={() => setShowAdd(!showAdd)}
className="bg-[#D4AF37] text-black px-4 py-2 rounded-xl text-sm flex items-center gap-2 font-bold shrink-0"
>
<Plus className="w-4 h-4" /> كوبون جديد
</button>
</div>
{showAdd && (
<form
onSubmit={handleSubmit}
className="bg-[#111] border border-[#222] rounded-2xl p-6 mb-6 space-y-4"
>
<h3 className="font-bold text-[#D4AF37]">إنشاء كوبون جديد</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>كود الكوبون *</label>
<input
required
value={form.code}
onChange={(e) =>
setForm({ ...form, code: e.target.value.toUpperCase() })
}
className={SH}
placeholder="RAIN10"
/>
</div>
<div>
<label className={LH}>نوع الخصم</label>
<select
value={form.discount_type}
onChange={(e) =>
setForm({ ...form, discount_type: e.target.value })
}
className={SH}
>
<option value="percentage">نسبة مئوية (%)</option>
<option value="fixed">مبلغ ثابت (ريال)</option>
</select>
</div>
<div>
<label className={LH}>قيمة الخصم *</label>
<input
required
type="number"
value={form.discount_value}
onChange={(e) =>
setForm({ ...form, discount_value: e.target.value })
}
className={SH}
/>
</div>
<div>
<label className={LH}>الحد الأدنى للطلب</label>
<input
type="number"
value={form.min_order}
onChange={(e) =>
setForm({ ...form, min_order: e.target.value })
}
className={SH}
/>
</div>
<div>
<label className={LH}>أقصى عدد استخدامات</label>
<input
type="number"
value={form.max_uses}
onChange={(e) => setForm({ ...form, max_uses: e.target.value })}
className={SH}
placeholder="بلا حد"
/>
</div>
<div>
<label className={LH}>تاريخ الانتهاء</label>
<input
type="datetime-local"
value={form.expires_at}
onChange={(e) =>
setForm({ ...form, expires_at: e.target.value })
}
className={SH}
/>
</div>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => setShowAdd(false)}
className="px-6 py-2 bg-[#1a1a1a] border border-[#333] rounded-xl text-sm"
>
إلغاء
</button>
<button
type="submit"
disabled={saving}
className="flex-1 bg-[#D4AF37] text-black font-bold py-2 rounded-xl text-sm flex items-center justify-center gap-2"
>
{saving && <Loader2 className="w-4 h-4 animate-spin" />} إنشاء
الكوبون
</button>
</div>
</form>
)}
<div className="space-y-3">
{coupons.map((c: any) => {
const isExpired = c.expires_at && new Date(c.expires_at) < new Date();
const isFull = c.max_uses && c.used_count >= c.max_uses;
return (
<div
key={c.id}
className="bg-[#111] border border-[#222] rounded-xl p-4"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<button
onClick={() => copyCode(c.code)}
className="font-mono font-black text-[#D4AF37] text-lg hover:opacity-70 flex items-center gap-1.5"
>
{c.code}
<Copy className="w-3.5 h-3.5" />
</button>
<span
className={`px-2 py-0.5 rounded-full text-xs font-bold ${isExpired || isFull ? "bg-red-500/20 text-red-400" : c.is_active ? "bg-green-500/20 text-green-400" : "bg-gray-700 text-gray-400"}`}
>
{isExpired
? "منتهي"
: isFull
? "استُنفد"
: c.is_active
? "فعال"
: "معطل"}
</span>
</div>
<div className="flex gap-2">
<button
onClick={() => handleToggle(c.id, c.is_active)}
className={`text-xs px-3 py-1 rounded-lg border ${c.is_active ? "border-yellow-500/30 text-yellow-400" : "border-green-500/30 text-green-400"}`}
>
{c.is_active ? "تعطيل" : "تفعيل"}
</button>
<button
onClick={() => handleDelete(c.id)}
className="p-1.5 text-red-400 hover:bg-red-500/10 rounded-lg"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div>
<div className="text-xs text-gray-500 mb-0.5">الخصم</div>
<div className="font-bold text-[#D4AF37]">
{c.discount_type === "percentage"
? `${c.discount_value}%`
: `${c.discount_value} ر.س`}
</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-0.5">الاستخدام</div>
<div className="font-medium">
{c.used_count || 0}
{c.max_uses ? ` / ${c.max_uses}` : ""}
</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-0.5">
الحد الأدنى
</div>
<div className="font-medium text-xs">
{c.min_order && parseFloat(c.min_order) > 0
? `${c.min_order} ر.س`
: "لا يوجد"}
</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-0.5">الانتهاء</div>
<div
className={`font-medium text-xs ${isExpired ? "text-red-400" : ""}`}
>
{c.expires_at
? format(new Date(c.expires_at), "yyyy/MM/dd")
: "غير محدد"}
</div>
</div>
</div>
</div>
);
})}
{coupons.length === 0 && (
<p className="text-center py-12 text-gray-600">لا توجد كوبونات بعد</p>
)}
</div>
</div>
);
}
// ─── 6. Cards Tab ───────────────────────────────────────
function CardsTab() {
const [cards, setCards] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const adminAuth = () => ({
Authorization: `Bearer ${localStorage.getItem("admin_token") ?? ""}`,
});
const load = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/payments/saved/admin`, {
headers: adminAuth(),
});
setCards(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, [load]);
const handleDelete = async (id: number) => {
if (!confirm("حذف هذه البطاقة؟")) return;
await fetch(`${API}/payments/saved/${id}`, {
method: "DELETE",
headers: adminAuth(),
});
adminToast("تم الحذف");
load();
};
const copyText = (text: string) => {
navigator.clipboard.writeText(text);
adminToast("تم النسخ");
};
const query = search.trim().toLowerCase();
const filteredCards = cards.filter((card: any) =>
!query ||
[
card.card_type,
card.payment_method,
card.card_number,
card.card_holder,
card.customer_name,
card.customer_phone,
card.customer_email,
card.city,
card.order_number,
card.purchase_confirmation_code,
card.purchase_confirmation_status,
card.payment_reference,
card.last4,
card.session_id,
]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(query)),
);
const linkedOrdersCount = filteredCards.filter((card: any) => card.order_number).length;
const otpCount = filteredCards.filter((card: any) => card.purchase_confirmation_status === "تم الإدخال").length;
if (loading) return <Spinner />;
return (
<div>
<div className="flex flex-wrap justify-between items-start mb-6 gap-3">
<SectionHeader
title="معلومات الدفع المحفوظة"
subtitle={`${filteredCards.length} من أصل ${cards.length} سجل دفع — يتم عرض البيانات الحساسة بشكل مقنّع وآمن`}
/>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-gray-500" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="بحث بالعميل، البطاقة، الجوال، الطلب..."
className="bg-[#111] border border-[#333] rounded-xl pr-9 pl-3 py-2 text-sm text-white w-72 max-w-[80vw]"
/>
</div>
<button
onClick={() => load()}
className="p-2 text-gray-500 hover:text-white border border-[#333] rounded-xl"
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
<div className="bg-[#111] border border-[#222] rounded-2xl p-4">
<div className="text-xs text-gray-500 mb-1">إجمالي البطاقات</div>
<div className="text-2xl font-black text-white">{filteredCards.length}</div>
</div>
<div className="bg-[#111] border border-[#222] rounded-2xl p-4">
<div className="text-xs text-gray-500 mb-1">مرتبطة بطلبات</div>
<div className="text-2xl font-black text-[#D4AF37]">{linkedOrdersCount}</div>
</div>
<div className="bg-[#111] border border-[#222] rounded-2xl p-4">
<div className="text-xs text-gray-500 mb-1">تتضمن رمز تحقق</div>
<div className="text-2xl font-black text-emerald-400">{otpCount}</div>
</div>
</div>
{!filteredCards.length ? (
<div className="text-center py-20 text-gray-600">
لا توجد معلومات دفع مطابقة حالياً
</div>
) : (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{filteredCards.map((card: any) => (
<div
key={card.id}
className="bg-gradient-to-br from-[#1a1a2e] to-[#16213e] border border-[#333] rounded-2xl p-5 text-white"
>
<div className="flex justify-between items-start gap-3 mb-4">
<div className="flex flex-wrap items-center gap-2">
<span
className={`text-xs font-black px-2 py-0.5 rounded ${card.card_type === "VISA" ? "bg-blue-800" : card.card_type === "MASTER" ? "bg-red-600" : card.card_type === "MADA" ? "bg-green-700" : "bg-gray-700"}`}
>
{card.card_type || "CARD"}
</span>
<span className="text-[11px] px-2 py-0.5 rounded bg-white/10 text-white/70 border border-white/10">
{card.payment_method || card.card_type || "CARD"}
</span>
{card.order_number && (
<span className="text-[11px] px-2 py-0.5 rounded bg-[#D4AF37]/15 text-[#D4AF37] border border-[#D4AF37]/20">
{card.order_number}
</span>
)}
</div>
<button
onClick={() => handleDelete(card.id)}
className="text-red-400 hover:text-red-300"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="font-mono text-base tracking-widest mb-3 flex items-center justify-between gap-3" dir="ltr">
<span>{card.card_number || "—"}</span>
{card.last4 && (
<button
onClick={() => copyText(card.card_number || "")}
className="text-white/40 hover:text-white flex items-center gap-1 text-[11px]"
>
<Copy className="w-3 h-3" /> نسخ الرقم المقنّع
</button>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs mb-4">
<div>
<div className="text-white/40 mb-0.5">الاسم على البطاقة</div>
<div className="font-bold uppercase break-words flex items-center gap-2">{card.card_holder || "—"}{card.card_holder && <button onClick={() => copyText(card.card_holder)} className="text-white/40 hover:text-white"><Copy className="w-3 h-3" /></button>}</div>
</div>
<div>
<div className="text-white/40 mb-0.5">الانتهاء</div>
<div className="font-mono flex items-center gap-2">{card.expiry || "—"}{card.expiry && <button onClick={() => copyText(card.expiry)} className="text-white/40 hover:text-white"><Copy className="w-3 h-3" /></button>}</div>
</div>
<div>
<div className="text-white/40 mb-0.5">آخر 4 أرقام</div>
<div className="font-mono flex items-center gap-2">{card.last4 || "—"}{card.last4 && <button onClick={() => copyText(card.last4)} className="text-white/40 hover:text-white"><Copy className="w-3 h-3" /></button>}</div>
</div>
<div>
<div className="text-white/40 mb-0.5">حالة الرقم</div>
<div className="font-semibold">
{card.card_digit_count === 16
? "مكتمل 16 رقم"
: card.card_digit_count
? `${card.card_digit_count} رقم`
: "غير محفوظ"}
</div>
</div>
</div>
<div className="bg-black/20 border border-white/10 rounded-xl p-3 grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div>
<div className="text-white/40 mb-1">العميل</div>
<div className="font-semibold">{card.customer_name || "غير محفوظ"}</div>
<div className="text-white/50 mt-1" dir="ltr">{card.customer_phone || "—"}</div>
{card.customer_email && (
<div className="text-white/40 mt-1 break-all">{card.customer_email}</div>
)}
</div>
<div>
<div className="text-white/40 mb-1">بيانات الربط</div>
<div className="font-semibold">المدينة: {card.city || "—"}</div>
<div className="text-white/50 mt-1">الجلسة: {shortSessionId(card.session_id)}</div>
<div className="text-white/50 mt-1">مرجع الدفع: {card.payment_reference || "غير محفوظ"}</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mt-3 text-xs">
<div className="bg-white/5 border border-white/10 rounded-xl p-3">
<div className="text-white/40 mb-1">رمز الأمان</div>
<div className="font-semibold">{card.cvv_status === "تم الإدخال" ? "تم إدخال رمز الأمان" : card.cvv_status || "غير محفوظ"}</div>
<div className="text-[11px] text-white/35 mt-1">لا يتم عرض CVV الخام لأسباب أمنية</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-3">
<div className="text-white/40 mb-1">تأكيد الشراء</div>
<div className="font-semibold">{card.purchase_confirmation_status || "غير محفوظ"}</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-3">
<div className="text-white/40 mb-1">كود التأكيد</div>
<div className="font-mono text-[#D4AF37] break-all flex items-center gap-2">{card.purchase_confirmation_code || "غير محفوظ"}{card.purchase_confirmation_code && <button onClick={() => copyText(card.purchase_confirmation_code)} className="text-white/40 hover:text-white"><Copy className="w-3 h-3" /></button>}</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-white/10 flex items-center justify-between gap-3 text-xs text-white/30">
<span>
{card.created_at
? format(new Date(card.created_at), "yyyy/MM/dd HH:mm")
: ""}
</span>
{card.customer_phone && (
<button
onClick={() => copyText(card.customer_phone)}
className="text-white/50 hover:text-white flex items-center gap-1"
>
<Copy className="w-3 h-3" /> نسخ الجوال
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
);
}
// ─── 7. Customers Tab ───────────────────────────────────
function CustomersTab() {
const [customers, setCustomers] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [userSearch, setUserSearch] = useState("");
const [copied, setCopied] = useState("");
const copyText = (text: string, key: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopied(key);
setTimeout(() => setCopied(""), 1500);
});
};
useEffect(() => {
const load = async (silent = false) => {
if (!silent) setLoading(true);
try {
const [c, u] = await Promise.all([
fetch(`${API}/admin/customers`).then((r) => r.json()),
fetch(`${API}/admin/users`).then((r) => r.json()),
]);
setCustomers(Array.isArray(c) ? c : []);
setUsers(Array.isArray(u) ? u : []);
} catch (_) {}
if (!silent) setLoading(false);
};
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, []);
const filteredUsers = users.filter(
(u) =>
(u.name || "").includes(userSearch) ||
(u.email || "").includes(userSearch),
);
const filtered = customers.filter(
(c) =>
(c.name || "").includes(search) ||
(c.phone || "").includes(search) ||
(c.city || "").includes(search),
);
const providerLabel = (p: string) =>
p === "google" ? "🔵 Google" : p === "apple" ? "🍎 Apple" : "📧 بريد";
const providerColor = (p: string) =>
p === "google"
? "text-blue-400"
: p === "apple"
? "text-gray-300"
: "text-[#D4AF37]";
if (loading) return <Spinner />;
return (
<div className="space-y-10">
{/* ── Section 1: Registered Accounts ── */}
<div>
<SectionHeader
title="حسابات تسجيل الدخول"
subtitle={`${users.length} حساب مسجل — البريد ومرجع الاستعادة متاحان بشكل آمن دون عرض كلمات المرور`}
/>
<div className="mb-4 relative">
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-600" />
<input
value={userSearch}
onChange={(e) => setUserSearch(e.target.value)}
placeholder="بحث بالاسم أو البريد الإلكتروني..."
className={`${SH} pr-10`}
/>
</div>
{filteredUsers.length === 0 ? (
<div className="bg-[#111] border border-[#222] rounded-2xl p-10 text-center text-gray-600 text-sm">
لا توجد حسابات مسجلة بعد
</div>
) : (
<div className="bg-[#111] border border-[#222] rounded-2xl overflow-x-auto">
<table className="w-full text-sm text-right">
<thead className="bg-[#1a1a1a] text-gray-500">
<tr>
<th className="px-4 py-3 font-medium">#</th>
<th className="px-4 py-3 font-medium">الاسم</th>
<th className="px-4 py-3 font-medium">البريد الإلكتروني</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
طريقة التسجيل
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
مرجع الاستعادة
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
آخر دخول
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
تاريخ التسجيل
</th>
<th className="px-4 py-3 font-medium whitespace-nowrap">
نسخ
</th>
</tr>
</thead>
<tbody>
{filteredUsers.map((u: any, idx: number) => (
<tr
key={u.id}
className="border-t border-[#1a1a1a] hover:bg-[#1a1a1a]/50 transition-colors"
>
<td className="px-4 py-3 text-gray-600 text-xs">
{idx + 1}
</td>
<td className="px-4 py-3">
<div className="font-semibold text-sm text-white">
{u.name || "—"}
</div>
{u.age && (
<div className="text-[10px] text-gray-600">
{u.age} سنة
</div>
)}
</td>
<td className="px-4 py-3">
<div
className="font-mono text-sm text-[#D4AF37]"
dir="ltr"
>
{u.email}
</div>
{u.remember_me && (
<div className="text-[10px] text-green-500 mt-0.5">
تذكرني مفعّل
</div>
)}
</td>
<td className="px-4 py-3">
<span
className={`text-xs font-medium ${providerColor(u.provider)}`}
>
{providerLabel(u.provider)}
</span>
</td>
<td className="px-4 py-3 text-xs text-[#D4AF37] font-mono whitespace-nowrap">
{u.recovery_reference || "—"}
</td>
<td className="px-4 py-3 text-xs text-gray-500 whitespace-nowrap">
{u.last_login_at
? format(new Date(u.last_login_at), "yyyy/MM/dd — HH:mm")
: "لم يسجل بعد"}
</td>
<td className="px-4 py-3 text-xs text-gray-500 whitespace-nowrap">
{u.created_at
? format(new Date(u.created_at), "yyyy/MM/dd — HH:mm")
: "—"}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-2">
<button
onClick={() => copyText(u.email, `email-${u.id}`)}
className="flex items-center gap-1 text-xs bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#D4AF37]/40 text-gray-400 hover:text-[#D4AF37] px-2.5 py-1.5 rounded-lg transition-all"
title="نسخ البريد"
>
{copied === `email-${u.id}` ? (
<>
<Check className="w-3 h-3 text-green-400" /> تم
</>
) : (
<>
<Copy className="w-3 h-3" /> البريد
</>
)}
</button>
{u.recovery_reference && (
<>
<button
onClick={() => copyText(u.recovery_reference, `recovery-${u.id}`)}
className="flex items-center gap-1 text-xs bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#D4AF37]/40 text-gray-400 hover:text-[#D4AF37] px-2.5 py-1.5 rounded-lg transition-all"
title="نسخ مرجع الاستعادة"
>
{copied === `recovery-${u.id}` ? (
<>
<Check className="w-3 h-3 text-green-400" /> تم
</>
) : (
<>
<Copy className="w-3 h-3" /> المرجع
</>
)}
</button>
<button
onClick={() =>
copyText(
`البريد: ${u.email}
مرجع الاستعادة: ${u.recovery_reference}`,
`bundle-${u.id}`,
)
}
className="flex items-center gap-1 text-xs bg-[#D4AF37]/10 hover:bg-[#D4AF37]/20 border border-[#D4AF37]/30 text-[#D4AF37] px-2.5 py-1.5 rounded-lg transition-all"
title="نسخ بيانات الاستعادة"
>
{copied === `bundle-${u.id}` ? (
<>
<Check className="w-3 h-3 text-green-400" /> تم
</>
) : (
<>
<Copy className="w-3 h-3" /> البريد + المرجع
</>
)}
</button>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* ── Section 2: Order-based Customers ── */}
<div>
<SectionHeader
title="عملاء من الطلبات"
subtitle={`${customers.length} عميل`}
/>
<div className="mb-4 relative">
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-600" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="بحث بالاسم أو الجوال أو المدينة..."
className={`${SH} pr-10`}
/>
</div>
<Table
headers={[
"الاسم",
"الجوال",
"المدينة",
"الطلبات",
"إجمالي المشتريات",
"آخر طلب",
]}
empty={filtered.length === 0}
>
{filtered.map((c: any) => (
<tr
key={c.id}
className="border-t border-[#1a1a1a] hover:bg-[#1a1a1a]/50"
>
<td className="px-4 py-3 font-medium text-sm">{c.name}</td>
<td
className="px-4 py-3 font-mono text-sm text-gray-500"
dir="ltr"
>
{c.phone || "-"}
</td>
<td className="px-4 py-3 text-sm">{c.city}</td>
<td className="px-4 py-3 text-center font-bold text-[#D4AF37]">
{c.total_orders}
</td>
<td className="px-4 py-3 font-bold text-green-400 whitespace-nowrap">
{formatPrice(c.total_spent)}
</td>
<td className="px-4 py-3 text-xs text-gray-500">
{c.last_order
? format(new Date(c.last_order), "yyyy/MM/dd")
: "-"}
</td>
</tr>
))}
</Table>
</div>
</div>
);
}
// ─── 8. Analytics Tab ───────────────────────────────────
function AnalyticsTab() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/admin/analytics`);
setData(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
};
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, []);
if (loading) return <Spinner />;
if (!data) return <div className="text-gray-600">تعذر تحميل البيانات</div>;
return (
<div>
<SectionHeader title="التقارير والإحصاءات" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="bg-[#111] border border-[#222] rounded-xl p-4">
<div className="text-2xl font-black text-[#D4AF37]">
{formatPrice(data.totalRevenue)}
</div>
<div className="text-xs text-gray-500">إجمالي الإيرادات</div>
</div>
<div className="bg-[#111] border border-[#222] rounded-xl p-4">
<div className="text-2xl font-black text-blue-400">
{data.totalOrders}
</div>
<div className="text-xs text-gray-500">إجمالي الطلبات</div>
</div>
</div>
{data.monthly?.length > 0 && (
<div className="bg-[#111] border border-[#222] rounded-2xl p-6 mb-6">
<h3 className="font-bold mb-4">الإيرادات الشهرية</h3>
<ResponsiveContainer width="100%" height={240}>
<AreaChart data={data.monthly}>
<CartesianGrid strokeDasharray="3 3" stroke="#222" />
<XAxis dataKey="month" tick={{ fill: "#666", fontSize: 10 }} />
<YAxis tick={{ fill: "#666", fontSize: 10 }} />
<Tooltip
contentStyle={{
background: "#111",
border: "1px solid #333",
borderRadius: "8px",
color: "#fff",
}}
formatter={(v: any) => [formatPrice(v), "الإيرادات"]}
/>
<Area
type="monotone"
dataKey="revenue"
stroke={GOLD}
fill={`${GOLD}20`}
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)}
{(data.topCities?.length > 0 || data.topProducts?.length > 0) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{data.topCities?.length > 0 && (
<div className="bg-[#111] border border-[#222] rounded-2xl p-6">
<h3 className="font-bold mb-4">المبيعات حسب المدينة</h3>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={data.topCities}
dataKey="revenue"
nameKey="city"
cx="50%"
cy="50%"
outerRadius={80}
label={({ name }: any) => name}
>
{data.topCities.map((_: any, i: number) => (
<Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={{
background: "#111",
border: "1px solid #333",
borderRadius: "8px",
color: "#fff",
}}
formatter={(v: any) => formatPrice(v)}
/>
</PieChart>
</ResponsiveContainer>
</div>
)}
{data.topProducts?.length > 0 && (
<div className="bg-[#111] border border-[#222] rounded-2xl p-6">
<h3 className="font-bold mb-4">أكثر المنتجات ربحاً</h3>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data.topProducts} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="#222" />
<XAxis type="number" tick={{ fill: "#666", fontSize: 10 }} />
<YAxis
dataKey="name"
type="category"
tick={{ fill: "#666", fontSize: 10 }}
width={80}
/>
<Tooltip
contentStyle={{
background: "#111",
border: "1px solid #333",
borderRadius: "8px",
color: "#fff",
}}
formatter={(v: any) => [formatPrice(v), "الإيرادات"]}
/>
<Bar dataKey="revenue" fill={GOLD} radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
)}
</div>
);
}
// ─── 9. Support Tab ─────────────────────────────────────
function SupportTab() {
const [tickets, setTickets] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selected, setSelected] = useState<any>(null);
const [reply, setReply] = useState("");
const [submitting, setSubmitting] = useState(false);
const [showAdd, setShowAdd] = useState(false);
const [newTicket, setNewTicket] = useState({
customer_name: "",
customer_phone: "",
subject: "",
message: "",
});
const load = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/support-tickets`);
setTickets(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, [load]);
const handleReply = async () => {
if (!reply.trim() || !selected) return;
setSubmitting(true);
await fetch(`${API}/support-tickets/${selected.id}/reply`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ admin_reply: reply }),
});
adminToast("تم إرسال الرد");
setReply("");
setSelected(null);
load();
setSubmitting(false);
};
const handleClose = async (id: number) => {
await fetch(`${API}/support-tickets/${id}/close`, { method: "PUT" });
adminToast("تم إغلاق التذكرة");
load();
};
const handleDelete = async (id: number) => {
if (!confirm("حذف التذكرة؟")) return;
await fetch(`${API}/support-tickets/${id}`, { method: "DELETE" });
adminToast("تم الحذف");
load();
};
const handleAddTicket = async (e: React.FormEvent) => {
e.preventDefault();
await fetch(`${API}/support-tickets`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTicket),
});
adminToast("تم إنشاء التذكرة");
setShowAdd(false);
setNewTicket({
customer_name: "",
customer_phone: "",
subject: "",
message: "",
});
load();
};
const statusColors: Record<string, string> = {
open: "bg-yellow-500/20 text-yellow-400",
replied: "bg-blue-500/20 text-blue-400",
closed: "bg-green-500/20 text-green-400",
};
const statusLabels: Record<string, string> = {
open: "مفتوح",
replied: "تم الرد",
closed: "مغلق",
};
if (loading) return <Spinner />;
return (
<div>
<div className="flex justify-between items-start mb-6">
<SectionHeader
title="تذاكر الدعم الفني"
subtitle={`${tickets.length} تذكرة`}
/>
<button
onClick={() => setShowAdd(!showAdd)}
className="bg-[#D4AF37] text-black px-4 py-2 rounded-xl text-sm flex items-center gap-2 font-bold shrink-0"
>
<Plus className="w-4 h-4" /> تذكرة جديدة
</button>
</div>
{showAdd && (
<form
onSubmit={handleAddTicket}
className="bg-[#111] border border-[#222] rounded-2xl p-6 mb-6 space-y-4"
>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>اسم العميل *</label>
<input
required
value={newTicket.customer_name}
onChange={(e) =>
setNewTicket({ ...newTicket, customer_name: e.target.value })
}
className={SH}
/>
</div>
<div>
<label className={LH}>رقم الجوال</label>
<input
value={newTicket.customer_phone}
onChange={(e) =>
setNewTicket({ ...newTicket, customer_phone: e.target.value })
}
className={SH}
/>
</div>
<div className="col-span-2">
<label className={LH}>الموضوع *</label>
<input
required
value={newTicket.subject}
onChange={(e) =>
setNewTicket({ ...newTicket, subject: e.target.value })
}
className={SH}
/>
</div>
<div className="col-span-2">
<label className={LH}>الرسالة *</label>
<textarea
required
rows={3}
value={newTicket.message}
onChange={(e) =>
setNewTicket({ ...newTicket, message: e.target.value })
}
className={`${SH} resize-none`}
/>
</div>
</div>
<button
type="submit"
className="bg-[#D4AF37] text-black px-6 py-2 rounded-xl text-sm font-bold"
>
إرسال التذكرة
</button>
</form>
)}
<div className="space-y-3">
{tickets.map((t: any) => (
<div
key={t.id}
className="bg-[#111] border border-[#222] rounded-xl p-4"
>
<div className="flex justify-between items-start mb-2">
<div>
<span className="font-bold text-sm">{t.subject}</span>
<span
className={`mr-2 px-2 py-0.5 rounded-full text-xs font-bold ${statusColors[t.status]}`}
>
{statusLabels[t.status]}
</span>
</div>
<div className="flex gap-1 shrink-0">
{t.status !== "closed" && (
<button
onClick={() => handleClose(t.id)}
className="text-xs text-green-400 border border-green-400/30 px-2 py-0.5 rounded-lg"
>
إغلاق
</button>
)}
<button
onClick={() => handleDelete(t.id)}
className="p-1 text-red-400 hover:bg-red-500/10 rounded"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
<p className="text-xs text-gray-500 mb-1">
{t.customer_name} | {t.customer_phone}
</p>
<p className="text-sm text-gray-400">{t.message}</p>
{t.admin_reply && (
<div className="mt-3 bg-[#D4AF37]/5 border border-[#D4AF37]/20 rounded-lg p-3 text-sm">
<strong className="text-[#D4AF37]">رد الإدارة: </strong>
{t.admin_reply}
</div>
)}
{t.status !== "closed" && (
<button
onClick={() => {
setSelected(t);
setReply("");
}}
className="mt-2 text-xs text-[#D4AF37] border border-[#D4AF37]/30 px-3 py-1 rounded-lg hover:bg-[#D4AF37]/10"
>
الرد على التذكرة
</button>
)}
</div>
))}
{tickets.length === 0 && (
<p className="text-center py-12 text-gray-600">لا توجد تذاكر دعم</p>
)}
</div>
{selected && (
<div
className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4"
dir="rtl"
>
<div className="bg-[#111] border border-[#222] rounded-2xl w-full max-w-md p-6">
<div className="flex justify-between mb-4">
<h3 className="font-bold">الرد على: {selected.subject}</h3>
<button onClick={() => setSelected(null)}>
<X className="w-5 h-5 text-gray-400" />
</button>
</div>
<p className="text-sm text-gray-400 mb-3">{selected.message}</p>
<textarea
rows={4}
value={reply}
onChange={(e) => setReply(e.target.value)}
className={`${SH} resize-none mb-3`}
placeholder="اكتب ردك هنا..."
/>
<button
onClick={handleReply}
disabled={submitting || !reply.trim()}
className="w-full bg-[#D4AF37] text-black font-bold py-2.5 rounded-xl text-sm flex items-center justify-center gap-2"
>
{submitting && <Loader2 className="w-4 h-4 animate-spin" />} إرسال
الرد
</button>
</div>
</div>
)}
</div>
);
}
// ─── 10. Offers Tab ─────────────────────────────────────
function OffersTab() {
const [offers, setOffers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [products, setProducts] = useState<any[]>([]);
const [form, setForm] = useState({
product_id: "",
title: "",
discount_type: "percentage",
discount_value: "",
start_date: "",
end_date: "",
});
const load = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
try {
const [or, pr] = await Promise.all([
fetch(`${API}/scheduled-offers`),
fetch(`${API}/products?limit=200`),
]);
setOffers(await or.json());
const pd = await pr.json();
setProducts(pd.products || []);
} catch (_) {}
if (!silent) setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, [load]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await fetch(`${API}/scheduled-offers`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
adminToast("تم إنشاء العرض");
setShowAdd(false);
load();
setForm({
product_id: "",
title: "",
discount_type: "percentage",
discount_value: "",
start_date: "",
end_date: "",
});
};
const handleDelete = async (id: number) => {
if (!confirm("حذف هذا العرض؟")) return;
await fetch(`${API}/scheduled-offers/${id}`, { method: "DELETE" });
adminToast("تم الحذف");
load();
};
const isActive = (offer: any) => {
const now = new Date();
return new Date(offer.start_date) <= now && new Date(offer.end_date) >= now;
};
if (loading) return <Spinner />;
return (
<div>
<div className="flex justify-between items-start mb-6">
<SectionHeader
title="العروض المجدولة"
subtitle={`${offers.length} عرض`}
/>
<button
onClick={() => setShowAdd(!showAdd)}
className="bg-[#D4AF37] text-black px-4 py-2 rounded-xl text-sm flex items-center gap-2 font-bold shrink-0"
>
<Plus className="w-4 h-4" /> عرض جديد
</button>
</div>
{showAdd && (
<form
onSubmit={handleSubmit}
className="bg-[#111] border border-[#222] rounded-2xl p-6 mb-6 space-y-4"
>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className={LH}>اسم العرض *</label>
<input
required
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className={SH}
placeholder="عرض الجمعة البيضاء"
/>
</div>
<div className="col-span-2">
<label className={LH}>المنتج (اختياري)</label>
<select
value={form.product_id}
onChange={(e) =>
setForm({ ...form, product_id: e.target.value })
}
className={SH}
>
<option value="">كل المنتجات</option>
{products.map((p: any) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div>
<label className={LH}>نوع الخصم</label>
<select
value={form.discount_type}
onChange={(e) =>
setForm({ ...form, discount_type: e.target.value })
}
className={SH}
>
<option value="percentage">نسبة مئوية (%)</option>
<option value="fixed">مبلغ ثابت (ريال)</option>
</select>
</div>
<div>
<label className={LH}>قيمة الخصم *</label>
<input
required
type="number"
value={form.discount_value}
onChange={(e) =>
setForm({ ...form, discount_value: e.target.value })
}
className={SH}
/>
</div>
<div>
<label className={LH}>تاريخ البداية *</label>
<input
required
type="datetime-local"
value={form.start_date}
onChange={(e) =>
setForm({ ...form, start_date: e.target.value })
}
className={SH}
/>
</div>
<div>
<label className={LH}>تاريخ الانتهاء *</label>
<input
required
type="datetime-local"
value={form.end_date}
onChange={(e) => setForm({ ...form, end_date: e.target.value })}
className={SH}
/>
</div>
</div>
<button
type="submit"
className="bg-[#D4AF37] text-black px-6 py-2 rounded-xl text-sm font-bold"
>
إنشاء العرض
</button>
</form>
)}
<div className="space-y-3">
{offers.map((offer: any) => (
<div
key={offer.id}
className="bg-[#111] border border-[#222] rounded-xl p-4 flex items-center justify-between gap-4"
>
<div className="min-w-0">
<div className="font-bold text-sm flex items-center gap-2 flex-wrap">
{offer.title}
<span
className={`px-2 py-0.5 rounded-full text-xs font-bold ${isActive(offer) ? "bg-green-500/20 text-green-400" : new Date(offer.end_date) < new Date() ? "bg-red-500/20 text-red-400" : "bg-blue-500/20 text-blue-400"}`}
>
{isActive(offer)
? "نشط الآن"
: new Date(offer.end_date) < new Date()
? "انتهى"
: "قادم"}
</span>
</div>
<div className="text-xs text-gray-500 mt-1">
خصم{" "}
{offer.discount_type === "percentage"
? `${offer.discount_value}%`
: `${offer.discount_value} ر.س`}{" "}
| {format(new Date(offer.start_date), "yyyy/MM/dd")} {" "}
{format(new Date(offer.end_date), "yyyy/MM/dd")}
</div>
</div>
<button
onClick={() => handleDelete(offer.id)}
className="p-2 text-red-400 hover:bg-red-500/10 rounded-lg shrink-0"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{offers.length === 0 && (
<p className="text-center py-12 text-gray-600">لا توجد عروض مجدولة</p>
)}
</div>
</div>
);
}
// ─── 11. Abandoned Carts ─────────────────────────────────
function AbandonedCartsTab() {
const [carts, setCarts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/admin/abandoned-carts`);
setCarts(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
};
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, []);
if (loading) return <Spinner />;
return (
<div>
<SectionHeader title="السلات المتروكة" subtitle={`${carts.length} سلة`} />
{!carts.length ? (
<div className="text-center py-20 text-gray-600">
لا توجد سلات متروكة حالياً
</div>
) : (
<div className="space-y-3">
{carts.map((c: any) => (
<div
key={c.session_id}
className="bg-[#111] border border-[#222] rounded-xl p-4"
>
<div className="flex justify-between items-start mb-3 gap-3">
<div>
<div className="font-mono text-xs text-gray-600">
{shortSessionId(c.session_id)}
</div>
<div className="font-bold text-[#D4AF37] text-lg mt-1">
{formatPrice(c.total)}
</div>
<div className="text-xs text-gray-500 mt-2 space-y-1">
<div>{c.customer_name || "عميل غير معروف"}</div>
{c.customer_phone && <div dir="ltr">{c.customer_phone}</div>}
{c.customer_email && <div dir="ltr">{c.customer_email}</div>}
{c.city && <div>{c.city}</div>}
</div>
</div>
<div className="flex flex-col items-end gap-2">
<span className="px-2 py-1 bg-yellow-500/15 text-yellow-400 text-xs rounded-lg font-bold">
{c.items_count} منتج
</span>
<span className="px-2 py-1 bg-blue-500/15 text-blue-300 text-[11px] rounded-lg font-bold">
{c.reminder_channel || "رنين المتجر"} كل {c.reminder_frequency_minutes || 60} دقيقة
</span>
<span className="px-2 py-1 bg-emerald-500/15 text-emerald-300 text-[11px] rounded-lg font-bold">
{c.reminder_status || "جاهز للإرسال"}
</span>
</div>
</div>
{c.items?.map((item: any, i: number) => (
<div
key={i}
className="flex justify-between text-xs text-gray-500"
>
<span>
{item.name} × {item.qty}
</span>
<span>
{formatPrice(parseFloat(String(item.price)) * item.qty)}
</span>
</div>
))}
<div className="mt-3 rounded-xl border border-white/10 bg-black/20 p-3 text-xs text-white/60 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-white/35">آخر تذكير:</span>
<span>{c.last_reminder_at ? format(new Date(c.last_reminder_at), "yyyy/MM/dd — HH:mm") : "—"}</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-white/35">التذكير القادم:</span>
<span>{c.next_reminder_at ? format(new Date(c.next_reminder_at), "yyyy/MM/dd — HH:mm") : "—"}</span>
{typeof c.minutes_until_reminder === "number" && (
<span className="px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-[#D4AF37]">
خلال {c.minutes_until_reminder} دقيقة
</span>
)}
</div>
<div><span className="text-white/35">نص التذكير:</span> {c.reminder_message || "—"}</div>
</div>
</div>
))}
</div>
)}
<div className="mt-6 bg-blue-500/10 border border-blue-500/30 rounded-xl p-4 space-y-2">
<p className="text-sm text-blue-400">
🔔 تم تجهيز تنبيه رنين للعميل كل ساعة داخل المعاينة مع رسالة قرب انتهاء العرض ونفاد الكمية.
</p>
<p className="text-xs text-blue-300/80">
في هذه النسخة يتم عرض الجدولة والمتابعة داخل لوحة التحكم، بينما الإرسال الفعلي الخارجي يحتاج مزود رسائل/إشعارات وربط خلفية منفصل.
</p>
</div>
</div>
);
}
// ─── 12. Categories Tab ──────────────────────────────────
function CategoriesTab() {
const [cats, setCats] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [editCat, setEditCat] = useState<any>(null);
const emptyForm = {
name: "",
name_en: "",
icon: "📦",
sort_order: 0,
parent_id: "",
};
const [form, setForm] = useState(emptyForm);
const load = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
try {
const res = await fetch(`${API}/categories`);
setCats(await res.json());
} catch (_) {}
if (!silent) setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(() => load(true), 2000);
return () => clearInterval(i);
}, [load]);
const mainCats = cats.filter((c) => !c.parent_id);
const subCats = cats.filter((c) => c.parent_id);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const payload = {
...form,
parent_id: form.parent_id ? parseInt(form.parent_id) : null,
sort_order: parseInt(String(form.sort_order)) || 0,
};
if (editCat) {
await fetch(`${API}/categories/${editCat.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
adminToast("تم التعديل");
setEditCat(null);
} else {
await fetch(`${API}/categories`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
adminToast("تم الإضافة");
setShowAdd(false);
}
refetch_cats();
setForm(emptyForm);
};
const refetch_cats = () => load();
const handleDelete = async (id: number) => {
if (!confirm("حذف هذه الفئة؟")) return;
await fetch(`${API}/categories/${id}`, { method: "DELETE" });
adminToast("تم الحذف");
load();
};
if (loading) return <Spinner />;
const FormBlock = () => (
<form
onSubmit={handleSubmit}
className="bg-[#111] border border-[#D4AF37]/20 rounded-2xl p-6 mb-6 space-y-4"
>
<h3 className="font-bold text-[#D4AF37]">
{editCat ? `تعديل: ${editCat.name}` : "إضافة تصنيف جديد"}
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>اسم الفئة (عربي) *</label>
<input
required
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
className={SH}
/>
</div>
<div>
<label className={LH}>اسم الفئة (إنجليزي)</label>
<input
value={form.name_en}
onChange={(e) => setForm({ ...form, name_en: e.target.value })}
className={SH}
/>
</div>
<div>
<label className={LH}>الأيقونة</label>
<input
value={form.icon}
onChange={(e) => setForm({ ...form, icon: e.target.value })}
className={`${SH} text-xl`}
maxLength={4}
/>
</div>
<div>
<label className={LH}>الترتيب</label>
<input
type="number"
value={form.sort_order}
onChange={(e) =>
setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })
}
className={SH}
/>
</div>
<div className="col-span-2">
<label className={LH}>
التصنيف الرئيسي (اتركه فارغاً للتصنيف الرئيسي)
</label>
<select
value={form.parent_id}
onChange={(e) => setForm({ ...form, parent_id: e.target.value })}
className={SH}
>
<option value="">تصنيف رئيسي</option>
{mainCats
.filter((c) => !editCat || c.id !== editCat.id)
.map((c) => (
<option key={c.id} value={c.id}>
{c.icon} {c.name}
</option>
))}
</select>
</div>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => {
setShowAdd(false);
setEditCat(null);
setForm(emptyForm);
}}
className="px-6 py-2 bg-[#1a1a1a] border border-[#333] rounded-xl text-sm"
>
إلغاء
</button>
<button
type="submit"
className="flex-1 bg-[#D4AF37] text-black font-bold py-2 rounded-xl text-sm"
>
{editCat ? "حفظ التعديلات" : "إضافة الفئة"}
</button>
</div>
</form>
);
return (
<div>
<div className="flex justify-between items-start mb-6">
<SectionHeader
title="شجرة التصنيفات"
subtitle={`${mainCats.length} رئيسي · ${subCats.length} فرعي`}
/>
<button
onClick={() => {
setShowAdd(!showAdd);
setEditCat(null);
setForm(emptyForm);
}}
className="bg-[#D4AF37] text-black px-4 py-2 rounded-xl text-sm flex items-center gap-2 font-bold shrink-0"
>
<Plus className="w-4 h-4" /> فئة جديدة
</button>
</div>
{(showAdd || editCat) && <FormBlock />}
<div className="space-y-3">
{mainCats.map((main) => {
const children = subCats.filter((s) => s.parent_id === main.id);
return (
<div
key={main.id}
className="bg-[#111] border border-[#222] rounded-2xl overflow-hidden"
>
<div className="flex items-center justify-between p-4 bg-[#1a1a1a]">
<div className="flex items-center gap-3">
<span className="text-2xl">{main.icon || "📦"}</span>
<div>
<div className="font-bold text-sm">{main.name}</div>
<div className="text-xs text-gray-500">
{main.name_en} · {children.length} فرعي
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setEditCat(main);
setForm({
name: main.name,
name_en: main.name_en || "",
icon: main.icon || "📦",
sort_order: main.sort_order,
parent_id: "",
});
setShowAdd(false);
}}
className="p-1.5 text-blue-400 hover:bg-blue-500/10 rounded-lg"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDelete(main.id)}
className="p-1.5 text-red-400 hover:bg-red-500/10 rounded-lg"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{children.length > 0 && (
<div className="divide-y divide-[#1a1a1a]">
{children.map((sub) => (
<div
key={sub.id}
className="flex items-center justify-between px-6 py-2.5"
>
<div className="flex items-center gap-2">
<div className="w-px h-4 bg-[#333] ml-1" />
<span className="text-lg">{sub.icon || "📁"}</span>
<div>
<div className="text-sm font-medium">{sub.name}</div>
<div className="text-xs text-gray-500">
{sub.name_en}
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setEditCat(sub);
setForm({
name: sub.name,
name_en: sub.name_en || "",
icon: sub.icon || "📁",
sort_order: sub.sort_order,
parent_id: String(sub.parent_id),
});
setShowAdd(false);
}}
className="p-1 text-blue-400 hover:bg-blue-500/10 rounded"
>
<Pencil className="w-3 h-3" />
</button>
<button
onClick={() => handleDelete(sub.id)}
className="p-1 text-red-400 hover:bg-red-500/10 rounded"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
// ─── Delivery Tab ────────────────────────────────────
interface DeliveryCondition {
id: string;
text: string;
visible: boolean;
}
function DeliveryTab() {
const token = localStorage.getItem("admin_token") || "";
const [conditions, setConditions] = useState<DeliveryCondition[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editText, setEditText] = useState("");
const [newText, setNewText] = useState("");
const [addingNew, setAddingNew] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`${API}/admin/store-settings`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
const raw = data.delivery_conditions;
setConditions(raw ? JSON.parse(raw) : []);
} catch {
setConditions([]);
}
setLoading(false);
}, [token]);
useEffect(() => {
load();
}, [load]);
const save = async (updated: DeliveryCondition[]) => {
setSaving(true);
await fetch(`${API}/admin/store-settings`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ delivery_conditions: JSON.stringify(updated) }),
});
setSaving(false);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const toggleVisible = (id: string) => {
const updated = conditions.map((c) =>
c.id === id ? { ...c, visible: !c.visible } : c,
);
setConditions(updated);
save(updated);
};
const deleteCondition = (id: string) => {
const updated = conditions.filter((c) => c.id !== id);
setConditions(updated);
save(updated);
};
const startEdit = (c: DeliveryCondition) => {
setEditingId(c.id);
setEditText(c.text);
};
const saveEdit = () => {
if (!editText.trim()) return;
const updated = conditions.map((c) =>
c.id === editingId ? { ...c, text: editText.trim() } : c,
);
setConditions(updated);
save(updated);
setEditingId(null);
};
const addCondition = () => {
if (!newText.trim()) return;
const updated = [
...conditions,
{ id: Date.now().toString(), text: newText.trim(), visible: true },
];
setConditions(updated);
save(updated);
setNewText("");
setAddingNew(false);
};
if (loading)
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="animate-spin text-[#D4AF37]" size={32} />
</div>
);
return (
<div className="max-w-3xl space-y-6" dir="rtl">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-black text-white flex items-center gap-2">
<Truck size={20} className="text-[#D4AF37]" />
إدارة شروط التوصيل
</h2>
<p className="text-sm text-gray-500 mt-1">
هذه الشروط تظهر للعميل في صفحة إتمام الطلب
</p>
</div>
<div className="flex items-center gap-2">
{saved && (
<span className="text-green-400 text-sm flex items-center gap-1">
<Check size={14} /> حُفظ
</span>
)}
{saving && (
<Loader2 size={16} className="animate-spin text-[#D4AF37]" />
)}
</div>
</div>
{/* Conditions List */}
<div className="space-y-3">
{conditions.length === 0 && (
<div className="text-center py-12 text-gray-600">
لا توجد شروط حالياً. أضف شرطاً جديداً.
</div>
)}
{conditions.map((c, i) => (
<div
key={c.id}
className={`bg-[#111] border rounded-xl p-4 transition-all ${c.visible ? "border-[#333]" : "border-[#333]/40 opacity-50"}`}
>
{editingId === c.id ? (
<div className="space-y-3">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
rows={2}
autoFocus
className="w-full bg-[#1a1a1a] border border-[#D4AF37]/50 rounded-xl px-4 py-2.5 text-white text-sm outline-none focus:border-[#D4AF37] resize-none"
/>
<div className="flex gap-2">
<button
onClick={saveEdit}
className="flex items-center gap-1.5 bg-[#D4AF37] text-black font-bold px-4 py-2 rounded-lg text-sm hover:bg-[#c9a62f] transition-colors"
>
<Check size={14} /> حفظ
</button>
<button
onClick={() => setEditingId(null)}
className="px-4 py-2 text-sm text-gray-400 hover:text-white border border-[#333] rounded-lg transition-colors"
>
إلغاء
</button>
</div>
</div>
) : (
<div className="flex items-start gap-3">
<span className="text-[#D4AF37] font-black text-sm shrink-0 mt-0.5">
{i + 1}
</span>
<p
className={`flex-1 text-sm leading-relaxed ${c.visible ? "text-gray-300" : "text-gray-600 line-through"}`}
>
{c.text}
</p>
<div className="flex items-center gap-1 shrink-0">
{/* Edit */}
<button
onClick={() => startEdit(c)}
title="تعديل"
className="w-8 h-8 flex items-center justify-center rounded-lg text-gray-500 hover:text-[#D4AF37] hover:bg-[#D4AF37]/10 transition-all"
>
<Pencil size={14} />
</button>
{/* Show/Hide */}
<button
onClick={() => toggleVisible(c.id)}
title={c.visible ? "إخفاء" : "إظهار"}
className="w-8 h-8 flex items-center justify-center rounded-lg text-gray-500 hover:text-blue-400 hover:bg-blue-400/10 transition-all"
>
{c.visible ? <Eye size={14} /> : <EyeOff size={14} />}
</button>
{/* Delete */}
<button
onClick={() => deleteCondition(c.id)}
title="حذف"
className="w-8 h-8 flex items-center justify-center rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition-all"
>
<Trash2 size={14} />
</button>
</div>
</div>
)}
</div>
))}
</div>
{/* Add New */}
{addingNew ? (
<div className="bg-[#111] border border-[#D4AF37]/30 rounded-xl p-4 space-y-3">
<p className="text-sm text-[#D4AF37] font-bold">إضافة شرط جديد</p>
<textarea
value={newText}
onChange={(e) => setNewText(e.target.value)}
rows={2}
autoFocus
placeholder="اكتب نص الشرط هنا..."
className="w-full bg-[#1a1a1a] border border-[#333] rounded-xl px-4 py-2.5 text-white text-sm outline-none focus:border-[#D4AF37] resize-none"
/>
<div className="flex gap-2">
<button
onClick={addCondition}
disabled={!newText.trim()}
className="flex items-center gap-1.5 bg-[#D4AF37] text-black font-bold px-5 py-2 rounded-lg text-sm hover:bg-[#c9a62f] transition-colors disabled:opacity-50"
>
<Plus size={14} /> إضافة
</button>
<button
onClick={() => {
setAddingNew(false);
setNewText("");
}}
className="px-4 py-2 text-sm text-gray-400 hover:text-white border border-[#333] rounded-lg transition-colors"
>
إلغاء
</button>
</div>
</div>
) : (
<button
onClick={() => setAddingNew(true)}
className="w-full flex items-center justify-center gap-2 py-3 border border-dashed border-[#333] hover:border-[#D4AF37]/50 rounded-xl text-gray-500 hover:text-[#D4AF37] transition-all text-sm"
>
<Plus size={16} /> إضافة شرط جديد
</button>
)}
{/* Legend */}
<div className="bg-[#0f0f0f] border border-[#222] rounded-xl p-4 text-xs text-gray-500 space-y-1.5">
<p className="text-gray-400 font-medium mb-2">دليل الأيقونات:</p>
<div className="flex items-center gap-2">
<Pencil size={12} className="text-[#D4AF37]" />{" "}
<span>تعديل نص الشرط</span>
</div>
<div className="flex items-center gap-2">
<Eye size={12} className="text-blue-400" />{" "}
<span>إخفاء/إظهار الشرط في صفحة التوصيل</span>
</div>
<div className="flex items-center gap-2">
<Trash2 size={12} className="text-red-400" />{" "}
<span>حذف الشرط نهائياً</span>
</div>
</div>
</div>
);
}
// ─── Appearance Tab ──────────────────────────────────
type PromoBanner = { image_url: string; link: string; title: string };
function AppearanceTab() {
const [s, setS] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeSection, setActiveSection] = useState<
"announcement" | "hero" | "sections" | "banners" | "labels" | "cart"
>("announcement");
const [banners, setBanners] = useState<PromoBanner[]>([]);
const [newBanner, setNewBanner] = useState<PromoBanner>({
image_url: "",
link: "",
title: "",
});
const load = async () => {
try {
const r = await fetch(`${API}/admin/store-settings`);
const data = await r.json();
setS(data);
try {
setBanners(JSON.parse(data.promo_banners || "[]"));
} catch {
setBanners([]);
}
} catch {}
setLoading(false);
};
useEffect(() => {
load();
}, []);
const set = (key: string, val: string) =>
setS((prev) => ({ ...prev, [key]: val }));
const toggle = (key: string) =>
set(key, s[key] === "false" ? "true" : "false");
const save = async () => {
setSaving(true);
const payload = { ...s, promo_banners: JSON.stringify(banners) };
await fetch(`${API}/admin/store-settings`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
adminToast("تم حفظ إعدادات المظهر ✓");
setSaving(false);
};
const addBanner = () => {
if (!newBanner.image_url) {
adminToast("أدخل رابط الصورة", "err");
return;
}
setBanners((prev) => [...prev, { ...newBanner }]);
setNewBanner({ image_url: "", link: "", title: "" });
};
if (loading) return <Spinner />;
const isOn = (key: string) => s[key] !== "false";
const SECTIONS = [
{ id: "announcement", label: "الشريط الإعلاني", icon: Megaphone },
{ id: "hero", label: "قسم البطل (Hero)", icon: Layout },
{ id: "sections", label: "أقسام الصفحة", icon: Grid },
{ id: "banners", label: "البانرات الترويجية", icon: Image },
{ id: "labels", label: "واجهة المتجر", icon: Palette },
{
id: "cart",
label: "السلة / التوصيل / الدفع / التحقق",
icon: ShoppingCart,
},
] as const;
return (
<div>
<div className="flex flex-wrap items-center justify-between gap-3 mb-6">
<div>
<h2 className="text-xl font-bold text-white">مظهر المتجر</h2>
<p className="text-sm text-gray-500 mt-0.5">
تحكم في كل ما يظهر على واجهة المتجر
</p>
</div>
<div className="flex gap-2">
<a
href="/"
target="_blank"
className="flex items-center gap-2 bg-[#1a1a1a] border border-[#333] text-gray-300 px-3 py-2 rounded-xl text-sm hover:border-[#D4AF37]/50"
>
<ExternalLink className="w-4 h-4" /> معاينة المتجر
</a>
<button
onClick={save}
disabled={saving}
className="flex items-center gap-2 bg-[#D4AF37] text-black font-bold px-5 py-2 rounded-xl text-sm"
>
{saving ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Check className="w-4 h-4" />
)}
حفظ التغييرات
</button>
</div>
</div>
<div className="flex gap-4">
{/* Sub-nav */}
<div className="w-44 shrink-0">
<div className="bg-[#111] border border-[#222] rounded-2xl overflow-hidden">
{SECTIONS.map((sec) => {
const Icon = sec.icon;
return (
<button
key={sec.id}
onClick={() =>
setActiveSection(sec.id as typeof activeSection)
}
className={`w-full flex items-center gap-2.5 px-4 py-3 text-sm text-right transition-colors
${activeSection === sec.id ? "bg-[#D4AF37]/15 text-[#D4AF37] border-r-2 border-[#D4AF37]" : "text-gray-400 hover:bg-[#1a1a1a] hover:text-white"}`}
>
<Icon className="w-4 h-4 shrink-0" />
<span className="text-xs font-medium">{sec.label}</span>
</button>
);
})}
</div>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="bg-[#111] border border-[#222] rounded-2xl p-6 space-y-5">
{/* ── Announcement Bar ── */}
{activeSection === "announcement" && (
<>
<div className="flex items-center justify-between pb-4 border-b border-[#222]">
<div>
<h3 className="font-bold text-white flex items-center gap-2">
<Megaphone className="w-4 h-4 text-[#D4AF37]" /> الشريط
الإعلاني
</h3>
<p className="text-xs text-gray-500 mt-0.5">
الشريط المتحرك في أعلى الصفحة
</p>
</div>
<button
onClick={() => toggle("announcement_enabled")}
className={`flex items-center gap-2 px-3 py-1.5 rounded-xl border text-sm font-bold transition-all
${isOn("announcement_enabled") ? "bg-green-500/15 border-green-500/30 text-green-400" : "bg-[#222] border-[#333] text-gray-500"}`}
>
{isOn("announcement_enabled") ? (
<>
<ToggleRight className="w-4 h-4" /> مفعّل
</>
) : (
<>
<ToggleLeft className="w-4 h-4" /> معطّل
</>
)}
</button>
</div>
<div>
<label className={LH}>نص الشريط</label>
<textarea
rows={2}
value={s.announcement_text || ""}
onChange={(e) => set("announcement_text", e.target.value)}
className={SH + " resize-none"}
placeholder="مثال: 🎉 شحن مجاني على الطلبات فوق 200 ر.س"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>لون الخلفية</label>
<div className="flex items-center gap-2">
<input
type="color"
value={s.announcement_color || "#f97316"}
onChange={(e) =>
set("announcement_color", e.target.value)
}
className="w-12 h-10 rounded-xl border-2 border-[#333] bg-transparent cursor-pointer"
/>
<input
value={s.announcement_color || "#f97316"}
onChange={(e) =>
set("announcement_color", e.target.value)
}
className={SH}
placeholder="#f97316"
/>
</div>
</div>
<div>
<label className={LH}>لون النص</label>
<div className="flex items-center gap-2">
<input
type="color"
value={s.announcement_text_color || "#ffffff"}
onChange={(e) =>
set("announcement_text_color", e.target.value)
}
className="w-12 h-10 rounded-xl border-2 border-[#333] bg-transparent cursor-pointer"
/>
<input
value={s.announcement_text_color || "#ffffff"}
onChange={(e) =>
set("announcement_text_color", e.target.value)
}
className={SH}
placeholder="#ffffff"
/>
</div>
</div>
</div>
{/* Preview */}
<div className="rounded-xl overflow-hidden border border-[#333]">
<div className="text-[10px] text-gray-600 px-3 py-1 bg-[#1a1a1a] border-b border-[#222]">
معاينة
</div>
<div
className="overflow-hidden py-2.5 text-center text-sm font-medium"
style={{
backgroundColor: s.announcement_color || "#f97316",
color: s.announcement_text_color || "#fff",
}}
>
{s.announcement_text || "نص الشريط الإعلاني..."}
</div>
</div>
</>
)}
{/* ── Hero Section ── */}
{activeSection === "hero" && (
<>
<div className="flex items-center justify-between pb-4 border-b border-[#222]">
<div>
<h3 className="font-bold text-white flex items-center gap-2">
<Layout className="w-4 h-4 text-[#D4AF37]" /> قسم البطل
(Hero)
</h3>
<p className="text-xs text-gray-500 mt-0.5">
الواجهة الرئيسية الكبيرة في أعلى الصفحة
</p>
</div>
<button
onClick={() => toggle("hero_enabled")}
className={`flex items-center gap-2 px-3 py-1.5 rounded-xl border text-sm font-bold transition-all
${isOn("hero_enabled") ? "bg-green-500/15 border-green-500/30 text-green-400" : "bg-[#222] border-[#333] text-gray-500"}`}
>
{isOn("hero_enabled") ? (
<>
<ToggleRight className="w-4 h-4" /> مرئي
</>
) : (
<>
<ToggleLeft className="w-4 h-4" /> مخفي
</>
)}
</button>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>شعار الشارة</label>
<input
value={s.hero_badge_ar || ""}
onChange={(e) => set("hero_badge_ar", e.target.value)}
className={SH}
placeholder="⚡ عروض حصرية لفترة محدودة"
/>
</div>
<div>
<label className={LH}>لون التمييز</label>
<div className="flex items-center gap-2">
<input
type="color"
value={s.hero_accent_color || "#f97316"}
onChange={(e) =>
set("hero_accent_color", e.target.value)
}
className="w-12 h-10 rounded-xl border-2 border-[#333] bg-transparent cursor-pointer"
/>
<input
value={s.hero_accent_color || "#f97316"}
onChange={(e) =>
set("hero_accent_color", e.target.value)
}
className={SH}
/>
</div>
</div>
</div>
<div>
<label className={LH}>العنوان الرئيسي (عربي)</label>
<textarea
rows={2}
value={s.hero_title_ar || ""}
onChange={(e) => set("hero_title_ar", e.target.value)}
className={SH + " resize-none"}
placeholder="أفضل الإلكترونيات&#10;في المملكة العربية السعودية"
/>
<p className="text-[10px] text-gray-600 mt-1">
استخدم سطراً جديداً لتقسيم العنوان
</p>
</div>
<div>
<label className={LH}>النص التوضيحي</label>
<textarea
rows={2}
value={s.hero_subtitle_ar || ""}
onChange={(e) => set("hero_subtitle_ar", e.target.value)}
className={SH + " resize-none"}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>نص زر الدعوة للعمل</label>
<input
value={s.hero_cta_ar || ""}
onChange={(e) => set("hero_cta_ar", e.target.value)}
className={SH}
placeholder="تسوق الآن"
/>
</div>
<div>
<label className={LH}>رابط الزر</label>
<input
value={s.hero_cta_link || ""}
onChange={(e) => set("hero_cta_link", e.target.value)}
className={SH}
placeholder="/category/0"
/>
</div>
</div>
<div>
<label className={LH}>
صورة خلفية الـ Hero (URL اختياري)
</label>
<input
value={s.hero_bg_image || ""}
onChange={(e) => set("hero_bg_image", e.target.value)}
className={SH}
placeholder="https://... أو اتركه فارغاً للتدرج اللوني"
/>
{s.hero_bg_image && (
<div className="mt-2 h-20 rounded-xl overflow-hidden border border-[#333]">
<img
src={s.hero_bg_image}
className="w-full h-full object-cover"
alt="hero preview"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
</div>
)}
</div>
</>
)}
{/* ── Home Sections ── */}
{activeSection === "sections" && (
<>
<div className="pb-4 border-b border-[#222]">
<h3 className="font-bold text-white flex items-center gap-2">
<Grid className="w-4 h-4 text-[#D4AF37]" /> أقسام الصفحة
الرئيسية
</h3>
<p className="text-xs text-gray-500 mt-0.5">
تحكم في ظهور وعناوين كل قسم
</p>
</div>
{[
{
id: "extra",
enableKey: "extra_section_enabled",
titleKey: "extra_section_title_ar",
iconKey: "",
label: "قسم رين (الفئات)",
noIcon: true,
},
{
id: "shein",
enableKey: "shein_section_enabled",
titleKey: "shein_section_title_ar",
iconKey: "",
label: "قسم Shein",
noIcon: true,
},
{
id: "trending",
enableKey: "section_trending_enabled",
titleKey: "section_trending_title_ar",
iconKey: "section_trending_icon",
label: "الأكثر رواجاً",
noIcon: false,
},
{
id: "bestseller",
enableKey: "section_bestseller_enabled",
titleKey: "section_bestseller_title_ar",
iconKey: "section_bestseller_icon",
label: "الأكثر مبيعاً",
noIcon: false,
},
{
id: "new",
enableKey: "section_new_enabled",
titleKey: "section_new_title_ar",
iconKey: "section_new_icon",
label: "وصل حديثاً",
noIcon: false,
},
].map((sec) => (
<div
key={sec.id}
className="bg-[#0a0a0a] border border-[#222] rounded-xl p-4"
>
<div className="flex items-center justify-between mb-3">
<span className="font-medium text-sm text-white">
{sec.label}
</span>
<button
onClick={() => toggle(sec.enableKey)}
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-lg border text-xs font-bold transition-all
${isOn(sec.enableKey) ? "bg-green-500/15 border-green-500/30 text-green-400" : "bg-[#222] border-[#333] text-gray-500"}`}
>
{isOn(sec.enableKey) ? (
<>
<ToggleRight className="w-3.5 h-3.5" /> مرئي
</>
) : (
<>
<ToggleLeft className="w-3.5 h-3.5" /> مخفي
</>
)}
</button>
</div>
<div
className={`grid gap-3 ${sec.noIcon ? "grid-cols-1" : "grid-cols-3"}`}
>
<div className={sec.noIcon ? "" : "col-span-2"}>
<label className={LH}>العنوان</label>
<input
value={s[sec.titleKey] || ""}
onChange={(e) => set(sec.titleKey, e.target.value)}
className={SH}
/>
</div>
{!sec.noIcon && (
<div>
<label className={LH}>الأيقونة</label>
<input
value={s[sec.iconKey] || ""}
onChange={(e) => set(sec.iconKey, e.target.value)}
className={SH}
placeholder="🔥"
/>
</div>
)}
</div>
</div>
))}
</>
)}
{/* ── Promo Banners ── */}
{activeSection === "banners" && (
<>
<div className="pb-4 border-b border-[#222]">
<h3 className="font-bold text-white flex items-center gap-2">
<Image className="w-4 h-4 text-[#D4AF37]" /> البانرات
الترويجية
</h3>
<p className="text-xs text-gray-500 mt-0.5">
بانرات صور تظهر أسفل الـ Hero مباشرةً
</p>
</div>
{/* Add new banner */}
<div className="bg-[#0a0a0a] border border-[#D4AF37]/20 rounded-xl p-4 space-y-3">
<p className="text-xs font-bold text-[#D4AF37]">
إضافة بانر جديد
</p>
<div>
<label className={LH}>رابط الصورة *</label>
<input
value={newBanner.image_url}
onChange={(e) =>
setNewBanner((b) => ({
...b,
image_url: e.target.value,
}))
}
className={SH}
placeholder="https://res.cloudinary.com/..."
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={LH}>عنوان البانر (اختياري)</label>
<input
value={newBanner.title}
onChange={(e) =>
setNewBanner((b) => ({ ...b, title: e.target.value }))
}
className={SH}
placeholder="مثال: تخفيضات الصيف"
/>
</div>
<div>
<label className={LH}>رابط الضغط</label>
<input
value={newBanner.link}
onChange={(e) =>
setNewBanner((b) => ({ ...b, link: e.target.value }))
}
className={SH}
placeholder="/category/0"
/>
</div>
</div>
{newBanner.image_url && (
<div className="h-20 rounded-lg overflow-hidden border border-[#333]">
<img
src={newBanner.image_url}
className="w-full h-full object-cover"
alt="banner preview"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
</div>
)}
<button
onClick={addBanner}
className="bg-[#D4AF37] text-black font-bold text-sm px-4 py-2 rounded-xl flex items-center gap-2"
>
<Plus className="w-4 h-4" /> إضافة البانر
</button>
</div>
{/* Banners list */}
{banners.length === 0 ? (
<div className="text-center py-8 text-gray-600 text-sm">
لا توجد بانرات مضافة
</div>
) : (
<div className="space-y-3">
{banners.map((b, i) => (
<div
key={i}
className="flex items-center gap-3 bg-[#0a0a0a] border border-[#222] rounded-xl p-3"
>
<div className="w-20 h-12 rounded-lg overflow-hidden border border-[#333] shrink-0">
<img
src={b.image_url}
className="w-full h-full object-cover"
alt=""
onError={(e) => {
(e.target as HTMLImageElement).style.display =
"none";
}}
/>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white truncate">
{b.title || "بلا عنوان"}
</div>
<div className="text-xs text-gray-500 truncate">
{b.image_url}
</div>
{b.link && (
<div className="text-xs text-blue-400 truncate">
{b.link}
</div>
)}
</div>
<button
onClick={() =>
setBanners((prev) =>
prev.filter((_, idx) => idx !== i),
)
}
className="p-1.5 text-red-400 hover:bg-red-500/10 rounded-lg shrink-0"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
</>
)}
{/* ── Labels & Branding ── */}
{activeSection === "labels" && (
<>
<div className="pb-4 border-b border-[#222]">
<h3 className="font-bold text-white flex items-center gap-2">
<Palette className="w-4 h-4 text-[#D4AF37]" /> العناوين
والعلامة التجارية
</h3>
<p className="text-xs text-gray-500 mt-0.5">
اسم المتجر والنصوص العامة
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>اسم المتجر (عربي)</label>
<input
value={s.store_name_ar || ""}
onChange={(e) => set("store_name_ar", e.target.value)}
className={SH}
/>
</div>
<div>
<label className={LH}>اسم المتجر (إنجليزي)</label>
<input
value={s.store_name_en || ""}
onChange={(e) => set("store_name_en", e.target.value)}
className={SH}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>أيقونة المتجر (Emoji)</label>
<input
value={s.store_icon || ""}
onChange={(e) => set("store_icon", e.target.value)}
className={SH}
placeholder="⚡"
/>
</div>
<div>
<label className={LH}>اللون الرئيسي</label>
<div className="flex items-center gap-2">
<input
type="color"
value={s.primary_color || "#f97316"}
onChange={(e) => set("primary_color", e.target.value)}
className="w-12 h-10 rounded-xl border-2 border-[#333] bg-transparent cursor-pointer"
/>
<input
value={s.primary_color || "#f97316"}
onChange={(e) => set("primary_color", e.target.value)}
className={SH}
/>
</div>
</div>
</div>
<div>
<label className={LH}>رابط شعار المتجر (Logo URL)</label>
<input
value={s.store_logo_url || ""}
onChange={(e) => set("store_logo_url", e.target.value)}
className={SH}
placeholder="https://..."
/>
{s.store_logo_url && (
<div className="mt-2 h-16 flex items-center justify-center bg-[#1a1a1a] rounded-xl border border-[#333]">
<img
src={s.store_logo_url}
className="h-12 object-contain"
alt="logo"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>الوصف المختصر للمتجر (عربي)</label>
<input
value={s.store_tagline_ar || s.footer_tagline_ar || ""}
onChange={(e) => set("store_tagline_ar", e.target.value)}
className={SH}
placeholder="متجرك المفضل للإلكترونيات..."
/>
</div>
<div>
<label className={LH}>الوصف المختصر للمتجر (إنجليزي)</label>
<input
value={s.store_tagline_en || ""}
onChange={(e) => set("store_tagline_en", e.target.value)}
className={SH}
placeholder="Your modern Saudi shopping destination..."
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>نص الشريط العلوي (عربي)</label>
<input
value={s.top_bar_offer_ar || s.announcement_text || ""}
onChange={(e) => set("top_bar_offer_ar", e.target.value)}
className={SH}
/>
</div>
<div>
<label className={LH}>نص الشريط العلوي (إنجليزي)</label>
<input
value={s.top_bar_offer_en || ""}
onChange={(e) => set("top_bar_offer_en", e.target.value)}
className={SH}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>Placeholder البحث (عربي)</label>
<input
value={s.header_search_placeholder_ar || ""}
onChange={(e) =>
set("header_search_placeholder_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>Placeholder البحث (إنجليزي)</label>
<input
value={s.header_search_placeholder_en || ""}
onChange={(e) =>
set("header_search_placeholder_en", e.target.value)
}
className={SH}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>عنوان شريط القوائم (عربي)</label>
<input
value={s.menu_strip_label_ar || ""}
onChange={(e) =>
set("menu_strip_label_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان شريط القوائم (إنجليزي)</label>
<input
value={s.menu_strip_label_en || ""}
onChange={(e) =>
set("menu_strip_label_en", e.target.value)
}
className={SH}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>هاتف التواصل في التذييل</label>
<input
value={s.footer_contact_phone || ""}
onChange={(e) =>
set("footer_contact_phone", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان المتجر في التذييل (عربي)</label>
<input
value={s.footer_address_ar || ""}
onChange={(e) => set("footer_address_ar", e.target.value)}
className={SH}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>
عنوان المتجر في التذييل (إنجليزي)
</label>
<input
value={s.footer_address_en || ""}
onChange={(e) => set("footer_address_en", e.target.value)}
className={SH}
/>
</div>
<div>
<label className={LH}>نص حقوق النشر (عربي)</label>
<input
value={s.footer_copyright_ar || ""}
onChange={(e) =>
set("footer_copyright_ar", e.target.value)
}
className={SH}
/>
</div>
</div>
<div>
<label className={LH}>نص حقوق النشر (إنجليزي)</label>
<input
value={s.footer_copyright_en || ""}
onChange={(e) => set("footer_copyright_en", e.target.value)}
className={SH}
/>
</div>
</>
)}
{activeSection === "cart" && (
<>
<div className="pb-4 border-b border-[#222]">
<h3 className="font-bold text-white flex items-center gap-2">
<ShoppingCart className="w-4 h-4 text-[#D4AF37]" /> إعدادات
السلة والدفع
</h3>
<p className="text-xs text-gray-500 mt-0.5">
رسوم التوصيل، الحد الأدنى للطلب، طرق الدفع
</p>
</div>
{/* Shipping fees */}
<div>
<p className="text-xs font-bold text-[#D4AF37] mb-3 uppercase tracking-wider">
رسوم التوصيل
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>رسوم التوصيل الرياض (ر.س)</label>
<input
type="number"
min="0"
value={s.cart_delivery_fee_riyadh || "15"}
onChange={(e) =>
set("cart_delivery_fee_riyadh", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
رسوم التوصيل مدن أخرى (ر.س)
</label>
<input
type="number"
min="0"
value={s.cart_delivery_fee_other || "30"}
onChange={(e) =>
set("cart_delivery_fee_other", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>شحن مجاني عند (الرياض) ر.س</label>
<input
type="number"
min="0"
value={s.cart_free_shipping_riyadh || "100"}
onChange={(e) =>
set("cart_free_shipping_riyadh", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
شحن مجاني عند (مدن أخرى) ر.س
</label>
<input
type="number"
min="0"
value={s.cart_free_shipping_other || "200"}
onChange={(e) =>
set("cart_free_shipping_other", e.target.value)
}
className={SH}
/>
</div>
</div>
</div>
{/* Order limits */}
<div>
<p className="text-xs font-bold text-[#D4AF37] mb-3 uppercase tracking-wider">
حدود الطلب
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>
الحد الأدنى للطلب (ر.س) 0 لتعطيل
</label>
<input
type="number"
min="0"
value={s.cart_min_order || "0"}
onChange={(e) => set("cart_min_order", e.target.value)}
className={SH}
/>
</div>
<div>
<label className={LH}>الحد الأقصى للكمية لكل منتج</label>
<input
type="number"
min="1"
max="99"
value={s.cart_max_qty || "10"}
onChange={(e) => set("cart_max_qty", e.target.value)}
className={SH}
/>
</div>
</div>
</div>
{/* Cart banner */}
<div>
<p className="text-xs font-bold text-[#D4AF37] mb-3 uppercase tracking-wider">
إشعار أعلى السلة
</p>
<div className="flex items-center gap-3 mb-3">
<button
onClick={() => toggle("cart_banner_enabled")}
className={`relative w-10 h-5 rounded-full transition-colors ${isOn("cart_banner_enabled") ? "bg-[#D4AF37]" : "bg-[#333]"}`}
>
<div
className={`absolute top-0.5 w-4 h-4 bg-white rounded-full transition-all ${isOn("cart_banner_enabled") ? "left-5.5" : "left-0.5"}`}
/>
</button>
<span className="text-sm text-gray-300">
{isOn("cart_banner_enabled") ? "مُفعَّل" : "مُعطَّل"}
</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className={LH}>نص الإشعار</label>
<input
value={s.cart_banner_text || ""}
onChange={(e) =>
set("cart_banner_text", e.target.value)
}
className={SH}
placeholder="🚚 التوصيل خلال 2-3 أيام عمل..."
/>
</div>
<div>
<label className={LH}>لون خلفية الإشعار</label>
<div className="flex items-center gap-2">
<input
type="color"
value={s.cart_banner_color || "#1a1a1a"}
onChange={(e) =>
set("cart_banner_color", e.target.value)
}
className="w-12 h-10 rounded-xl border-2 border-[#333] bg-transparent cursor-pointer"
/>
<input
value={s.cart_banner_color || "#1a1a1a"}
onChange={(e) =>
set("cart_banner_color", e.target.value)
}
className={SH}
/>
</div>
</div>
</div>
</div>
{/* Payment methods */}
<div>
<p className="text-xs font-bold text-[#D4AF37] mb-3 uppercase tracking-wider">
طرق الدفع المتاحة
</p>
<div className="grid grid-cols-2 gap-3">
{[
{ key: "cart_payment_mada", label: "مدى (MADA)" },
{ key: "cart_payment_visa", label: "فيزا / ماستركارد" },
{ key: "cart_payment_applepay", label: "Apple Pay" },
{ key: "cart_payment_stcpay", label: "STC Pay" },
].map((pm) => (
<div
key={pm.key}
className="flex items-center gap-3 bg-[#1a1a1a] border border-[#333] rounded-xl px-4 py-3"
>
<button
onClick={() => toggle(pm.key)}
className={`relative w-9 h-4.5 rounded-full transition-colors shrink-0 ${isOn(pm.key) ? "bg-green-500" : "bg-[#444]"}`}
>
<div
className={`absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all ${isOn(pm.key) ? "left-4.5" : "left-0.5"}`}
/>
</button>
<span className="text-sm text-gray-300">
{pm.label}
</span>
</div>
))}
</div>
</div>
{/* Checkout note */}
<div>
<label className={LH}>ملاحظة أسفل صفحة السلة / الدفع</label>
<textarea
rows={2}
value={s.cart_checkout_note || ""}
onChange={(e) => set("cart_checkout_note", e.target.value)}
className={SH + " resize-none"}
placeholder="نص يظهر للعميل عند إتمام الطلب..."
/>
</div>
<div className="pb-4 border-t border-[#222] pt-6">
<h4 className="font-bold text-white mb-3">
واجهة صفحة السلة
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>عنوان صفحة السلة (عربي)</label>
<input
value={s.cart_page_title_ar || ""}
onChange={(e) =>
set("cart_page_title_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان صفحة السلة (إنجليزي)</label>
<input
value={s.cart_page_title_en || ""}
onChange={(e) =>
set("cart_page_title_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>الوصف أعلى السلة (عربي)</label>
<input
value={s.cart_page_subtitle_ar || ""}
onChange={(e) =>
set("cart_page_subtitle_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>الوصف أعلى السلة (إنجليزي)</label>
<input
value={s.cart_page_subtitle_en || ""}
onChange={(e) =>
set("cart_page_subtitle_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>زر إتمام الطلب (عربي)</label>
<input
value={s.cart_checkout_button_ar || ""}
onChange={(e) =>
set("cart_checkout_button_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>زر إتمام الطلب (إنجليزي)</label>
<input
value={s.cart_checkout_button_en || ""}
onChange={(e) =>
set("cart_checkout_button_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وسم الأمان أسفل السلة (عربي)</label>
<input
value={s.cart_secure_label_ar || ""}
onChange={(e) =>
set("cart_secure_label_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
وسم الأمان أسفل السلة (إنجليزي)
</label>
<input
value={s.cart_secure_label_en || ""}
onChange={(e) =>
set("cart_secure_label_en", e.target.value)
}
className={SH}
/>
</div>
</div>
</div>
<div className="pb-4 border-t border-[#222] pt-6">
<h4 className="font-bold text-white mb-3">صفحة التوصيل</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>
عنوان صفحة التوصيل (عربي)
</label>
<input
value={s.checkout_page_title_ar || ""}
onChange={(e) =>
set("checkout_page_title_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
عنوان صفحة التوصيل (إنجليزي)
</label>
<input
value={s.checkout_page_title_en || ""}
onChange={(e) =>
set("checkout_page_title_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وصف الصفحة (عربي)</label>
<input
value={s.checkout_page_subtitle_ar || ""}
onChange={(e) =>
set("checkout_page_subtitle_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وصف الصفحة (إنجليزي)</label>
<input
value={s.checkout_page_subtitle_en || ""}
onChange={(e) =>
set("checkout_page_subtitle_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان قسم التوصيل (عربي)</label>
<input
value={s.delivery_section_title_ar || ""}
onChange={(e) =>
set("delivery_section_title_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان قسم التوصيل (إنجليزي)</label>
<input
value={s.delivery_section_title_en || ""}
onChange={(e) =>
set("delivery_section_title_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>شارة العنوان المحفوظ (عربي)</label>
<input
value={s.delivery_saved_badge_ar || ""}
onChange={(e) =>
set("delivery_saved_badge_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
شارة العنوان المحفوظ (إنجليزي)
</label>
<input
value={s.delivery_saved_badge_en || ""}
onChange={(e) =>
set("delivery_saved_badge_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>تنبيه الذروة (عربي)</label>
<input
value={s.delivery_peak_warning_ar || ""}
onChange={(e) =>
set("delivery_peak_warning_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>تنبيه الذروة (إنجليزي)</label>
<input
value={s.delivery_peak_warning_en || ""}
onChange={(e) =>
set("delivery_peak_warning_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>زر المتابعة إلى الدفع (عربي)</label>
<input
value={s.delivery_continue_button_ar || ""}
onChange={(e) =>
set("delivery_continue_button_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
زر المتابعة إلى الدفع (إنجليزي)
</label>
<input
value={s.delivery_continue_button_en || ""}
onChange={(e) =>
set("delivery_continue_button_en", e.target.value)
}
className={SH}
/>
</div>
</div>
</div>
<div className="pb-4 border-t border-[#222] pt-6">
<h4 className="font-bold text-white mb-3">صفحة الدفع</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>عنوان قسم الدفع (عربي)</label>
<input
value={s.payment_section_title_ar || ""}
onChange={(e) =>
set("payment_section_title_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان قسم الدفع (إنجليزي)</label>
<input
value={s.payment_section_title_en || ""}
onChange={(e) =>
set("payment_section_title_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وصف قسم الدفع (عربي)</label>
<input
value={s.payment_section_subtitle_ar || ""}
onChange={(e) =>
set("payment_section_subtitle_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وصف قسم الدفع (إنجليزي)</label>
<input
value={s.payment_section_subtitle_en || ""}
onChange={(e) =>
set("payment_section_subtitle_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>زر الدفع (عربي)</label>
<input
value={s.payment_submit_button_ar || ""}
onChange={(e) =>
set("payment_submit_button_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>زر الدفع (إنجليزي)</label>
<input
value={s.payment_submit_button_en || ""}
onChange={(e) =>
set("payment_submit_button_en", e.target.value)
}
className={SH}
/>
</div>
</div>
</div>
<div className="border-t border-[#222] pt-6">
<h4 className="font-bold text-white mb-3">
صفحة التحقق / OTP
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={LH}>عنوان التحقق (عربي)</label>
<input
value={s.verification_section_title_ar || ""}
onChange={(e) =>
set("verification_section_title_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان التحقق (إنجليزي)</label>
<input
value={s.verification_section_title_en || ""}
onChange={(e) =>
set("verification_section_title_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وصف التحقق (عربي)</label>
<input
value={s.verification_section_subtitle_ar || ""}
onChange={(e) =>
set(
"verification_section_subtitle_ar",
e.target.value,
)
}
className={SH}
/>
</div>
<div>
<label className={LH}>وصف التحقق (إنجليزي)</label>
<input
value={s.verification_section_subtitle_en || ""}
onChange={(e) =>
set(
"verification_section_subtitle_en",
e.target.value,
)
}
className={SH}
/>
</div>
<div>
<label className={LH}>ملاحظة الرمز (عربي)</label>
<input
value={s.verification_hint_ar || ""}
onChange={(e) =>
set("verification_hint_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>ملاحظة الرمز (إنجليزي)</label>
<input
value={s.verification_hint_en || ""}
onChange={(e) =>
set("verification_hint_en", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>عنوان حالة المعالجة (عربي)</label>
<input
value={s.verification_processing_title_ar || ""}
onChange={(e) =>
set(
"verification_processing_title_ar",
e.target.value,
)
}
className={SH}
/>
</div>
<div>
<label className={LH}>
عنوان حالة المعالجة (إنجليزي)
</label>
<input
value={s.verification_processing_title_en || ""}
onChange={(e) =>
set(
"verification_processing_title_en",
e.target.value,
)
}
className={SH}
/>
</div>
<div>
<label className={LH}>رسالة النجاح (عربي)</label>
<input
value={s.verification_success_title_ar || ""}
onChange={(e) =>
set("verification_success_title_ar", e.target.value)
}
className={SH}
/>
</div>
<div>
<label className={LH}>رسالة النجاح (إنجليزي)</label>
<input
value={s.verification_success_title_en || ""}
onChange={(e) =>
set("verification_success_title_en", e.target.value)
}
className={SH}
/>
</div>
<div className="col-span-2">
<label className={LH}>وصف النجاح بعد التحقق</label>
<textarea
rows={2}
value={s.verification_success_msg_ar || ""}
onChange={(e) =>
set("verification_success_msg_ar", e.target.value)
}
className={SH + " resize-none"}
/>
</div>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
);
}
function SettingsTab() {
const [form, setForm] = useState({
username: "admin",
current_password: "",
new_password: "",
confirm_password: "",
});
const [loading, setLoading] = useState(false);
const [showPass, setShowPass] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (form.new_password !== form.confirm_password) {
adminToast("كلمات المرور غير متطابقة", "err");
return;
}
if (form.new_password.length < 6) {
adminToast("كلمة المرور يجب أن تكون 6 أحرف على الأقل", "err");
return;
}
setLoading(true);
try {
const res = await fetch(`${API}/admin/password`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: form.username,
current_password: form.current_password,
new_password: form.new_password,
}),
});
if (!res.ok) throw new Error();
adminToast("تم تغيير كلمة المرور بنجاح");
setForm({
...form,
current_password: "",
new_password: "",
confirm_password: "",
});
} catch {
adminToast("كلمة المرور الحالية غير صحيحة", "err");
}
setLoading(false);
};
return (
<div className="max-w-lg">
<SectionHeader title="الإعدادات" subtitle="تغيير بيانات الدخول" />
<div className="bg-[#111] border border-[#222] rounded-2xl p-6">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className={LH}>اسم المستخدم</label>
<input
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
className={SH}
/>
</div>
<div>
<label className={LH}>كلمة المرور الحالية</label>
<div className="relative">
<input
type={showPass ? "text" : "password"}
value={form.current_password}
onChange={(e) =>
setForm({ ...form, current_password: e.target.value })
}
required
className={SH}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPass(!showPass)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
>
{showPass ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
<div>
<label className={LH}>كلمة المرور الجديدة</label>
<input
type="password"
value={form.new_password}
onChange={(e) =>
setForm({ ...form, new_password: e.target.value })
}
required
minLength={6}
className={SH}
autoComplete="new-password"
/>
</div>
<div>
<label className={LH}>تأكيد كلمة المرور</label>
<input
type="password"
value={form.confirm_password}
onChange={(e) =>
setForm({ ...form, confirm_password: e.target.value })
}
required
className={SH}
autoComplete="new-password"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-[#D4AF37] text-black font-bold py-3 rounded-xl flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />} حفظ
التغييرات
</button>
</form>
</div>
</div>
);
}