import React, { useState, useEffect, useCallback, useRef, useMemo } from "https://esm.sh/react@18.2.0"; import { createRoot } from "https://esm.sh/react-dom@18.2.0/client"; import { Wallet, CreditCard, TrendingDown, ShoppingCart, LogOut, Shield, Users, Plus, Trash2, Download, Search, Package, Receipt, CheckCircle, X, Eye, EyeOff, Settings, BarChart3, Wifi, AlertCircle, ChevronDown, ChevronUp, Upload, Copy, Ban, Check, Clock, MapPin, RefreshCw, ArrowUpRight, Phone, ChevronsUpDown, ArrowRight, Send, Trophy, Zap, UserCheck, ArrowLeft, Power, Edit2, } from "https://esm.sh/lucide-react@0.383.0?deps=react@18.2.0"; /* ============================================================ MikroTik Cards Shop — النسخة 3 ============================================================ */ const CURRENCY = "ريال"; const fmt = (n) => new Intl.NumberFormat("ar-YE").format(Math.round(n || 0)); const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7); const now = () => new Date().toISOString(); const fmtDate = (iso) => iso ? new Date(iso).toLocaleString("ar-YE", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "-"; const fmtDay = (iso) => iso ? new Date(iso).toLocaleDateString("ar-YE", { year: "numeric", month: "short", day: "numeric" }) : "-"; function randomPass() { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; let s = ""; for (let i = 0; i < 8; i++) s += chars[Math.floor(Math.random() * chars.length)]; return s; } /* ============================================================ Auth + Encrypted API Layer - JWT في localStorage - مفتاح جلسة AES-256-GCM لتشفير كل الطلبات والردود ============================================================ */ const AUTH = { token: null, key: null, keyB64: null, role: null, uid: null, name: null }; function b64ToBytes(b64) { return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); } function bytesToB64(bytes) { let s = ""; bytes.forEach((b) => (s += String.fromCharCode(b))); return btoa(s); } async function deriveKey(b64) { return crypto.subtle.importKey("raw", b64ToBytes(b64), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); } async function aesEncrypt(plain) { const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, AUTH.key, new TextEncoder().encode(plain)); const out = new Uint8Array(iv.length + ct.byteLength); out.set(iv); out.set(new Uint8Array(ct), iv.length); return bytesToB64(out); } async function aesDecrypt(cipherB64) { const data = b64ToBytes(cipherB64); const iv = data.slice(0, 12), ct = data.slice(12); const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, AUTH.key, ct); return new TextDecoder().decode(plain); } async function setAuth({ token, sessionKey, user }) { AUTH.token = token; AUTH.keyB64 = sessionKey; AUTH.key = await deriveKey(sessionKey); AUTH.role = user?.role; AUTH.uid = user?.id; AUTH.name = user?.name; try { localStorage.setItem("netcard_token", token); localStorage.setItem("netcard_skey", sessionKey); localStorage.setItem("netcard_user", JSON.stringify(user)); } catch (e) {} } function clearAuth() { AUTH.token = AUTH.key = AUTH.keyB64 = AUTH.role = AUTH.uid = AUTH.name = null; try { localStorage.removeItem("netcard_token"); localStorage.removeItem("netcard_skey"); localStorage.removeItem("netcard_user"); } catch (e) {} } async function restoreAuth() { try { const token = localStorage.getItem("netcard_token"); const skey = localStorage.getItem("netcard_skey"); const user = JSON.parse(localStorage.getItem("netcard_user") || "null"); if (token && skey && user) { AUTH.token = token; AUTH.keyB64 = skey; AUTH.key = await deriveKey(skey); AUTH.role = user.role; AUTH.uid = user.id; AUTH.name = user.name; return user; } } catch (e) {} return null; } /** نداء API مصادق + مشفّر. يُرجع الكائن المفكوك. */ async function api(action, body = null) { const opts = { method: body !== null ? "POST" : "GET", headers: {} }; if (AUTH.token) opts.headers["Authorization"] = "Bearer " + AUTH.token; if (body !== null) { opts.headers["Content-Type"] = "application/json"; if (AUTH.key) { const enc = await aesEncrypt(JSON.stringify(body)); opts.body = JSON.stringify({ enc }); } else { opts.body = JSON.stringify(body); } } const res = await fetch("api.php?action=" + action, opts); const text = await res.text(); let data; try { data = JSON.parse(text); } catch (e) { return { error: "invalid_response", raw: text }; } if (res.status === 401) { clearAuth(); return { error: "unauthorized" }; } if (data && data.enc && AUTH.key) { try { return JSON.parse(await aesDecrypt(data.enc)); } catch (e) { return { error: "decrypt_failed" }; } } return data; } /** تسجيل الدخول (غير مشفّر — نقطة عامة) */ async function apiLogin(role, phone, password) { try { const res = await fetch("api.php?action=login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role, phone, password }), }); return await res.json(); } catch (e) { return { error: "network", message: String(e.message || e) }; } } /* ============================================================ Telegram — عبر الـ backend (مشفّر + مصادق) ============================================================ */ async function notifyTelegram(_t, _c, text) { const r = await api("tg-message", { text }); return r.ok ? { ok: true } : { ok: false, reason: r.reason || r.description || r.error || "telegram_err" }; } async function sendTelegramFile(_t, _c, filename, _content, caption) { const r = await api("tg-backup", { filename, caption }); return r.ok ? { ok: true } : { ok: false, reason: r.reason || r.description || r.error || "telegram_err" }; } /* ============================================================ Data layer ============================================================ */ const DEFAULT_DB = { defaultDebtLimit: 5000, telegramBotToken: "", telegramChatId: "", lastBackupAt: 0, customers: {}, agents: {}, categories: [], transactions: [], depositRequests: [], }; function migrate(db) { const d = { ...DEFAULT_DB, ...db }; if (!d.agents) d.agents = {}; d.customers = d.customers || {}; Object.values(d.customers).forEach((c) => { if (c.region == null) c.region = ""; if (typeof c.banned !== "boolean") c.banned = false; if (!c.createdAt) c.createdAt = now(); }); Object.values(d.agents).forEach((a) => { if (!a.regions) a.regions = []; if (!a.createdAt) a.createdAt = now(); }); (d.categories || []).forEach((cat) => { (cat.codes || []).forEach((c) => { if (!c.addedAt) c.addedAt = now(); }); }); return d; } async function loadDB() { const data = await api("load"); if (data && !data.error && typeof data === "object") return migrate(data); return null; } async function saveDB(db) { const r = await api(AUTH.role === "customer" ? "customer-save" : "save", { state: db }); return !!(r && r.ok); } /* ============================================================ App ============================================================ */ function App() { const [db, setDb] = useState(null); const [booting, setBooting] = useState(true); const [session, setSession] = useState(null); const [toast, setToast] = useState(null); const [modal, setModal] = useState(null); const [dialog, setDialog] = useState(null); const showToast = useCallback((msg, kind = "ok") => { setToast({ msg, kind }); setTimeout(() => setToast(null), 2800); }, []); const askConfirm = useCallback((opts, onYes) => { setDialog({ kind: "confirm", ...opts, onYes: () => { setDialog(null); onYes && onYes(); } }); }, []); const askPrompt = useCallback((opts, onSubmit) => { setDialog({ kind: "prompt", ...opts, onSubmit: (v) => { setDialog(null); onSubmit && onSubmit(v); } }); }, []); // جلب البيانات بعد المصادقة const refresh = useCallback(async () => { const data = await loadDB(); if (data) setDb(data); return data; }, []); // استعادة جلسة محفوظة عند بدء التطبيق useEffect(() => { (async () => { const user = await restoreAuth(); if (user) { const data = await loadDB(); if (data) { setDb(data); setSession({ role: user.role, phone: user.id }); } else { clearAuth(); } } setBooting(false); })(); }, []); const persist = useCallback((updater) => { setDb((prev) => { const next = typeof updater === "function" ? updater(prev) : updater; saveDB(next); return next; }); }, []); // عند نجاح تسجيل الدخول const onLoggedIn = useCallback(async (user) => { const data = await loadDB(); setDb(data || JSON.parse(JSON.stringify(DEFAULT_DB))); setSession({ role: user.role, phone: user.id }); }, []); const onLogout = useCallback(async () => { try { await api("logout"); } catch (e) {} clearAuth(); setSession(null); setDb(null); }, []); // نسخة احتياطية تلقائية يومية لتيليجرام (عند دخول الأدمن) useEffect(() => { if (!db || !session || session.role !== "admin") return; if (!db.telegramBotToken || !db.telegramChatId) return; const DAY = 24 * 60 * 60 * 1000; if (Date.now() - (db.lastBackupAt || 0) < DAY) return; (async () => { const stamp = new Date().toISOString().slice(0, 10); const r = await sendTelegramFile(null, null, `netcard_backup_${stamp}.json`, null, `🗄️ نسخة احتياطية تلقائية\n📅 ${stamp}`); if (r.ok) { await refresh(); showToast("تم رفع نسخة احتياطية تلقائية إلى تيليجرام", "ok"); } })(); }, [db, session, refresh, showToast]); if (booting) return ; const helpers = { showToast, askConfirm, askPrompt, setModal, refresh }; return ( {!session ? ( ) : !db ? ( ) : session.role === "admin" ? ( ) : session.role === "agent" ? ( ) : ( )} setModal(null)} /> setDialog(null)} /> ); } /* ============================================================ Shell + Loader ============================================================ */ function Shell({ children }) { return (
{children}
); } function SplashLoader() { return (

جارٍ التحميل…

); } /* ============================================================ Login (3 tabs: customer / agent / admin) ============================================================ */ function LoginScreen({ onLoggedIn, showToast }) { const [tab, setTab] = useState("customer"); const [phone, setPhone] = useState(""); const [p, setP] = useState(""); const [show, setShow] = useState(false); const [busy, setBusy] = useState(false); const [lockUntil, setLockUntil] = useState(0); // timestamp ms const [secsLeft, setSecsLeft] = useState(0); // عدّاد القفل التنازلي useEffect(() => { if (!lockUntil) return; const tick = () => { const left = Math.ceil((lockUntil - Date.now()) / 1000); setSecsLeft(left > 0 ? left : 0); if (left <= 0) setLockUntil(0); }; tick(); const id = setInterval(tick, 500); return () => clearInterval(id); }, [lockUntil]); const locked = secsLeft > 0; const submit = async () => { if (busy || locked) return; if (tab !== "admin" && !phone.trim()) return showToast("أدخل رقم الهاتف", "err"); if (!p) return showToast("أدخل كلمة المرور", "err"); setBusy(true); const res = await apiLogin(tab, phone.trim(), p); setBusy(false); if (res.ok && res.token) { await setAuth({ token: res.token, sessionKey: res.sessionKey, user: res.user }); setP(""); onLoggedIn(res.user); return; } if (res.error === "locked") { setLockUntil(Date.now() + (res.secondsLeft || 120) * 1000); showToast(res.message || "تم قفل الحساب مؤقتاً", "err"); } else if (res.error === "banned") { showToast(res.message || "تم حظر هذا الحساب", "err"); } else if (res.error === "invalid") { const rem = res.remaining; showToast(rem != null ? `بيانات خاطئة. تبقّى ${rem} ${rem === 1 ? "محاولة" : "محاولات"}` : "بيانات الدخول غير صحيحة", "err"); } else if (res.error === "network") { showToast("تعذّر الاتصال بالخادم. تأكّد أن Apache يعمل", "err"); } else { showToast(res.message || "فشل تسجيل الدخول", "err"); } }; const mmss = (s) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; return (

NETCARD

منصة بيع كروت المايكروتك

{tab !== "admin" && (
setPhone(e.target.value)} placeholder="7XXXXXXXX" inputMode="tel" disabled={locked} onKeyDown={(e) => e.key === "Enter" && submit()} />
)}
setP(e.target.value)} placeholder="••••••" onKeyDown={(e) => e.key === "Enter" && submit()} />
{locked ? (
محظور مؤقتاً بسبب المحاولات الخاطئة — حاول بعد {mmss(secsLeft)}
) : null}
); } /* ============================================================ Customer Panel ============================================================ */ function CustomerPanel({ db, persist, phone, onLogout, helpers }) { const c = db.customers[phone]; const [view, setView] = useState("shop"); const [purchasedCard, setPurchasedCard] = useState(null); // { code, category, price, speed, validity } const myTx = db.transactions.filter((t) => t.customer === phone).sort((a, b) => b.date.localeCompare(a.date)); const myDeposits = db.depositRequests.filter((r) => r.customer === phone).sort((a, b) => b.date.localeCompare(a.date)); const buy = async (cat) => { const available = cat.codes.some((x) => !x.used); if (!available) return helpers.showToast("لا توجد كروت متاحة في هذه الفئة", "err"); const r = await api("customer-buy", { categoryId: cat.id }); if (!r || !r.ok) return helpers.showToast(r?.error || "تعذّر الشراء", "err"); await helpers.refresh(); setPurchasedCard({ code: r.code, category: cat.name, price: cat.price, speed: cat.speed, validity: cat.validity }); }; return (
}, { id: "deposit", label: "إيداع", icon: }, { id: "history", label: "سجل المعاملات", icon: }, { id: "settings", label: "الإعدادات", icon: }, ]} view={view} setView={setView} />
} label="رصيد المحفظة" value={`${fmt(c.balance)} ${CURRENCY}`} tone="green" action={} /> } label="المديونية" value={`${fmt(c.debt)} ${CURRENCY}`} tone="red" /> } label="حد المديونية" value={`${fmt(c.debtLimit)} ${CURRENCY}`} tone="blue" />
{view === "shop" && (
{db.categories.length === 0 && } {db.categories.map((cat) => { const available = cat.codes.some((x) => !x.used); return (
{!available && نفدت}

{cat.name}

السرعة: {cat.speed}المدة: {cat.validity}
{fmt(cat.price)} {CURRENCY}
); })}
)} {view === "deposit" && } {view === "history" && } {view === "settings" && } {purchasedCard && setPurchasedCard(null)} showToast={helpers.showToast} />}
); } function PurchaseModal({ card, onClose, showToast }) { const copy = () => { navigator.clipboard.writeText(card.code); showToast("تم نسخ الكود", "ok"); }; return (
e.stopPropagation()} className="rise"> {/* Header */}

تمّت العملية بنجاح!

كرتك جاهز للاستخدام

{/* The Card */}
NETCARD
{card.category}

رقم الكرت

{card.code}

السرعة: {card.speed} المدة: {card.validity} السعر: {fmt(card.price)} {CURRENCY}
); } function CustomerSettings({ phone, db, persist, helpers }) { const [cur, setCur] = useState(""); const [np, setNp] = useState(""); const [cf, setCf] = useState(""); const [show, setShow] = useState(false); const c = db.customers[phone]; const change = async () => { if (!cur || !np || !cf) return helpers.showToast("املأ جميع الحقول", "err"); if (np.length < 4) return helpers.showToast("كلمة المرور الجديدة قصيرة جداً", "err"); if (np !== cf) return helpers.showToast("كلمتا المرور غير متطابقتين", "err"); const r = await api("change-my-password", { current: cur, new: np }); if (r && r.ok) { setCur(""); setNp(""); setCf(""); helpers.showToast("تم تغيير كلمة المرور بنجاح", "ok"); } else if (r && r.reason === "wrong_current") { helpers.showToast("كلمة المرور الحالية غير صحيحة", "err"); } else { helpers.showToast("تعذّر تغيير كلمة المرور", "err"); } }; return (
setCur(e.target.value)} /> setNp(e.target.value)} /> setCf(e.target.value)} />
); } function CustomerDeposit({ db, persist, phone, helpers, myDeposits }) { const [amount, setAmount] = useState(""); const [note, setNote] = useState(""); const pending = myDeposits.filter((r) => r.status === "pending"); const history = myDeposits.filter((r) => r.status !== "pending"); const submit = async () => { const v = Number(amount); if (!v || v <= 0) return helpers.showToast("أدخل مبلغاً صحيحاً", "err"); const r = await api("customer-deposit-request", { amount: v, note: note.trim() }); if (!r || !r.ok) return helpers.showToast(r?.error || "تعذّر إرسال الطلب", "err"); await helpers.refresh(); setAmount(""); setNote(""); helpers.showToast("تم إرسال طلب الإيداع. بانتظار الموافقة", "ok"); // إشعار تيليجرام يُرسَل تلقائياً من الخادم عند إنشاء الطلب }; return ( <>
setAmount(e.target.value)} />