Files
musadaq-saas/public/shell.php

73 lines
18 KiB
PHP

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مُصادَق</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root { --emerald:#10b981; --bg-base:#080c14; --bg-surface:#0d1424; --bg-elevated:#111827; --bg-hover:rgba(255,255,255,.04); --border-default:rgba(255,255,255,.10); --text-primary:#f0f6fc; --text-secondary:#8b949e; }
[data-theme="light"] { --bg-base:#f6f8fa; --bg-surface:#fff; --bg-elevated:#f0f3f7; --bg-hover:rgba(0,0,0,.04); --border-default:rgba(0,0,0,.1); --text-primary:#0d1117; --text-secondary:#57606a; }
body { font-family: "IBM Plex Sans Arabic", sans-serif; background: var(--bg-base); color: var(--text-primary); }
.mono { font-family: "IBM Plex Mono", monospace; }
.card { background: var(--bg-surface); border: 1px solid var(--border-default); border-radius: 14px; }
.nav-link { border-right: 3px solid transparent; color: var(--text-secondary); }
.nav-active { border-right-color: var(--emerald); background: rgba(16,185,129,.12); color: var(--text-primary); }
</style>
</head>
<body data-theme="dark">
<div class="min-h-screen flex">
<aside class="w-64 p-4 border-l" style="background:var(--bg-surface);border-color:var(--border-default)">
<div class="font-bold text-2xl mb-6">مُصادَق</div>
<nav id="sidebar-nav" class="space-y-2"></nav>
<div class="mt-8 pt-4 border-t space-y-2 text-sm" style="border-color:var(--border-default)">
<div id="user-meta" class="text-xs text-gray-400">-</div>
<button onclick="theme.toggle()" class="w-full card p-2">تبديل الوضع</button>
<button onclick="logout()" class="w-full card p-2 text-red-400">خروج</button>
</div>
</aside>
<div class="flex-1">
<header class="h-16 px-6 flex items-center justify-between border-b" style="border-color:var(--border-default);background:var(--bg-surface)">
<h1 id="page-title" class="text-lg font-semibold">لوحة التحكم</h1>
<button onclick="openUploadModal()" class="card px-4 py-2 text-sm">رفع فاتورة جديدة</button>
</header>
<main id="content" class="p-6"></main>
</div>
</div>
<div id="modal-overlay" class="hidden fixed inset-0 bg-black/60 p-6 items-center justify-center"></div>
<script>
const state={user:null,currentPage:null,currentParams:{},companies:[],theme:localStorage.getItem("theme")||"dark"};
const API={token:()=>localStorage.getItem("access_token"),async request(method,path,body=null,isFormData=false){const headers={"Authorization":"Bearer "+(API.token()||"")};if(!isFormData&&body)headers["Content-Type"]="application/json";const opts={method,headers};if(body)opts.body=isFormData?body:JSON.stringify(body);let res=await fetch("/api/v1"+path,opts);if(res.status===404)res=await fetch("index.php?route=/api/v1"+path,opts);if(res.status===401){logout();throw new Error("unauthorized");}const data=await res.json();if(!data.success)throw new Error(data.error?.message_ar||"Request failed");return data;},get:(p)=>API.request("GET",p),post:(p,b)=>API.request("POST",p,b),put:(p,b)=>API.request("PUT",p,b),delete:(p)=>API.request("DELETE",p),upload:(p,f)=>API.request("POST",p,f,true)};
const toast={show(msg){alert(msg);}};
const modal={open(html){const el=document.getElementById("modal-overlay");el.innerHTML='<div class="card p-6 w-full max-w-2xl">'+html+"</div>";el.classList.remove("hidden");el.classList.add("flex");},close(){const el=document.getElementById("modal-overlay");el.classList.add("hidden");el.classList.remove("flex");}};
const theme={init(){document.body.setAttribute("data-theme",state.theme);},toggle(){state.theme=state.theme==="dark"?"light":"dark";localStorage.setItem("theme",state.theme);theme.init();}};
function logout(){localStorage.removeItem("access_token");location.reload();}
function statusBadge(s){const map={approved:"text-emerald-400",rejected:"text-red-400",extracting:"text-indigo-400",extracted:"text-blue-400",validation_failed:"text-red-400",submitting:"text-amber-400",uploaded:"text-slate-400"};return `<span class="${map[s]||"text-gray-400"}">${s}</span>`;}
async function loadCompaniesCache(){const res=await API.get("/companies");state.companies=res.data||[];}
function navItems(){const role=state.user?.role||"viewer";const items=[["dashboard","لوحة التحكم"],["invoices","الفواتير"],["companies","الشركات"],["settings","الإعدادات"]];if(role==="admin"||role==="super_admin")items.push(["users","الموظفون"],["risk-monitor","مراقبة المخاطر"]);if(role==="super_admin")items.push(["admin","إدارة النظام"]);return items;}
function renderSidebar(){document.getElementById("sidebar-nav").innerHTML=navItems().map(([key,label])=>`<a href="#/${key}" data-page="${key}" class="nav-link block px-3 py-2 rounded">${label}</a>`).join("");document.getElementById("user-meta").textContent=`${state.user?.name||"-"} — ${state.user?.role||"-"}`;}
function setActive(page){document.querySelectorAll(".nav-link").forEach(el=>el.classList.toggle("nav-active",el.dataset.page===page));}
function animateCounter(el,target){const t0=performance.now();const d=1200;const step=(t)=>{const p=Math.min(1,(t-t0)/d);el.textContent=Math.floor(target*p);if(p<1)requestAnimationFrame(step)};requestAnimationFrame(step);}
async function renderDashboard(){document.getElementById("page-title").textContent="لوحة التحكم";const {data}=await API.get("/dashboard");document.getElementById("content").innerHTML=`<div class="grid md:grid-cols-4 gap-4 mb-4">${[{k:"invoices_this_month",l:"فواتير الشهر"},{k:"approved_invoices",l:"معتمدة"},{k:"companies_count",l:"الشركات"},{k:"subscription_usage_pct",l:"استهلاك الباقة"}].map(x=>`<div class="card p-4"><div class="text-sm text-gray-400">${x.l}</div><div class="text-2xl font-bold mono stat" data-v="${data[x.k]||0}">0</div></div>`).join("")}</div>${data.risk_alerts_count>0?`<div class="card p-3 mb-4 text-amber-300">⚠️ ${data.risk_alerts_count} فاتورة تحتاج مراجعة</div>`:""}<div class="grid md:grid-cols-3 gap-4"><div class="card p-4 md:col-span-2"><h3 class="mb-3">آخر 10 فواتير</h3><table class="w-full text-sm"><thead><tr class="text-gray-400"><th>شركة</th><th>رقم</th><th>مبلغ</th><th>حالة</th></tr></thead><tbody>${(data.recent_invoices||[]).map(i=>`<tr class="border-t cursor-pointer" onclick="router.navigate('invoices',{id:'${i.id}'})"><td>${i.company_name||"-"}</td><td class="mono">${i.invoice_number||"-"}</td><td class="mono">${i.grand_total||0}</td><td>${statusBadge(i.status)}</td></tr>`).join("")}</tbody></table></div><div class="card p-4"><canvas id="status-chart"></canvas></div></div><div class="card p-4 mt-4"><h3 class="mb-2">المساعد الذكي</h3><div class="flex gap-2"><input id="ai-q" class="flex-1 card p-2" placeholder="اسأل عن بيانات مكتبك..."><button onclick="askAI()" class="card px-3">إرسال</button></div><div id="ai-answer" class="mt-3 text-sm text-gray-300"></div></div>`;document.querySelectorAll(".stat").forEach(el=>animateCounter(el,Number(el.dataset.v)));new Chart(document.getElementById("status-chart"),{type:"doughnut",data:{labels:(data.status_distribution||[]).map(x=>x.status),datasets:[{data:(data.status_distribution||[]).map(x=>x.count)}]}});}
async function askAI(){const q=document.getElementById("ai-q").value.trim();if(!q)return;const res=await API.post("/ai/query",{query:q});document.getElementById("ai-answer").textContent=res.data?.answer||"لا توجد إجابة.";}
async function renderCompanies(){document.getElementById("page-title").textContent="الشركات";const res=await API.get("/companies");document.getElementById("content").innerHTML=`<div class="grid md:grid-cols-2 gap-4">${(res.data||[]).map(c=>`<div class="card p-4"><div class="font-semibold">${c.name}</div><div class="mono text-sm text-gray-400">TIN: ${c.tax_identification_number||"-"}</div><div class="mt-2 text-xs">${c.city||"-"}</div><div class="mt-3 flex gap-2"><button class="card px-2 py-1 text-xs" onclick="openJoFotaraModal('${c.id}')">إعدادات JoFotara</button></div></div>`).join("")}</div>`;}
async function renderInvoices(params={}){if(params.id)return renderInvoiceDetail(params.id);document.getElementById("page-title").textContent="الفواتير";const q=state.currentParams;const page=q.page||1;const res=await API.get(`/invoices?page=${page}&per_page=20`);const rows=res.data||[];const meta=res.meta||{};document.getElementById("content").innerHTML=`<div class="card p-4"><table class="w-full text-sm"><thead><tr class="text-gray-400"><th>الشركة</th><th>رقم الفاتورة</th><th>التاريخ</th><th>الإجمالي</th><th>الحالة</th><th>الثقة</th></tr></thead><tbody>${rows.map(i=>`<tr class="border-t cursor-pointer" onclick="router.navigate('invoices',{id:'${i.id}'})"><td>${i.company_name||"-"}</td><td class="mono">${i.invoice_number||"-"}</td><td class="mono">${i.invoice_date||"-"}</td><td class="mono">${i.grand_total||0}</td><td>${statusBadge(i.status)}</td><td class="mono">${Math.round((i.ai_confidence_score||0)*100)}%</td></tr>`).join("")}</tbody></table><div class="mt-3 flex gap-2"><button class="card px-3 py-1" ${page<=1?"disabled":""} onclick="router.navigate('invoices',{page:${page-1}})">السابق</button><span class="px-2 py-1">صفحة ${meta.page||1} / ${meta.last_page||1}</span><button class="card px-3 py-1" ${(meta.page||1)>=(meta.last_page||1)?"disabled":""} onclick="router.navigate('invoices',{page:${page+1}})">التالي</button></div></div>`;}
async function renderInvoiceDetail(id){document.getElementById("page-title").textContent="تفاصيل الفاتورة";const {data:i}=await API.get(`/invoices/${id}`);const fileUrl=`/api/v1/invoices/${i.id}/file`;document.getElementById("content").innerHTML=`<button class="mb-3 text-sm" onclick="router.navigate('invoices')">← رجوع للفواتير</button><div class="grid md:grid-cols-5 gap-4"><div class="card p-4 md:col-span-3"><div class="font-semibold mb-2">${i.supplier_name||"-"}${i.invoice_number||"-"}</div><div class="text-sm text-gray-400 mb-3">شركة: ${i.company_name||"-"} | الحالة: ${i.status}</div><table class="w-full text-sm"><thead><tr><th>الوصف</th><th>كمية</th><th>سعر</th><th>مجموع</th></tr></thead><tbody>${(i.lines||[]).map(l=>`<tr class="border-t"><td>${l.description||"-"}</td><td class="mono">${l.quantity||0}</td><td class="mono">${l.unit_price||0}</td><td class="mono">${l.line_total||0}</td></tr>`).join("")}</tbody></table></div><div class="card p-2 md:col-span-2"><iframe src="${fileUrl}" class="w-full h-[520px]"></iframe><a class="text-xs underline" target="_blank" href="${fileUrl}">فتح في تبويب جديد</a></div></div>`;}
async function renderRiskMonitor(){document.getElementById("page-title").textContent="مراقبة المخاطر";const res=await API.get("/risks");document.getElementById("content").innerHTML=`<div class="card p-4"><table class="w-full text-sm"><thead><tr class="text-gray-400"><th>الشركة</th><th>نوع المخاطر</th><th>النقاط</th><th>الفاتورة</th><th></th></tr></thead><tbody>${(res.data||[]).map(r=>`<tr class="border-t"><td>${r.company_name||"-"}</td><td>${r.risk_type||"-"}</td><td class="mono">${r.score||0}</td><td>${r.invoice_number||"-"}</td><td><button class="card px-2 py-1 text-xs" onclick="resolveRisk('${r.id}')">حل</button></td></tr>`).join("")}</tbody></table></div>`;}
async function resolveRisk(id){await API.put(`/risks/${id}/resolve`,{});toast.show("تم حل التنبيه");renderRiskMonitor();}
async function renderSettings(){document.getElementById("page-title").textContent="الإعدادات";const me=(await API.get("/auth/me")).data;const keys=(await API.get("/api-keys")).data||[];document.getElementById("content").innerHTML=`<div class="grid md:grid-cols-2 gap-4"><div class="card p-4"><h3 class="mb-2">الملف الشخصي</h3><div>${me.name}</div><div class="text-sm text-gray-400">${me.email}</div></div><div class="card p-4"><h3 class="mb-2">مفاتيح API</h3><button class="card px-3 py-1 text-sm mb-2" onclick="createApiKey()">+ إنشاء مفتاح جديد</button>${keys.map(k=>`<div class="text-xs border-t pt-2 mt-2 mono">${k.public_key}</div>`).join("")}</div></div>`;}
async function createApiKey(){const res=await API.post("/api-keys",{name:"UI Key"});modal.open(`<h3 class="text-lg mb-3">تم إنشاء مفتاح</h3><div class="mono text-sm">Public: ${res.data.public_key}</div><div class="mono text-sm mt-2">Secret: ${res.data.secret}</div><div class="text-xs mt-2 text-red-300">احفظ السر الآن، لن يظهر مرة أخرى.</div><button class="card px-3 py-1 mt-4" onclick="modal.close()">إغلاق</button>`);}
async function renderAdmin(){document.getElementById("page-title").textContent="إدارة النظام";const stats=(await API.get("/admin/stats")).data;const queue=(await API.get("/admin/queue")).data;document.getElementById("content").innerHTML=`<div class="grid md:grid-cols-2 gap-4"><div class="card p-4"><div>المستأجرون: <span class="mono">${stats.tenants}</span></div><div>الفواتير: <span class="mono">${stats.invoices}</span></div><div>المستخدمون: <span class="mono">${stats.users}</span></div></div><div class="card p-4"><h3>الصف</h3><pre class="text-xs">${JSON.stringify(queue.counts||[],null,2)}</pre></div></div>`;}
async function renderUsers(){document.getElementById("page-title").textContent="الموظفون";const res=await API.get("/users");document.getElementById("content").innerHTML=`<div class="card p-4">${(res.data||[]).map(u=>`<div class="border-t py-2">${u.name}${u.role}</div>`).join("")}</div>`;}
async function openUploadModal(){if(state.companies.length===0)await loadCompaniesCache();modal.open(`<h3 class="text-lg mb-3">رفع فاتورة جديدة</h3><form id="upf" class="space-y-3"><select name="company_id" class="card p-2 w-full" required><option value="">اختر الشركة...</option>${state.companies.map(c=>`<option value="${c.id}">${c.name}</option>`).join("")}</select><input type="file" name="file" accept=".pdf,image/*" required class="card p-2 w-full"><div class="text-xs text-gray-400">الحد الأقصى: 20MB</div><button class="card px-3 py-2">رفع ومعالجة</button><button type="button" class="card px-3 py-2 mr-2" onclick="modal.close()">إلغاء</button></form><div id="up-status" class="text-sm mt-2"></div>`);document.getElementById("upf").onsubmit=async(e)=>{e.preventDefault();const fd=new FormData(e.target);const out=document.getElementById("up-status");out.textContent="جاري الرفع...";const r=await API.upload("/invoices/upload",fd);const id=r.data.invoice_id;out.textContent="جاري استخراج البيانات بالذكاء الاصطناعي...";const poll=async()=>{const s=(await API.get(`/invoices/${id}/status`)).data;if(["extracted","validation_failed","approved","rejected"].includes(s.status)){modal.close();router.navigate("invoices");return;}setTimeout(poll,3000)};setTimeout(poll,3000);};}
function openJoFotaraModal(companyId){modal.open(`<h3 class="text-lg mb-3">إعدادات JoFotara</h3><form id="jf" class="space-y-2"><input name="client_id" placeholder="Client ID" class="card p-2 w-full"><input type="password" name="secret_key" placeholder="Secret Key" class="card p-2 w-full"><input name="income_source_sequence" placeholder="Income Source Sequence" class="card p-2 w-full"><button class="card px-3 py-2">حفظ الإعدادات</button></form>`);document.getElementById("jf").onsubmit=async(e)=>{e.preventDefault();await API.put(`/companies/${companyId}/jofotara`,Object.fromEntries(new FormData(e.target)));modal.close();toast.show("تم حفظ الإعدادات");};}
const router={routes:{dashboard:renderDashboard,companies:renderCompanies,invoices:renderInvoices,users:renderUsers,"risk-monitor":renderRiskMonitor,settings:renderSettings,admin:renderAdmin},navigate(path,params={}){state.currentPage=path;state.currentParams=params;if(params.id)window.location.hash="#/"+path+"/"+params.id;else if(params.page)window.location.hash="#/"+path+"?page="+params.page;else window.location.hash="#/"+path;setActive(path.split("/")[0]);const fn=this.routes[path]||this.routes[path.split("/")[0]];if(fn)fn(params);},init(){const h=(window.location.hash||"#/dashboard").replace("#/","");const [pRaw,id]=h.split("/");const [p,q]=pRaw.split("?");const params={};if(id)params.id=id;if(q?.startsWith("page="))params.page=Number(q.split("=")[1]);this.navigate(p||"dashboard",params);window.addEventListener("hashchange",()=>this.init());}};
async function initAuth(){if(!API.token())return renderLogin();try{const me=(await API.get("/auth/me")).data;state.user=me;renderSidebar();await loadCompaniesCache();theme.init();router.init();}catch(e){renderLogin();}}
function renderLogin(){document.getElementById("content").innerHTML=`<div class="max-w-md mx-auto card p-6 mt-10"><h2 class="text-xl mb-4">تسجيل الدخول</h2><form id="lf" class="space-y-3"><input type="email" class="card p-2 w-full" name="email" placeholder="email" required><input type="password" class="card p-2 w-full" name="password" placeholder="password" required><button class="card px-4 py-2">دخول</button></form></div>`;document.getElementById("lf").onsubmit=async(e)=>{e.preventDefault();const res=await API.post("/auth/login",Object.fromEntries(new FormData(e.target)));if(res.data?.access_token){localStorage.setItem("access_token",res.data.access_token);location.reload();}};}
initAuth();
</script>
</body>
</html>