7042 lines
275 KiB
TypeScript
7042 lines
275 KiB
TypeScript
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> | التاريخ: ${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="أفضل الإلكترونيات في المملكة العربية السعودية"
|
||
/>
|
||
<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>
|
||
);
|
||
}
|