feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق
الباك إند: - تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً، وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً. - OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع. - N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر. N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)، ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird. فلاتر: - docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام). - السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب. - طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط. - الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3ab74a13a8
commit
de4bbd00ac
@@ -182,12 +182,45 @@
|
||||
<td><span class="badge ${suspended ? '' : 'active'}">${esc(t.status)}</span></td>
|
||||
<td>${t.completed_trips}/${t.trips}</td><td>${money(t.gmv)}</td><td>${money(t.revenue)}</td>
|
||||
<td><button class="ghost" data-act="manifest">مانيفست</button>
|
||||
<button class="ghost" data-act="admin">تعيين أدمن</button>
|
||||
<button class="ghost" data-act="logo">اللوغو</button>
|
||||
<button class="ghost" data-act="status">${suspended ? 'تفعيل' : 'تعليق'}</button></td>`;
|
||||
|
||||
tr.querySelector('[data-act=manifest]').onclick = async () => {
|
||||
const m = await api('GET', `/admin/tenants/${t.id}/app-manifest`);
|
||||
alert(JSON.stringify(m, null, 2));
|
||||
};
|
||||
// أول أدمن للمستأجر: تُنشئ الحساب إن لم يوجد (docs/22 — N1d).
|
||||
// بلا هذا لا يمكن الدخول إلى لوحة مستأجر مزوَّد حديثاً إطلاقاً.
|
||||
tr.querySelector('[data-act=admin]').onclick = async () => {
|
||||
const phone = prompt(`رقم هاتف أدمن «${t.name}» (بصيغة دولته: ${t.countryPack})`);
|
||||
if (!phone) return;
|
||||
try {
|
||||
const r = await api('POST', '/platform/users/role', {
|
||||
tenantSlug: t.slug, phone: phone.trim(), role: 'admin',
|
||||
});
|
||||
toast(r.created ? 'أُنشئ الحساب وعُيّن أدمن ✅' : 'صار أدمن ✅');
|
||||
} catch (e) { toast(e.message, false); }
|
||||
};
|
||||
// رفع اللوغو (docs/22 — N2). لا نمرّ بـapi() لأن الرفع multipart
|
||||
// لا JSON: ضبط Content-Type يدوياً يكسر حدّ الأجزاء (boundary).
|
||||
tr.querySelector('[data-act=logo]').onclick = () => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'file'; inp.accept = 'image/png,image/jpeg';
|
||||
inp.onchange = async () => {
|
||||
const f = inp.files?.[0]; if (!f) return;
|
||||
const fd = new FormData(); fd.append('file', f);
|
||||
try {
|
||||
const res = await fetch(`${API}/admin/tenants/${t.id}/logo`, {
|
||||
method: 'POST', headers: { 'x-platform-secret': SECRET }, body: fd,
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(j.message || `HTTP ${res.status}`);
|
||||
toast(`رُفع اللوغو ${j.logo?.width}×${j.logo?.height} ✅`);
|
||||
} catch (e) { toast(e.message, false); }
|
||||
};
|
||||
inp.click();
|
||||
};
|
||||
tr.querySelector('[data-act=status]').onclick = async () => {
|
||||
// التعليق يقطع الخدمة عن مستأجر حيّ فوراً — لذلك تأكيد صريح.
|
||||
const next = suspended ? 'active' : 'suspended';
|
||||
|
||||
Reference in New Issue
Block a user