استكمال قرار 2026-07-27 (كوميت fff76c9 أرشف الباك إند):
apps/rider → apps/rider-archive-cubit
apps/driver → apps/driver-archive-cubit
dashboards/admin-web → dashboards/admin-web-archive
dashboards/service-web → dashboards/service-web-archive
إعادة تسمية بلا تعديل محتوى. rider/driver كانا إعادة بناء Cubit مبنية على
عقد NestJS المؤرشف. admin-web و service-web نموذجان بملف HTML واحد (164 و
128 سطراً) — يحلّ محلهما siro_admin و siro_service المكتملان.
يُحمل من المؤرشف ولا يُعاد اكتشافه: عقد map-saas المثبَّت بالتجربة في
apps/rider-archive-cubit/lib/core/api/ (x-api-key ترويسة، reverse مصفوفة،
route في points) ودرس مسار FCM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
8.5 KiB
HTML
129 lines
8.5 KiB
HTML
<!doctype html>
|
|
<html lang="ar" dir="rtl">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Tripz — خدمة العملاء</title>
|
|
<!--
|
|
لوحة خدمة العملاء (docs/22 — N5). SPA مكتفية ذاتياً. المصادقة هاتف+OTP →
|
|
JWT بدور dispatcher/admin. بحث مستخدم برقمه (يُطبَّع ويُبحث بالفهرس الأعمى
|
|
على السيرفر) + عرض رحلاته + تفاصيل رحلة بالمعرّف. الحماية على السيرفر.
|
|
-->
|
|
<style>
|
|
:root { --bg:#0f1420; --card:#1a2130; --line:#2a3346; --fg:#e6ebf5; --mut:#8b97ad; --acc:#4f8cff; --ok:#2ecc71; --bad:#e74c3c; }
|
|
* { box-sizing:border-box; }
|
|
body { margin:0; font-family:system-ui,'Segoe UI',Tahoma,sans-serif; background:var(--bg); color:var(--fg); }
|
|
header { padding:14px 22px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:14px; }
|
|
header h1 { font-size:17px; margin:0; }
|
|
.wrap { max-width:900px; margin:0 auto; padding:22px; }
|
|
.card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:18px; margin-bottom:18px; }
|
|
.card h2 { margin:0 0 14px; font-size:14px; color:var(--mut); font-weight:600; }
|
|
input { padding:9px 11px; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--fg); font-size:14px; }
|
|
button.act { background:var(--acc); color:#fff; border:0; border-radius:6px; padding:9px 16px; font-size:14px; cursor:pointer; }
|
|
table { width:100%; border-collapse:collapse; }
|
|
th,td { text-align:right; padding:9px 10px; border-bottom:1px solid var(--line); font-size:13px; }
|
|
th { color:var(--mut); font-weight:600; font-size:12px; }
|
|
.badge { padding:2px 8px; border-radius:6px; font-size:11px; }
|
|
.b-paid,.b-completed{background:rgba(46,204,113,.15);color:var(--ok)} .b-cancelled{background:rgba(231,76,60,.15);color:var(--bad)}
|
|
.muted { color:var(--mut); font-size:13px; }
|
|
.kv { display:grid; grid-template-columns:auto 1fr; gap:6px 14px; font-size:14px; }
|
|
.kv b { color:var(--mut); font-weight:600; }
|
|
.flex { display:flex; gap:10px; }
|
|
.toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); padding:12px 20px; border-radius:8px; opacity:0; transition:opacity .2s; }
|
|
.toast.ok{background:var(--ok)} .toast.bad{background:var(--bad)} .toast.show{opacity:1}
|
|
dialog { background:var(--card); color:var(--fg); border:1px solid var(--line); border-radius:12px; padding:24px; max-width:420px; width:90%; }
|
|
dialog::backdrop{background:rgba(0,0,0,.6)} dialog input{width:100%;margin-top:8px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>🎧 Tripz — خدمة العملاء</h1>
|
|
<span class="muted" id="who"></span>
|
|
<span style="flex:1"></span>
|
|
<button class="act" id="logout" style="display:none" style="background:var(--bad)">خروج</button>
|
|
</header>
|
|
<div class="wrap" id="main" style="display:none">
|
|
<div class="card">
|
|
<h2>بحث عن مستخدم برقم الهاتف</h2>
|
|
<div class="flex"><input id="q" placeholder="07xxxxxxxx" style="flex:1" /><button class="act" id="searchBtn">بحث</button></div>
|
|
<div id="userResult" style="margin-top:14px"></div>
|
|
</div>
|
|
<div class="card">
|
|
<h2>تفاصيل رحلة بالمعرّف</h2>
|
|
<div class="flex"><input id="tid" placeholder="trip id" style="flex:1" /><button class="act" id="tripBtn">عرض</button></div>
|
|
<div id="tripResult" style="margin-top:14px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
<dialog id="loginDlg">
|
|
<h2 style="margin-top:0">دخول خدمة العملاء</h2>
|
|
<p class="muted">هاتف + رمز تحقّق.</p>
|
|
<input id="phone" placeholder="07xxxxxxxx" />
|
|
<div id="otpStep" style="display:none"><input id="otp" placeholder="الرمز (1234 تطوير)" /></div>
|
|
<div class="flex" style="margin-top:16px"><button class="act" id="sendOtp">إرسال الرمز</button><button class="act" id="verify" style="display:none;background:var(--ok)">دخول</button></div>
|
|
</dialog>
|
|
|
|
<script>
|
|
const API = location.origin.includes('localhost') ? 'http://localhost:4010/api' : '/api';
|
|
const TENANT = new URLSearchParams(location.search).get('tenant') || 'siro';
|
|
let TOKEN = sessionStorage.getItem('tripz_cs_token') || '';
|
|
const toast=(m,ok=true)=>{const t=document.getElementById('toast');t.textContent=m;t.className=`toast show ${ok?'ok':'bad'}`;setTimeout(()=>t.className='toast',2600);};
|
|
|
|
async function api(method, path, body){
|
|
const res = await fetch(API+path,{method,headers:{'Content-Type':'application/json','x-tenant-id':TENANT,...(TOKEN?{Authorization:`Bearer ${TOKEN}`}:{})},body:body?JSON.stringify(body):undefined});
|
|
const json = await res.json().catch(()=>({}));
|
|
if(!res.ok) throw new Error(json.message||`HTTP ${res.status}`);
|
|
return json;
|
|
}
|
|
const badge=(s)=>`<span class="badge b-${s}">${s}</span>`;
|
|
|
|
document.getElementById('searchBtn').onclick = async () => {
|
|
const box = document.getElementById('userResult'); box.innerHTML='<span class="muted">…</span>';
|
|
try {
|
|
const u = await api('GET',`/admin/users/search?phone=${encodeURIComponent(document.getElementById('q').value.trim())}`);
|
|
if(!u){ box.innerHTML='<span class="muted">لا مستخدم بهذا الرقم</span>'; return; }
|
|
const trips = await api('GET',`/trips/admin/list?rider=${u.id}`);
|
|
box.innerHTML = `<div class="kv"><b>المعرّف</b><span>${u.id}</span><b>الهاتف</b><span>${u.phone}</span>
|
|
<b>الدور</b><span>${u.role}</span><b>التقييم</b><span>${u.rating??'-'}</span><b>الحالة</b><span>${u.status}</span></div>
|
|
<h2 style="margin-top:16px">رحلاته (${trips.length})</h2>
|
|
<table><thead><tr><th>الرحلة</th><th>الحالة</th><th>الأجرة</th></tr></thead><tbody>${
|
|
trips.length?trips.map(t=>`<tr><td>${t.id.slice(0,8)}</td><td>${badge(t.status)}</td><td>${t.final_fare??t.quoted_fare??'-'}</td></tr>`).join(''):'<tr><td colspan=3 class="muted">لا رحلات</td></tr>'}</tbody></table>`;
|
|
} catch(e){ box.innerHTML=`<span class="muted">${e.message}</span>`; }
|
|
};
|
|
|
|
document.getElementById('tripBtn').onclick = async () => {
|
|
const box = document.getElementById('tripResult'); box.innerHTML='<span class="muted">…</span>';
|
|
try {
|
|
const t = await api('GET',`/trips/${document.getElementById('tid').value.trim()}`);
|
|
if(!t||!t.id){ box.innerHTML='<span class="muted">لا رحلة بهذا المعرّف</span>'; return; }
|
|
box.innerHTML = `<div class="kv"><b>الحالة</b><span>${badge(t.status)}</span><b>الأجرة</b><span>${t.final_fare??t.quoted_fare??'-'} ${t.currency||''}</span>
|
|
<b>المسافة</b><span>${t.distance_km??'-'} كم</span><b>وسيلة الدفع</b><span>${t.payment_method}</span>
|
|
<b>أُلغيت من</b><span>${t.cancelled_by||'-'}</span><b>رسم الإلغاء</b><span>${t.cancel_fee??'-'}</span></div>`;
|
|
} catch(e){ box.innerHTML=`<span class="muted">${e.message}</span>`; }
|
|
};
|
|
|
|
function openLogin(){ document.getElementById('loginDlg').showModal(); }
|
|
document.getElementById('sendOtp').onclick = async () => {
|
|
try{ const r=await api('POST','/auth/send-otp',{phone:document.getElementById('phone').value.trim()}); toast(r.dev_code?`رمز: ${r.dev_code}`:'أُرسل الرمز');
|
|
document.getElementById('otpStep').style.display=''; document.getElementById('verify').style.display=''; }catch(e){toast(e.message,false);}
|
|
};
|
|
document.getElementById('verify').onclick = async () => {
|
|
try{ const r=await api('POST','/auth/verify-otp',{phone:document.getElementById('phone').value.trim(),code:document.getElementById('otp').value.trim()});
|
|
if(!['admin','dispatcher'].includes(r.user?.role)){toast('ليس حساب خدمة عملاء',false);return;}
|
|
TOKEN=r.access_token; sessionStorage.setItem('tripz_cs_token',TOKEN); document.getElementById('loginDlg').close(); boot();
|
|
}catch(e){toast(e.message,false);}
|
|
};
|
|
document.getElementById('logout').onclick = ()=>{ sessionStorage.removeItem('tripz_cs_token'); location.reload(); };
|
|
|
|
function boot(){
|
|
if(!TOKEN){ openLogin(); return; }
|
|
document.getElementById('main').style.display='block';
|
|
document.getElementById('logout').style.display='';
|
|
document.getElementById('who').textContent=`المستأجر: ${TENANT}`;
|
|
}
|
|
boot();
|
|
</script>
|
|
</body>
|
|
</html>
|