/* Otmix Dashboard — admin overview */ const { useState, useEffect, useMemo, useRef } = React; const I = ({ d, s = 16, sw = 1.8 }) => ( {d} ); const ic = { home: >}/>, bot: >}/>, ops: >}/>, users: >}/>, reports: >}/>, card: >}/>, plug: >}/>, cog: >}/>, search: >}/>, bell: >}/>, bolt: >}/>, chev: }/>, chevR: }/>, arrow: >}/>, arrowL: >}/>, plus: >}/>, filter: >}/>, download: >}/>, more: >}/>, eye: >}/>, edit: >}/>, sun: >}/>, spark: >}/>, alert: >}/>, cash: >}/>, flame: >}/>, }; const AGENTS = [ { code:"P1", name:"الذمم الدائنة", icon:"AP", status:"live", kpi:"١٬٢٤٧", unit:"فاتورة هذا الأسبوع", spark:[8,11,9,14,12,18,17] }, { code:"P2", name:"الطلبات", icon:"OR", status:"live", kpi:"٤٢٣", unit:"طلب اليوم", spark:[10,12,15,11,18,22,28] }, { code:"P3", name:"السمعة", icon:"RP", status:"live", kpi:"٨٩", unit:"تقييم تم الرد", spark:[5,8,6,9,7,11,10] }, { code:"P4", name:"المطابقة", icon:"RC", status:"warn", kpi:"٣ استثناءات", unit:"تحتاج مراجعة", spark:[14,12,15,11,9,7,3] }, { code:"P5", name:"الرواتب", icon:"PR", status:"idle", kpi:"٢٤٨", unit:"موظف · جاهز", spark:[6,6,6,6,6,6,6] }, { code:"P6", name:"التوظيف", icon:"TL", status:"live", kpi:"٥٧", unit:"سيرة فُرزت", spark:[3,5,8,7,11,9,13] }, { code:"P7", name:"العقود", icon:"CT", status:"live", kpi:"١٢", unit:"عقد قيد المراجعة", spark:[4,6,5,7,8,10,9] }, { code:"P8", name:"التسويق", icon:"MK", status:"live", kpi:"٣٤", unit:"محتوى نُشر", spark:[7,9,11,10,13,15,14] }, { code:"P9", name:"المعرفة", icon:"KN", status:"live", kpi:"٢٬٨٤٠", unit:"محادثة هذا الأسبوع", spark:[18,22,19,25,28,32,30] }, ]; const KPIS = [ { lbl:"الفواتير المعالجة", v:"١٢٬٨٤٧", delta:"+12.4%", unit:"", spark:[12,15,14,18,17,22,21,26,28,30,29,34], dn:false }, { lbl:"الوكلاء النشطون", v:"٨", small:"/٩", delta:"+1", unit:"", spark:[7,7,7,8,8,8,8,8,8,8,8,8], dn:false }, { lbl:"التوفير الشهري", v:"٤٨٬٢٩٠", small:"ر.س", delta:"+18%", spark:[20,22,25,24,28,30,33,35,38,42,45,48], dn:false }, { lbl:"متوسط زمن المعالجة", v:"٨", small:"دقائق", delta:"-22%", spark:[18,17,15,14,13,12,11,10,9,9,8,8], dn:true }, ]; const ACTIVITY = [ { id:"#OPS-48291", type:"فاتورة", agent:"P1 · الذمم", agentColor:"AP", amt:"٤٬٨٢٠ ر.س", status:"معالج", sk:"ok", time:"قبل ٢ د" }, { id:"#ORD-12847", type:"طلب", agent:"P2 · الطلبات", agentColor:"OR", amt:"١٥٦ ر.س", status:"معالج", sk:"ok", time:"قبل ٤ د" }, { id:"#REC-00428", type:"مطابقة", agent:"P4 · المطابقة", agentColor:"RC", amt:"٢٤٬٣٤٠ ر.س", status:"استثناء", sk:"exc", time:"قبل ٧ د" }, { id:"#KNW-92041", type:"محادثة", agent:"P9 · المعرفة", agentColor:"KN", amt:"—", status:"معالج", sk:"ok", time:"قبل ١١ د" }, { id:"#OPS-48289", type:"فاتورة", agent:"P1 · الذمم", agentColor:"AP", amt:"٢٬١٤٠ ر.س", status:"قيد المراجعة", sk:"rev", time:"قبل ١٤ د" }, { id:"#TLT-04812", type:"سيرة ذاتية", agent:"P6 · التوظيف", agentColor:"TL", amt:"—", status:"معالج", sk:"ok", time:"قبل ١٨ د" }, { id:"#REP-22841", type:"تقييم", agent:"P3 · السمعة", agentColor:"RP", amt:"⭐ ٤", status:"معالج", sk:"ok", time:"قبل ٢٢ د" }, { id:"#CTR-01294", type:"عقد", agent:"P7 · العقود", agentColor:"CT", amt:"خطر متوسط", status:"قيد المراجعة", sk:"rev", time:"قبل ٢٧ د" }, ]; const INSIGHTS = [ { kind:"g", ico:ic.flame, ev:<>وكيل الفواتير اكتشف ١٢ فاتورة مكررة من Vendor X هذا الأسبوع. وفّر ٤٬٨٠٠ ر.س.>, time:"قبل ٤ د", tag:"ذمم دائنة", act:"مراجعة" }, { kind:"y", ico:ic.alert, ev:<>المطابقة البنكية سجّلت ٣ استثناءات في حساب الراجحي تحتاج موافقة يدوية.>, time:"قبل ١٢ د", tag:"مطابقة", act:"اعتماد" }, { kind:"g", ico:ic.cash, ev:<>الطلبات رفعت متوسط القيمة من ٤٢ ر.س إلى ٥٨ ر.س عبر بيع تكميلي تلقائي.>, time:"قبل ٢٥ د", tag:"طلبات", act:"عرض" }, { kind:"b", ico:ic.spark, ev:<>المعرفة أجاب على ٢٬٨٤٠ سؤال هذا الأسبوع · ٩٤٪ بثقة عالية بدون تصعيد.>, time:"قبل ساعة", tag:"معرفة", act:"عرض" }, { kind:"r", ico:ic.alert, ev:<>الرواتب · ٣ موظفين لم يكتمل تسجيلهم في GOSI قبل تنفيذ راتب الشهر.>, time:"قبل ٢ س", tag:"رواتب", act:"إصلاح" }, ]; /* ---------- Sparkline (mini) ---------- */ function Spark({ data, color = "currentColor", h = 30, fill = false }) { const w = 100; const max = Math.max(...data); const min = Math.min(...data); const range = Math.max(1, max - min); const pts = data.map((v, i) => [(i/(data.length-1))*w, h - ((v-min)/range)*(h-4) - 2]); const d = pts.map((p,i) => (i===0?"M":"L") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" "); return ( {fill && } ); } /* ---------- Big Chart ---------- */ function BigChart({ tab }) { const datasets = { invoices: [180,210,260,240,310,340,380,360,420,440,490,520,540,580,610,640,670,690,710,750,780,810,800,860,890,920,940,970,1010,1050], orders: [80,95,110,140,160,180,210,240,260,280,310,340,360,390,420,460,490,510,540,580,610,640,680,720,760,800,830,870,910,950], payroll: [40,40,42,42,44,44,46,46,48,48,50,50,52,52,54,54,56,56,58,58,60,60,62,62,64,64,66,66,68,68], all: [300,360,400,440,500,560,600,640,720,780,850,910,980,1040,1110,1180,1250,1320,1390,1460,1540,1610,1690,1770,1860,1950,2030,2110,2200,2300], }; const data = datasets[tab] || datasets.invoices; const W = 800, H = 200, PL = 40, PR = 14, PT = 18, PB = 30; const max = Math.max(...data); const min = 0; const range = max - min; const ix = i => PL + (i/(data.length-1))*(W-PL-PR); const iy = v => PT + (1 - (v-min)/range)*(H-PT-PB); const pts = data.map((v,i) => [ix(i), iy(v)]); const d = pts.map((p,i) => (i===0?"M":"L") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" "); const fillD = d + ` L ${ix(data.length-1).toFixed(1)} ${(H-PB).toFixed(1)} L ${ix(0).toFixed(1)} ${(H-PB).toFixed(1)} Z`; const [hover, setHover] = useState(null); const onMove = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const fx = (x / rect.width) * W; const idx = Math.round(((fx - PL) / (W - PL - PR)) * (data.length-1)); if (idx >= 0 && idx < data.length) setHover({ idx, v: data[idx], x: pts[idx][0], y: pts[idx][1] }); }; // y-axis ticks const ticks = [0, 0.25, 0.5, 0.75, 1].map(t => ({ y: PT + (1-t)*(H-PT-PB), v: Math.round(min + t*range) })); // x-axis ticks (every 5 days) const xTicks = [0,5,10,15,20,25,29].map(i => ({ x: ix(i), label: (i+1) })); return ( setHover(null)}> {ticks.map((t,i) => ( {t.v >= 1000 ? (t.v/1000).toFixed(1)+"k" : t.v} ))} {xTicks.map((t,i) => ( {t.label} ))} {hover && ( )} {hover && ( يوم {hover.idx+1}: {hover.v.toLocaleString("ar-EG")} )} ); } /* ---------- Donut ---------- */ function Donut() { const data = [ { code:"P1", label:"الذمم", v:32, c:"#4bec42" }, { code:"P2", label:"الطلبات", v:24, c:"#080808" }, { code:"P9", label:"المعرفة", v:18, c:"#888" }, { code:"P3", label:"السمعة", v:8, c:"#bdbdb7" }, { code:"P6", label:"التوظيف", v:6, c:"#888" }, { code:"P4", label:"المطابقة", v:5, c:"#444" }, { code:"P8", label:"التسويق", v:4, c:"#aaa" }, { code:"P7", label:"العقود", v:2, c:"#666" }, { code:"P5", label:"الرواتب", v:1, c:"#ccc" }, ]; const total = data.reduce((a,b)=>a+b.v,0); const size = 160; const r = 64; const cx = size/2; const cy = size/2; const sw = 24; let acc = 0; const C = 2*Math.PI*r; return ( {data.map((d,i) => { const frac = d.v/total; const dash = frac*C; const offset = -acc*C; acc += frac; return ( ); })} ١٤٬٢١٠ عملية / ٧ أيام {data.slice(0,6).map((d,i)=>( {d.code} · {d.label} {d.v}٪ ))} ); } /* ---------- Sidebar ---------- */ function Sidebar() { const [agentsOpen, setAgentsOpen] = useState(true); return ( ); } /* ---------- Topbar ---------- */ function Topbar({ openCmdK }) { return ( الرئيسية{ic.chevR}نظرة عامة {ic.search} ⌘K {ic.bolt} {ic.bell} أ ); } /* ---------- Command Palette ---------- */ function CmdK({ open, onClose }) { const [q, setQ] = useState(""); useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; if (open) { window.addEventListener("keydown", onKey); setQ(""); } return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); const items = [ { g:"الانتقال", items:[ { ico:ic.home, nm:"الرئيسية / نظرة عامة", kbd:"G H" }, { ico:ic.bot, nm:"أخصائي الحسابات الدائنة المؤتمت", kbd:"G P1" }, { ico:ic.bot, nm:"وكيل مبيعات الواتساب الذكي", kbd:"G P2" }, { ico:ic.ops, nm:"العمليات", kbd:"G O" }, { ico:ic.card, nm:"الفوترة والاشتراكات", kbd:"G B" }, ]}, { g:"إجراءات", items:[ { ico:ic.plus, nm:"تشغيل وكيل جديد", kbd:"⇧ N" }, { ico:ic.download, nm:"تصدير العمليات (CSV)" }, { ico:ic.spark, nm:"اطلب من Claude تحليلاً" }, ]}, ]; return ( e.stopPropagation()}> {ic.search} setQ(e.target.value)}/> ESC {items.map((g,gi)=>( {g.g} {g.items.filter(it=>!q||it.nm.includes(q)).map((it,i)=>( {it.ico} {it.nm} {it.kbd && {it.kbd}} ))} ))} ); } /* ---------- Main ---------- */ function App() { const [tab, setTab] = useState("invoices"); const [cmdOpen, setCmdOpen] = useState(false); const [counts, setCounts] = useState(KPIS.map(() => 0)); useEffect(() => { const onKey = (e) => { if ((e.metaKey||e.ctrlKey) && e.key.toLowerCase()==="k") { e.preventDefault(); setCmdOpen(true); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); return ( setCmdOpen(true)}/> {/* Welcome */} صباح الخير، أحمد 👋 💡 وكيل الفواتير اكتشف ١٢ فاتورة مكررة هذا الأسبوع — وفّر ٤٬٨٠٠ ر.س. تقرير الأسبوع تشغيل وكيل جديد {ic.arrow} {/* KPI Row */} {KPIS.map((k,i)=>( {k.lbl} {k.v}{k.small && {k.small}} {k.dn?"▼":"▲"} {k.delta} مقارنة بالشهر السابق ))} {/* Two-column row: chart + insights */} العمليات عبر آخر ٣٠ يوماً إجمالي ١٢٬٨٤٧ عملية · متوسط ٤٢٨/يوم {[["invoices","الفواتير"],["orders","الطلبات"],["payroll","الرواتب"],["all","الكل"]].map(([k,l])=>( setTab(k)}>{l} ))} عمليات يومية منطقة التراكم تحديث منذ ٤٢ ث رؤى من Claude عرض الكل {INSIGHTS.map((it,i)=>( {it.ico} {it.ev} {it.time}·{it.tag} {it.act} {ic.arrow} ))} {/* Donut + Agents */} توزيع العمليات حسب الوكيل آخر ٧ أيام أداء الوكلاء إدارة الكل {ic.arrow} {AGENTS.slice(0,6).map(a => ( {a.icon} {a.code} · {a.name} {a.status==="live"?"نشط":a.status==="warn"?"تحذير":"خامل"} {a.kpi}{a.unit} زمن التشغيل ٩٩٫٩٪ ↗ {a.code==="P4"?"-12%":"+8%"} ))} {/* Activity Table */} آخر العمليات تحديث لحظي · ٣٤٢ عملية اليوم {ic.filter} كل الوكلاء آخر ٢٤ ساعة {ic.download} تصدير CSV المعرّف النوع الوكيل القيمة الحالة الوقت {ACTIVITY.map((r,i)=>( {r.id} {r.type} {r.agentColor}{r.agent} {r.amt} {r.status} {r.time} {ic.eye}{ic.more} ))} عرض ١-٨ من ٣٤٢ {ic.chevR} ١٢٣...٤٣ {ic.chevR} setCmdOpen(false)}/> ); } ReactDOM.createRoot(document.getElementById("root")).render();