| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <link rel="icon" href="favicon.svg" type="image/svg+xml"> |
| <title>أدمن — نيّة</title> |
| <meta name="robots" content="noindex, nofollow"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&family=IBM+Plex+Sans+Arabic:wght@300;400;500;600&display=swap'); |
| :root{ |
| --bg:#FFFFFF; --ink:#0A0A0A; |
| --line: rgba(10,10,10,0.12); --line-strong: rgba(10,10,10,0.22); |
| --muted: rgba(10,10,10,0.55); --muted-soft: rgba(10,10,10,0.4); |
| --surface: rgba(10,10,10,0.03); --surface-hover: rgba(10,10,10,0.05); |
| --radius:14px; --radius-sm:10px; --ease: cubic-bezier(.22,1,.36,1); |
| --shadow-sm: 0 1px 3px rgba(10,10,10,0.05), 0 1px 2px rgba(10,10,10,0.04); |
| --shadow-md: 0 6px 20px rgba(10,10,10,0.07), 0 2px 6px rgba(10,10,10,0.04); |
| --good:#1E8E3E; --good-bg:#EAF7EE; --danger:#C00000; --danger-bg:#FDECEC; |
| } |
| *{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; } |
| html{ overflow-x:hidden; scroll-behavior:smooth; height:100%; } |
| body{ |
| font-family:'IBM Plex Sans Arabic', sans-serif; margin:0; padding:0; min-height:100vh; |
| background: var(--bg, #FFFFFF); color: var(--ink, #0A0A0A); overflow-x:hidden; -webkit-font-smoothing:antialiased; |
| } |
| .page-wrap{ max-width:1180px; margin:0 auto; padding:22px; animation: fadeIn .35s var(--ease, cubic-bezier(.22,1,.36,1)) both; } |
| h1,h2,h3,button{ font-family:'Cairo', sans-serif; } |
| @keyframes fadeIn{ from{opacity:0; transform:translateY(8px);} to{opacity:1; transform:none;} } |
| @keyframes rowIn{ from{opacity:0; transform:translateY(4px);} to{opacity:1; transform:none;} } |
| tr{ animation: rowIn .3s var(--ease, cubic-bezier(.22,1,.36,1)) both; } |
| img,video{ max-width:100%; height:auto; } |
| *{ word-wrap:break-word; overflow-wrap:break-word; } |
| |
| .topbar{ |
| display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; |
| margin-bottom:22px; padding-bottom:18px; border-bottom:1px solid var(--line, rgba(10,10,10,0.12)); |
| } |
| .topbar h1{ font-size:22px; font-weight:800; margin:0; } |
| |
| .stats{ display:flex; gap:12px; flex-wrap:wrap; margin-bottom:22px; } |
| .stat{ |
| background:var(--bg, #FFFFFF); border:1.5px solid var(--line, rgba(10,10,10,0.12)); border-radius: var(--radius-sm, 10px); |
| padding:14px 18px; font-size:13px; color:var(--muted, rgba(10,10,10,0.55)); transition: transform .2s var(--ease, cubic-bezier(.22,1,.36,1)), box-shadow .2s var(--ease, cubic-bezier(.22,1,.36,1)), border-color .2s var(--ease, cubic-bezier(.22,1,.36,1)); |
| } |
| .stat:hover{ transform: translateY(-2px); box-shadow: var(--shadow-sm, 0 1px 3px rgba(10,10,10,0.05), 0 1px 2px rgba(10,10,10,0.04)); border-color: var(--line-strong, rgba(10,10,10,0.22)); } |
| .stat b{ font-size:19px; display:block; font-family:'Cairo',sans-serif; font-weight:800; color:var(--ink, #0A0A0A); margin-top:2px; } |
| |
| .filters{ display:flex; gap:8px; margin-bottom:18px; flex-wrap:wrap; } |
| .filters button{ |
| padding:8px 18px; border-radius:999px; border:1.5px solid var(--line-strong, rgba(10,10,10,0.22)); background:var(--bg, #FFFFFF); |
| color:var(--ink, #0A0A0A); cursor:pointer; font-size:13px; font-weight:600; transition: all .18s var(--ease, cubic-bezier(.22,1,.36,1)); |
| } |
| .filters button:hover{ background: var(--surface, rgba(10,10,10,0.03)); } |
| .filters button:active{ transform: scale(.95); } |
| .filters button.active{ background:var(--ink, #0A0A0A); color:var(--bg, #FFFFFF); border-color:var(--ink, #0A0A0A); } |
| |
| input#search{ |
| padding:9px 14px; border-radius: var(--radius-sm, 10px); border:1.5px solid var(--line, rgba(10,10,10,0.12)); font-size:13.5px; |
| min-width:220px; font-family:inherit; transition: border-color .18s var(--ease, cubic-bezier(.22,1,.36,1)), box-shadow .18s var(--ease, cubic-bezier(.22,1,.36,1)); |
| } |
| input#search:focus{ outline:none; border-color:var(--ink, #0A0A0A); box-shadow:0 0 0 3.5px rgba(10,10,10,0.07); } |
| |
| select, input{ |
| font-family:inherit; border-radius: var(--radius-sm, 10px); border:1.5px solid var(--line, rgba(10,10,10,0.12)); |
| padding:6px 10px; font-size:13px; transition: border-color .18s var(--ease, cubic-bezier(.22,1,.36,1)), box-shadow .18s var(--ease, cubic-bezier(.22,1,.36,1)); |
| background:var(--bg, #FFFFFF); color:var(--ink, #0A0A0A); |
| } |
| select:focus, input:focus{ outline:none; border-color:var(--ink, #0A0A0A); box-shadow:0 0 0 3.5px rgba(10,10,10,0.07); } |
| |
| table{ border-collapse:separate; border-spacing:0; width:100%; background:var(--bg, #FFFFFF); border:1px solid var(--line, rgba(10,10,10,0.12)); border-radius: var(--radius, 14px); overflow:hidden; } |
| td, th{ border-bottom:1px solid var(--line, rgba(10,10,10,0.12)); padding:12px 14px; text-align:right; font-size:13.5px; } |
| th{ background: var(--surface, rgba(10,10,10,0.03)); font-weight:700; font-size:12.5px; color:var(--muted, rgba(10,10,10,0.55)); } |
| tr:last-child td{ border-bottom:none; } |
| tr:hover td{ background: var(--surface, rgba(10,10,10,0.03)); } |
| img{ max-width:180px; max-height:130px; border-radius: var(--radius-sm, 10px); cursor:pointer; transition: transform .2s var(--ease, cubic-bezier(.22,1,.36,1)); } |
| img:hover{ transform: scale(1.03); } |
| |
| button.act{ |
| margin:2px; padding:7px 14px; cursor:pointer; border-radius:8px; border:1.5px solid var(--line-strong, rgba(10,10,10,0.22)); |
| background:var(--bg, #FFFFFF); color:var(--ink, #0A0A0A); font-size:12.5px; font-weight:600; font-family:inherit; |
| transition: all .18s var(--ease, cubic-bezier(.22,1,.36,1)); |
| display:inline-flex; align-items:center; gap:4px; white-space:nowrap; line-height:1.3; |
| } |
| button.act:hover{ background: var(--surface, rgba(10,10,10,0.03)); } |
| button.act:active{ transform: scale(.94); } |
| button.act.approve{ background:var(--good, #1E8E3E); color:#ffffff; border-color:var(--good, #1E8E3E); } |
| button.act.reject{ background:var(--danger, #C00000); color:#ffffff; border-color:var(--danger, #C00000); } |
| |
| .status-pending, .status-open{ color:#B15C00; font-weight:700; } |
| .status-approved, .status-resolved, .status-active{ color:var(--good, #1E8E3E); font-weight:700; } |
| .status-rejected, .status-dismissed, .status-closed{ color:var(--danger, #C00000); font-weight:700; } |
| |
| .empty{ text-align:center; padding:50px 20px; color:var(--muted-soft, rgba(10,10,10,0.4)); font-size:14px; } |
| .badge{ display:inline-block; font-size:11px; font-weight:600; padding:3px 10px; border-radius:999px; background:var(--surface, rgba(10,10,10,0.03)); color:var(--muted, rgba(10,10,10,0.55)); margin-inline-start:4px; } |
| .badge.danger{ background:var(--danger-bg, #FDECEC); color:var(--danger, #C00000); } |
| .badge.good{ background:var(--good-bg, #EAF7EE); color:var(--good, #1E8E3E); } |
| |
| ::selection{ background:var(--ink, #0A0A0A); color:#fff; } |
| @media (max-width:720px){ |
| body{ padding:14px; } |
| .topbar{ margin-bottom:16px; padding-bottom:14px; } |
| table{ display:block; overflow-x:auto; -webkit-overflow-scrolling:touch; border-radius: var(--radius-sm, 10px); } |
| button.act{ min-height:38px; } |
| } |
| |
| |
| .u-overlay{ |
| display:none; position:fixed; inset:0; width:100vw; height:100vh; background:rgba(10,10,10,.5); |
| align-items:center; justify-content:center; z-index:99; padding:20px; |
| backdrop-filter: blur(2px); animation: fadeIn .18s var(--ease, cubic-bezier(.22,1,.36,1)) both; |
| } |
| .u-overlay.show{ display:flex; } |
| .u-modal{ |
| background:var(--bg, #FFFFFF); border-radius: var(--radius, 14px); width:100%; max-width:540px; |
| max-height:85vh; display:flex; flex-direction:column; box-shadow: var(--shadow-md, 0 6px 20px rgba(10,10,10,0.07), 0 2px 6px rgba(10,10,10,0.04)); |
| animation: popIn .22s var(--ease, cubic-bezier(.22,1,.36,1)) both; overflow:hidden; |
| } |
| @keyframes popIn{ from{ opacity:0; transform:scale(.96) translateY(8px);} to{ opacity:1; transform:none; } } |
| .u-modal-head{ |
| display:flex; align-items:center; justify-content:space-between; padding:16px 20px; |
| border-bottom:1px solid var(--line, rgba(10,10,10,0.12)); font-weight:700; font-size:14.5px; flex-shrink:0; |
| } |
| .u-close{ |
| border:none; background:var(--surface, rgba(10,10,10,0.03)); width:30px; height:30px; border-radius:999px; |
| display:flex; align-items:center; justify-content:center; cursor:pointer; transition: background .18s var(--ease, cubic-bezier(.22,1,.36,1)); |
| } |
| .u-close svg{ width:15px; height:15px; stroke:var(--ink, #0A0A0A); fill:none; stroke-width:2; } |
| .u-close:hover{ background:var(--line, rgba(10,10,10,0.12)); } |
| .u-modal-body{ padding:18px 20px 22px; overflow-y:auto; scrollbar-width:thin; scrollbar-color: var(--line-strong, rgba(10,10,10,0.22)) transparent; } |
| .u-modal-body::-webkit-scrollbar{ width:7px; } |
| .u-modal-body::-webkit-scrollbar-track{ background:transparent; } |
| .u-modal-body::-webkit-scrollbar-thumb{ background: var(--line-strong, rgba(10,10,10,0.22)); border-radius:999px; } |
| html{ scrollbar-width:thin; scrollbar-color: var(--line-strong, rgba(10,10,10,0.22)) transparent; } |
| html::-webkit-scrollbar{ width:8px; } |
| html::-webkit-scrollbar-track{ background:transparent; } |
| html::-webkit-scrollbar-thumb{ background: var(--line-strong, rgba(10,10,10,0.22)); border-radius:999px; } |
| .u-name{ margin:0 0 14px; font-size:17px; font-weight:800; } |
| .u-row{ |
| display:flex; align-items:center; flex-wrap:wrap; gap:6px 10px; padding:8px 0; |
| border-bottom:1px solid var(--line, rgba(10,10,10,0.12)); font-size:13.5px; |
| } |
| .u-row:last-of-type{ border-bottom:none; } |
| .u-lbl{ font-weight:700; color:var(--muted, rgba(10,10,10,0.55)); min-width:110px; flex-shrink:0; } |
| .u-inline{ display:flex; align-items:center; flex-wrap:wrap; gap:8px; } |
| .u-inline select{ padding:5px 8px; } |
| .u-idcard{ display:block; max-width:100%; max-height:220px; border-radius: var(--radius-sm, 10px); margin:10px 0; cursor:pointer; } |
| .u-actions{ display:flex; flex-wrap:wrap; gap:8px; margin:12px 0; } |
| .u-hr{ border:none; border-top:1px solid var(--line, rgba(10,10,10,0.12)); margin:16px 0; } |
| .u-block-lbl{ font-weight:700; color:var(--muted, rgba(10,10,10,0.55)); font-size:13px; margin-bottom:6px; } |
| .u-list{ margin:0 0 4px; padding-inline-start:20px; font-size:13px; } |
| .u-list li{ margin-bottom:5px; } |
| @media (max-width:480px){ |
| .u-lbl{ min-width:100%; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div class="page-wrap"> |
| <div class="topbar"> |
| <h2 style="margin:0;">لوحة الأدمن</h2> |
| </div> |
|
|
| <div class="filters" id="main-tabs"> |
| <button data-t="overview" class="active" onclick="setTab('overview')">نظرة عامة</button> |
| <button data-t="users" onclick="setTab('users')">كل المستخدمين</button> |
| <button data-t="projects" onclick="setTab('projects')">كل المشاريع</button> |
| <button data-t="verif" onclick="setTab('verif')">توثيق الهوية</button> |
| <button data-t="reports" onclick="setTab('reports')">البلاغات <span id="reports-badge"></span></button> |
| <button data-t="backups" onclick="setTab('backups')">النسخ الاحتياطية</button> |
| </div> |
|
|
| <div id="tab-overview"><div class="stats" id="overview-stats"></div></div> |
|
|
| <div id="tab-users" style="display:none"> |
| <div class="filters"> |
| <input type="text" id="search" placeholder="دوّر بالاسم أو الإيميل..." oninput="renderUsers()"> |
| <button data-rf="all" class="active" onclick="setRoleFilter('all')">الكل</button> |
| <button data-rf="owner" onclick="setRoleFilter('owner')">أصحاب مشاريع</button> |
| <button data-rf="investor" onclick="setRoleFilter('investor')">مستثمرين</button> |
| <button data-rf="suspended" onclick="setRoleFilter('suspended')">موقوفين</button> |
| </div> |
| <table><thead><tr><th>الاسم</th><th>الإيميل / الهاتف</th><th>الدور</th><th>التوثيق</th><th>مشاريعه</th><th>بلاغات ضده</th><th>الحالة</th><th>تاريخ التسجيل</th></tr></thead> |
| <tbody id="users-rows"></tbody></table> |
| </div> |
|
|
| <div id="tab-projects" style="display:none"> |
| <div class="filters"> |
| <button data-pf="all" class="active" onclick="setProjFilter('all')">الكل</button> |
| <button data-pf="active" onclick="setProjFilter('active')">شغالة</button> |
| <button data-pf="closed" onclick="setProjFilter('closed')">مقفولة</button> |
| <button data-pf="featured" onclick="setProjFilter('featured')">مميّزة</button> |
| </div> |
| <table><thead><tr><th>المشروع</th><th>صاحبه</th><th>القطاع</th><th>الهدف</th><th>النوايا</th><th>الحالة</th><th>إجراءات</th></tr></thead> |
| <tbody id="projects-rows"></tbody></table> |
| </div> |
|
|
| <div id="tab-verif" style="display:none"> |
| <div class="filters" id="filters"> |
| <button data-f="pending" class="active" onclick="setFilter('pending')">قيد الانتظار</button> |
| <button data-f="approved" onclick="setFilter('approved')">موثّقين</button> |
| <button data-f="rejected" onclick="setFilter('rejected')">مرفوضين</button> |
| <button data-f="all" onclick="setFilter('all')">الكل</button> |
| </div> |
| <table><thead><tr><th>الاسم</th><th>الإيميل</th><th>الدور</th><th>البطاقة</th><th>الحالة</th><th>إجراء</th></tr></thead> |
| <tbody id="rows"></tbody></table> |
| </div> |
|
|
| <div id="tab-reports" style="display:none"> |
| <div class="filters" id="report-filters"> |
| <button data-f="open" class="active" onclick="setReportFilter('open')">مفتوحة</button> |
| <button data-f="resolved" onclick="setReportFilter('resolved')">اتحلّت</button> |
| <button data-f="dismissed" onclick="setReportFilter('dismissed')">اتجاهلت</button> |
| <button data-f="all" onclick="setReportFilter('all')">الكل</button> |
| </div> |
| <table><thead><tr><th>المُبلَّغ عنه</th><th>السبب</th><th>التفاصيل</th><th>المُبلِّغ</th><th>الحالة</th><th>إجراء</th></tr></thead> |
| <tbody id="report-rows"></tbody></table> |
| </div> |
|
|
| <div id="tab-backups" style="display:none"> |
| <button onclick="runBackup()" style="margin-bottom:12px; padding:8px 16px; cursor:pointer; border-radius:6px; border:1px solid #999;"><svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M3 7l9-4 9 4-9 4-9-4z"/><path d="M3 7v10l9 4 9-4V7"/><path d="M12 11v10"/></svg> اعمل نسخة دلوقتي</button> |
| <table><thead><tr><th>الملف</th><th>الحجم</th><th>التاريخ</th><th>تحميل</th></tr></thead> |
| <tbody id="backup-rows"></tbody></table> |
| </div> |
|
|
| </div> |
|
|
| <div id="user-overlay" class="u-overlay" onclick="if(event.target===this) hideUserOverlay();"> |
| <div class="u-modal"> |
| <div class="u-modal-head"> |
| <span>تفاصيل المستخدم</span> |
| <button class="u-close" onclick="hideUserOverlay()" aria-label="إغلاق"> |
| <svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg> |
| </button> |
| </div> |
| <div class="u-modal-body" id="user-detail"></div> |
| </div> |
| </div> |
|
|
| <script> |
| let allRows = [], allReports = [], allUsers = [], allProjects = []; |
| let filter = 'pending', reportFilter = 'open', roleFilter = 'all', projFilter = 'all'; |
| async function api(path, opts={}){ |
| let res; |
| try{ |
| res = await fetch(path, opts); |
| }catch(e){ |
| throw new Error('فشل الاتصال بالسيرفر'); |
| } |
| if(!res.ok) throw new Error('فشل: ' + path); |
| return res.json(); |
| } |
| |
| function showUserOverlay(){ |
| document.getElementById('user-overlay').classList.add('show'); |
| document.body.style.overflow = 'hidden'; |
| } |
| function hideUserOverlay(){ |
| document.getElementById('user-overlay').classList.remove('show'); |
| document.body.style.overflow = ''; |
| } |
| function setTab(t){ |
| document.querySelectorAll('#main-tabs button[data-t]').forEach(b=>b.classList.toggle('active', b.dataset.t===t)); |
| ['overview','users','projects','verif','reports','backups'].forEach(id=>{ |
| document.getElementById('tab-'+id).style.display = (id===t) ? 'block' : 'none'; |
| }); |
| } |
| function setFilter(f){ filter=f; document.querySelectorAll('#filters button').forEach(b=>b.classList.toggle('active', b.dataset.f===f)); renderRows(); } |
| function setReportFilter(f){ reportFilter=f; document.querySelectorAll('#report-filters button').forEach(b=>b.classList.toggle('active', b.dataset.f===f)); renderReports(); } |
| function setRoleFilter(f){ roleFilter=f; document.querySelectorAll('#tab-users button[data-rf]').forEach(b=>b.classList.toggle('active', b.dataset.rf===f)); renderUsers(); } |
| function setProjFilter(f){ projFilter=f; document.querySelectorAll('#tab-projects button[data-pf]').forEach(b=>b.classList.toggle('active', b.dataset.pf===f)); renderProjects(); } |
| |
| async function load(){ |
| let ov; |
| try{ |
| ov = await api('/api/admin/overview'); |
| }catch(e){ alert(e.message || 'خطأ في التحميل'); return; } |
| renderOverview(ov); |
| try{ allUsers = await api('/api/admin/users'); renderUsers(); }catch(e){} |
| try{ allProjects = await api('/api/admin/projects'); renderProjects(); }catch(e){} |
| try{ allRows = await api('/api/admin/id-verifications'); renderRows(); }catch(e){} |
| try{ |
| allReports = await api('/api/admin/reports'); |
| const openCount = allReports.filter(r=>r.status==='open').length; |
| document.getElementById('reports-badge').textContent = openCount ? `(${openCount})` : ''; |
| renderReports(); |
| }catch(e){} |
| try{ renderBackups(await api('/api/admin/backups')); }catch(e){} |
| } |
| |
| function renderOverview(ov){ |
| document.getElementById('overview-stats').innerHTML = ` |
| <div class="stat"><b>${ov.usersTotal}</b>إجمالي المستخدمين</div> |
| <div class="stat"><b>${ov.usersVerified}</b>موثّقين</div> |
| <div class="stat"><b>${ov.usersSuspended}</b>موقوفين</div> |
| <div class="stat"><b>${ov.usersByRole.owner||0}</b>أصحاب مشاريع</div> |
| <div class="stat"><b>${ov.usersByRole.investor||0}</b>مستثمرين</div> |
| <div class="stat"><b>${ov.signups7d}</b>تسجيل جديد (٧ أيام)</div> |
| <div class="stat"><b>${ov.projectsTotal}</b>إجمالي المشاريع</div> |
| <div class="stat"><b>${ov.projectsActive}</b>مشاريع شغالة</div> |
| <div class="stat"><b>${ov.projectsClosed}</b>مشاريع مقفولة</div> |
| <div class="stat"><b>${ov.interestsTotal}</b>تسجيل نية</div> |
| <div class="stat"><b><bdi dir="ltr">${Math.round(ov.interestsAmount).toLocaleString('ar-EG')}</bdi></b>إجمالي النوايا (جنيه)</div> |
| <div class="stat"><b>${ov.appointmentsTotal}</b>مواعيد محجوزة</div> |
| <div class="stat"><b>${ov.pendingIdReviews}</b>توثيق قيد الانتظار</div> |
| <div class="stat"><b>${ov.reportsOpen}</b>بلاغات مفتوحة</div> |
| `; |
| } |
| |
| function renderUsers(){ |
| const q = (document.getElementById('search').value||'').trim().toLowerCase(); |
| let rows = allUsers; |
| if(roleFilter==='suspended') rows = rows.filter(u=>!u.active); |
| else if(roleFilter!=='all') rows = rows.filter(u=>u.role===roleFilter || u.role==='both'); |
| if(q) rows = rows.filter(u => (u.name||'').toLowerCase().includes(q) || (u.email||'').toLowerCase().includes(q)); |
| const roleLabel = { owner:'صاحب مشروع', investor:'مستثمر', both:'الاتنين', broker:'وسيط' }; |
| if(!rows.length){ document.getElementById('users-rows').innerHTML = `<tr><td colspan="8" class="empty">مفيش نتائج</td></tr>`; return; } |
| document.getElementById('users-rows').innerHTML = rows.map(u => ` |
| <tr> |
| <td><a href="#" onclick="openUser('${u.id}');return false;">${u.name}</a></td> |
| <td>${u.email}<br><small>${u.phone||''}</small></td> |
| <td>${roleLabel[u.role]||u.role}</td> |
| <td>${u.id_verified ? '<span class="badge good">موثّق</span>' : `<span class="badge">${idStatusLabel[u.id_status]||u.id_status}</span>`}</td> |
| <td>${u.projects_count}</td> |
| <td>${u.reports_count ? `<span class="badge danger">${u.reports_count}</span>` : '0'}</td> |
| <td>${u.active ? 'نشط' : '<span class="badge danger">موقوف</span>'}</td> |
| <td>${new Date(u.created_at*1000).toLocaleDateString('ar-EG')}</td> |
| </tr> |
| `).join(''); |
| } |
| |
| function renderProjects(){ |
| let rows = allProjects; |
| if(projFilter==='active') rows = rows.filter(p=>p.status==='active'); |
| else if(projFilter==='closed') rows = rows.filter(p=>p.status==='closed'); |
| else if(projFilter==='featured') rows = rows.filter(p=>p.featuredActive); |
| if(!rows.length){ document.getElementById('projects-rows').innerHTML = `<tr><td colspan="7" class="empty">مفيش مشاريع</td></tr>`; return; } |
| document.getElementById('projects-rows').innerHTML = rows.map(p => ` |
| <tr> |
| <td><a href="project.html?id=${p.id}" target="_blank">${p.name}</a> ${p.featuredActive?`<span class="badge good">مميّز${p.featured_until ? ' — باقيله ' + Math.max(0, Math.ceil((p.featured_until - Date.now()/1000)/86400)) + ' يوم' : ''}</span>`:''}</td> |
| <td><a href="#" onclick="openUser('${p.owner_id}');return false;">${p.owner_name}</a></td> |
| <td>${p.sector}</td> |
| <td><bdi dir="ltr">${Math.round(p.target).toLocaleString('ar-EG')} ج</bdi></td> |
| <td>${p.interests_count} (<bdi dir="ltr">${Math.round(p.interests_amount).toLocaleString('ar-EG')} ج</bdi>)</td> |
| <td class="status-${p.status}">${p.status==='closed'?'مقفول':'شغال'}</td> |
| <td> |
| ${p.featuredActive |
| ? `<button class="act" onclick="projAct('${p.id}','unfeature')">إلغاء التمييز</button>` |
| : `<button class="act approve" onclick="projAct('${p.id}','feature')">تمييز <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M12 3l2.6 5.7 6.2.6-4.7 4.2 1.4 6.2L12 16.9 6.5 19.7l1.4-6.2L3.2 9.3l6.2-.6z"/></svg></button>`} |
| ${p.status==='closed' |
| ? `<button class="act" onclick="projAct('${p.id}','reopen')">فتح تاني</button>` |
| : `<button class="act" onclick="projAct('${p.id}','close')">قفل</button>`} |
| <button class="act reject" onclick="projAct('${p.id}','delete')">حذف نهائي <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 7h14M9 7V4h6v3"/><path d="M7 7l1 13h8l1-13"/></svg>️</button> |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function projAct(id, action){ |
| if(action==='delete' && !confirm('حذف نهائي للمشروع وكل بياناته (النوايا والمواعيد والرسائل)، الإجراء ده مايترجعش. متأكد؟')) return; |
| let query = ''; |
| if(action === 'feature'){ |
| const input = prompt('تمييز المشروع لمدة كام يوم؟ (1-90)', '14'); |
| if(input === null) return; |
| const days = Number(input); |
| if(!days || days < 1 || days > 90){ alert('لازم رقم بين 1 و90'); return; } |
| query = `?days=${days}`; |
| } |
| try{ await api(`/api/admin/projects/${id}/${action}${query}`, { method:'POST' }); allProjects = await api('/api/admin/projects'); renderProjects(); } |
| catch(e){ alert('فشل الإجراء'); } |
| } |
| |
| function renderBackups(files){ |
| if(!files.length){ document.getElementById('backup-rows').innerHTML = `<tr><td colspan="4" class="empty">لسه مفيش نسخ احتياطية</td></tr>`; return; } |
| document.getElementById('backup-rows').innerHTML = files.map(f => ` |
| <tr> |
| <td>${f.name}</td> |
| <td>${(f.size/1024).toFixed(1)} KB</td> |
| <td>${new Date(f.created_at*1000).toLocaleString('ar-EG')}</td> |
| <td><a href="/api/admin/backups/${f.name}">تحميل</a></td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function runBackup(){ |
| try{ await api('/api/admin/backups/run', { method:'POST' }); renderBackups(await api('/api/admin/backups')); } |
| catch(e){ alert('فشل عمل النسخة'); } |
| } |
| |
| const idStatusLabel = { pending:'قيد الانتظار', approved:'موثّق', rejected:'مرفوض', none:'لسه ملقدمش' }; |
| const reportStatusLabel = { open:'مفتوحة', resolved:'اتحلّت', dismissed:'اتجاهلت' }; |
| |
| function renderRows(){ |
| const roleLabel = { owner:'صاحب مشروع', investor:'مستثمر', both:'الاتنين' }; |
| const rows = filter === 'all' ? allRows : allRows.filter(r => r.id_status === filter); |
| if(!rows.length){ document.getElementById('rows').innerHTML = `<tr><td colspan="6" class="empty">مفيش طلبات في القسم ده</td></tr>`; return; } |
| document.getElementById('rows').innerHTML = rows.map(r => ` |
| <tr> |
| <td><a href="#" onclick="openUser('${r.id}');return false;">${r.name}</a></td> |
| <td>${r.email}<br><small>${r.phone||''}</small></td> |
| <td>${roleLabel[r.role]||r.role}</td> |
| <td>${(r.id_card && r.id_card !== 'none') ? `<img src="/api/admin/id-card/${r.id_card}" onclick="window.open(this.src)" onerror="this.style.display='none'; this.insertAdjacentHTML('afterend','—');">` : '—'}</td> |
| <td class="status-${r.id_status}">${idStatusLabel[r.id_status]||r.id_status}</td> |
| <td> |
| <button class="act approve" ${r.id_status==='approved'?'disabled':''} onclick="act('${r.id}','approve')">توثيق <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg></button> |
| <button class="act reject" ${r.id_status==='rejected'?'disabled':''} onclick="act('${r.id}','reject')">رفض <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M6 6l12 12M18 6L6 18"/></svg></button> |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function act(userId, action){ |
| if(action==='reject' && !confirm('متأكد إنك عايز ترفض التوثيق ده؟')) return; |
| try{ await api(`/api/admin/id-verifications/${userId}/${action}`, { method:'POST' }); load(); } |
| catch(e){ alert('فشل الإجراء'); } |
| } |
| |
| function renderReports(){ |
| const rows = reportFilter === 'all' ? allReports : allReports.filter(r => r.status === reportFilter); |
| if(!rows.length){ document.getElementById('report-rows').innerHTML = `<tr><td colspan="6" class="empty">مفيش بلاغات في القسم ده</td></tr>`; return; } |
| document.getElementById('report-rows').innerHTML = rows.map(r => ` |
| <tr> |
| <td>${r.target_user_id ? `<a href="#" onclick="openUser('${r.target_user_id}');return false;">${r.target_name||r.target_user_id}</a>${r.target_active===0?' <small style="color:#c00;">(موقوف)</small>':''}` : (r.project_name || '—')}</td> |
| <td>${r.reason}</td> |
| <td>${r.details||'—'}</td> |
| <td>${r.reporter_name||'زائر'}${r.reporter_email?`<br><small>${r.reporter_email}</small>`:''}</td> |
| <td class="status-${r.status}">${reportStatusLabel[r.status]||r.status}</td> |
| <td> |
| ${r.status!=='resolved'?`<button class="act approve" onclick="reportAct('${r.id}','resolve')">اتحل <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg></button>`:''} |
| ${r.status!=='dismissed'?`<button class="act reject" onclick="reportAct('${r.id}','dismiss')">تجاهل <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M6 6l12 12M18 6L6 18"/></svg></button>`:''} |
| ${r.status!=='open'?`<button class="act" onclick="reportAct('${r.id}','reopen')">فتح تاني</button>`:''} |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function reportAct(reportId, action){ |
| try{ await api(`/api/admin/reports/${reportId}/${action}`, { method:'POST' }); load(); } |
| catch(e){ alert('فشل الإجراء'); } |
| } |
| |
| async function openUser(userId){ |
| let u; |
| try{ u = await api(`/api/admin/users/${userId}`); } |
| catch(e){ alert('تعذر تحميل بيانات المستخدم'); return; } |
| const roleLabel = { owner:'صاحب مشروع', investor:'مستثمر', both:'الاتنين', broker:'وسيط' }; |
| document.getElementById('user-detail').innerHTML = ` |
| <h3 class="u-name">${u.name} ${u.active?'':'<span class="badge danger">موقوف</span>'}</h3> |
| <div class="u-row"><span class="u-lbl">الإيميل</span><span>${u.email}</span></div> |
| <div class="u-row"><span class="u-lbl">الهاتف</span><span>${u.phone||'—'}</span></div> |
| <div class="u-row"> |
| <span class="u-lbl">الدور</span> |
| <span class="u-inline"> |
| ${roleLabel[u.role]||u.role} |
| <select id="role-select"> |
| ${['owner','investor','both','broker'].map(r=>`<option value="${r}" ${r===u.role?'selected':''}>${roleLabel[r]}</option>`).join('')} |
| </select> |
| <button class="act" onclick="changeRole('${u.id}')">تغيير</button> |
| </span> |
| </div> |
| <div class="u-row"> |
| <span class="u-lbl">توثيق الهوية</span> |
| <span class="u-inline">${idStatusLabel[u.id_status]||u.id_status} ${u.id_verified?'<span class="badge good">موثّق <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg></span>':''}</span> |
| </div> |
| ${(u.id_card && u.id_card !== 'none')?`<img class="u-idcard" src="/api/admin/id-card/${u.id_card}" onclick="window.open(this.src)" onerror="this.remove();">`:''} |
| <div class="u-actions"> |
| ${u.id_verified |
| ? `<button class="act reject" onclick="manualVerify('${u.id}', false)">إلغاء التوثيق</button>` |
| : `<button class="act approve" onclick="manualVerify('${u.id}', true)">توثيق يدوي (من غير بطاقة) <svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg></button>`} |
| </div> |
| <div class="u-row"><span class="u-lbl">البايو</span><span>${u.bio||'—'}</span></div> |
| <div class="u-row"><span class="u-lbl">تاريخ التسجيل</span><span>${new Date(u.created_at*1000).toLocaleDateString('ar-EG')}</span></div> |
| <hr class="u-hr"> |
| <div class="u-row"><span class="u-lbl">مشاريعه (${u.projects.length})</span><span>${u.projects.map(p=>p.name).join('، ')||'—'}</span></div> |
| <div class="u-block-lbl">بلاغات ضده (${u.reportsAgainst.length})</div> |
| <ul class="u-list">${u.reportsAgainst.map(r=>`<li>${r.reason} — ${reportStatusLabel[r.status]||r.status}${r.details?` (${r.details})`:''}</li>`).join('')||'<li>لا يوجد</li>'}</ul> |
| <hr class="u-hr"> |
| <div class="u-actions"> |
| ${u.active |
| ? `<button class="act reject" onclick="toggleUser('${u.id}','suspend')"><svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><circle cx="12" cy="12" r="9"/><path d="M6 6l12 12"/></svg> إيقاف الحساب</button>` |
| : `<button class="act approve" onclick="toggleUser('${u.id}','unsuspend')"><svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><path d="M5 13l4 4L19 7"/></svg> إعادة تفعيل الحساب</button>`} |
| <button class="act" onclick="resetUserPassword('${u.id}')"><svg viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-inline-end:3px"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/></svg> عمل كلمة مرور جديدة</button> |
| </div> |
| `; |
| showUserOverlay(); |
| } |
| |
| async function changeRole(userId){ |
| const role = document.getElementById('role-select').value; |
| try{ await api(`/api/admin/users/${userId}/set-role`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({role}) }); openUser(userId); allUsers = await api('/api/admin/users'); renderUsers(); } |
| catch(e){ alert('فشل تغيير الدور'); } |
| } |
| |
| async function manualVerify(userId, verified){ |
| if(!confirm(verified ? 'توثيق يدوي من غير مراجعة بطاقة — استخدمه بس لو اتأكدت من هوية الشخص بطريقة تانية. متأكد؟' : 'إلغاء التوثيق؟')) return; |
| try{ await api(`/api/admin/users/${userId}/verify-manual`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({verified}) }); openUser(userId); load(); } |
| catch(e){ alert('فشل الإجراء'); } |
| } |
| |
| async function resetUserPassword(userId){ |
| if(!confirm('ده إجراء استثنائي — استخدمه بس بعد ما تتأكد من هوية صاحب الحساب يدويًا (واتساب/إيميل). متأكد؟')) return; |
| let d; |
| try{ d = await api(`/api/admin/users/${userId}/reset-password`, { method:'POST' }); } |
| catch(e){ alert('فشل الإجراء'); return; } |
| alert(`كلمة المرور المؤقتة: ${d.tempPassword}\nكود الاسترجاع الجديد: ${d.recoveryCode}\n\nابعتهم لصاحب الحساب على طول (واتساب/إيميل) — مش هيتعرضوا تاني.`); |
| } |
| |
| async function toggleUser(userId, action){ |
| const msg = action==='suspend' ? 'متأكد إنك عايز توقف الحساب ده؟ هيتقفل خروجه فورًا.' : 'تفعيل الحساب تاني؟'; |
| if(!confirm(msg)) return; |
| try{ await api(`/api/admin/users/${userId}/${action}`, { method:'POST' }); hideUserOverlay(); load(); } |
| catch(e){ alert('فشل الإجراء'); } |
| } |
| |
| load(); |
| </script> |
| </body> |
| </html> |
|
|