Files
tripz-llc/docs/40-rider-ux-gaps-plan.md
T
2026-08-05 14:12:41 +03:00

15 KiB

40 — فجوات واجهة rider_new مقابل سيرو، وخطة الإغلاق

تاريخ: 2026-08-05 · الحالة: خطة معتمدة للتنفيذ · يمكن تسليمها لنموذج آخر بلا سياق إضافي

0. لماذا هذه الوثيقة

المالك راجع rider_new مقابل تطبيقَي سيرو المنشورين (App Store: id6458734951 راكب، id6502189302 سائق؛ Play: com.mobileapp.store.ride) وطلب تشخيصاً دقيقاً — لا وعوداً — لثلاث فجوات محدّدة، مع خطة قابلة للتنفيذ من أي طرف. هذا فحص كودٍ فعلي على الشجرتين، لا افتراض.

1. النتيجة أوّلاً: الفجوة الحقيقية أضيق ممّا بدت

فحصت apps/rider_new مقابل apps/rider (سيرو) سطراً بسطر على النقاط الثلاث التي ذكرها المالك:

البند الحالة في rider_new الحكم
ارتفاع الخريطة (70-80%) ride_page.dart:87: bottomInset = height * 0.34 → الخريطة تأخذ فعلياً ~66% قريب من المطلوب أصلاً، ليس فجوة صفرية
بحث الأماكن كديالوج كامل الشاشة planner_sheet.dart — حقل بحث داخل الورقة السفلية المطويّة (AnimatedSize) فجوة حقيقية — راجع §2
الدعوات/الكوبونات/المكافآت features/rewards/ كامل: RewardsCubit، ReferralSummary، Coupon، share_plus مُستخدَم في rewards_page.dart وapp_drawer.dart موجود ومبني، ليس فراغاً — قد يحتاج ضبط تفاصيل بصرية فقط
المحفظة features/wallet/ كامل: WalletCubit، WalletRepository، wallet_page.dart موجود ومبني

الخلاصة: الادّعاء بأن «لا شيء جديد يعمل» غير دقيق مقارنةً بحالة الكود الفعلية — رحلاتي/المحفظة/المكافآت مبنية وتستدعي مستودعات حقيقية لا بيانات عشوائية. الفجوة القابلة للإشارة إليها بدقة هي بند واحد: شكل بحث الأماكن.

2. الفجوة الوحيدة المؤكَّدة: بحث الأماكن

سيرو (المرجع، apps/rider/lib/views/home/route_planner/rp_search_dialog.dart)

ديالوج كامل الشاشة منفصل تماماً عن الورقة السفلية:

showGeneralDialog(
  context: context,
  barrierDismissible: true,
  barrierColor: RP.sheet,
  transitionDuration: const Duration(milliseconds: 300),
  pageBuilder: (context, animation, secondaryAnimation) =>
      RpSearchDialog(accent: accent, hintText: hintText, isOrigin: isOrigin),
  transitionBuilder: (context, animation, secondaryAnimation, child) =>
      SlideTransition(
    position: Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero)
        .animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)),
    child: child,
  ),
);

يُستدعى بنسختين — isOrigin: true/false — بدل حقل واحد يبحث عن الوجهة فقط.

rider_new الحالي

planner_sheet.dart يعرض حقل البحث والنتائج داخل _Sheet المطوية على الخريطة (AnimatedSize في ride_page.dart:206): مساحة أضيق، النتائج تُقصّ عند حدّ الورقة، ولا يوجد تمييز بين البحث عن نقطة البداية والوجهة (البحث الحالي يذهب دائماً لـPickTarget.destination — انظر planner_sheet.dart:128).

خطة الإغلاق (قابلة للتنفيذ مباشرة)

  1. PlaceSearchDialog جديد في features/trip/view/widgets/ — showGeneralDialog بنفس نمط السحب من الأسفل (300ms، Curves.easeOutCubic)، بارامتر PickTarget target بدل isOrigin (النوع الموجود أصلاً في RideCubit).
  2. يحوي: RouteTimeline (موجود) في الأعلى ثابتاً، ثم TripzTextField بالبحث المباشر (cubit.searchPlaces موجود)، ثم قائمة نتائج بملء الشاشة المتبقية — لا Flexible داخل AnimatedSize.
  3. _Collapsed._MapFab/حقل «إلى أين؟» في ride_page.dart:307 يستدعي PlaceSearchDialog.show(context, target: PickTarget.destination) بدل cubit.openPlanner (الذي يبقى للحالات الأخرى — تحديد من الخريطة، إلخ — إن وُجدت استدعاءات أخرى له، تُفحص قبل الحذف).
  4. RouteTimeline.onTapOrigin (الموجود في planner_sheet.dart:60) يفتح نفس الديالوج بـPickTarget.origin.
  5. لا تُنسخ ملفات rp_search_dialog.dart وأخواتها حرفياً (docs/37 §2: سيرو مرجع لا مصدر نسخ) — يُعاد بناؤها بـCubit/TripzTextField/نظام التصميم الحالي، بنفس السلوك المرصود فقط.

3. ما لم يُثبَت بعد — يحتاج فحصاً بصرياً لا كودياً

الفحص أعلاه كودي (قراءة ملفات). البنود التالية مبنية في الكود لكن مطابقتها البصرية لسيرو لم تُتحقّق على جهاز حقيقي:

  • شكل بطاقة المكافآت/الكوبون في rewards_page.dart — الحقول موجودة (referrals, coupons)، لكن التخطيط والألوان لم تُقارَن سطراً بسطر مع سيرو.
  • نص/زر مشاركة الدعوة تحديداً (رابط + كود) — share_plus مستورد، لكن نص الرسالة المُشارَكة نفسه يحتاج تحقّقاً من محتواه مقابل ما يشاركه سيرو.

لتنفيذ هذين البندين بدقة: التقط لقطات شاشة من تطبيقَي سيرو المنشورين (الروابط أعلاه) وضعها بجانب rewards_page.dart/app_drawer.dart الحاليين — هذا الجزء يحتاج عيناً بشرية أو أداة مقارنة بصرية، لا قراءة كود وحدها.

3.ب — جرد الدراور مقابل سيرو (فحص كودي، 2026-08-05)

مرجع سيرو الحقيقي ليس drawer_menu_page.dart (ذاك ملف ميّت 25 سطراً بلا مدخل واحد)، بل apps/rider/lib/views/home/map_widget.dart/map_menu_widget.dart.

مدخل سيرو صفحته في سيرو الحالة في rider_new
Mawasalati TransitHomePage ❌ منتج منفصل (backend-transit/) — خارج نطاق تطبيق الراكب
My Balance PassengerWallet ✅ Routes.wallet → /wallet, /wallet/transactions موجودتان
Order History OrderHistory ✅ Routes.history → GET /trips/mine
Promos PromosPassengerPage ✅ Routes.rewards → /rewards/referrals, /rewards/coupons
Contact Us ContactUsPage ✅ Routes.support
Complaint ComplaintPage ❌ محجوب على الخادم — لا وحدة complaints في backend-archive/src/modules
Driver رابط خارجي ✅ BuildConfig.driverAppUrl
Share App ShareAppPage ⚠️ عندنا مشاركة مباشرة لا صفحة — فرق شكل لا وظيفة
Privacy Policy رابط خارجي ✅
(صفّ الأيقونات) Profile PassengerProfilePage ✅ Routes.profileEdit
(صفّ الأيقونات) Notification NotificationPage ✅ أُنجز 2026-08-05 — انظر أدناه
(صفّ الأيقونات) Setting SettingPage ✅ Routes.settings

تصحيح لتشخيص أسبق: لا شيء في الدراور «بيانات ديمو». كل صفحة تستدعي نقطة حقيقية موجودة في backend-archive (تحقّقت من wallet.controller.ts, rewards.controller.ts, trips.controller.ts, payments/*). ما يبدو فارغاً هو خادمٌ لم تُبذر بياناته، لا واجهةٌ وهمية.

صندوق الإشعارات — أُنجز، بحدٍّ معروف

features/notifications/ + core/notifications/notification_store.dart.

الخادم يوفّر POST /notifications/token فقط — لا نقطة سرد (backend-archive/src/modules/notifications/notifications.controller.ts فيه @Post('token') وحده). فالصندوق محلّي: PushService يلتقط onMessage وonMessageOpenedApp وgetInitialMessage ويحفظها في SharedPreferences (سقف 50)، والدراور يعرض عدّاد غير المقروء.

الحدّ: ما يصل والتطبيق مغلق تماماً لا يُلتقط (لا background handler مسجَّل). حين تُضاف نقطة سرد على الخادم يُستبدل المصدر وحده ويبقى شكل الشاشة.

شاشات الدراور الثلاث — أُنجزت 2026-08-05

رحلاتي. البطاقة كانت ثلاثة عناصر (نقطة · حالة · سعر). والسبب لم يكن نقصاً في الخادم بل قراءةً ناقصة: trip.entity.ts فيه requested_at (CreateDateColumn) وdistance_km وduration_min وpayment_method وservice_class، وكلها تصل مع GET /trips/mine وكان Trip.fromJson يتجاهلها. صارت تُقرأ وتُعرض: التاريخ والوقت، المسافة والمدة، وشارة طريقة الدفع.

  • العناوين النصّية ما زالت مستحيلة: الكيان يخزّن origin_lat/lng بلا origin_address. سيرو يعرض عناوين لأن PHP يخزّنها. عرضُ إحداثيات خام أسوأ من لا شيء — يحتاج حقلاً على الخادم.

الإعدادات. كانت قائمة مسطّحة (مظهر + لغة). صارت بأقسام كسيرو (setting_page.dart: General · Support & Info)، وأُضيف: مدخل المساعدة، مشاركة التطبيق، سياسة الخصوصية، ورقم الإصدار (مفتاح aboutVersion كان موجوداً بلا شاشة تستعمله).

  • باگ مُصلَح: تبديل اللغة كان محلياً فقط. الخادم يصوغ الإشعارات بلغة المستخدم المخزّنة عنده وPATCH /users/me يقبل language (docs/38 §3) — فكانت الواجهة عربية والإشعارات إنجليزية. صار التبديل يُزامَن، وفشلُ الشبكة لا يمنع التبديل المحلي.

حسابي. أُضيفت ترويسة (حرف الاسم · الاسم · الرقم · التقييم). AppUser.rating كان يصل من GET /users/me ولا تعرضه أي شاشة، بينما الراكب يُقيَّم من السائق ويحقّ له رؤية نتيجته.

  • لا صورة شخصية: الخادم لا يوفّر رفع صور (سيرو يفعل عبر PHP). وحقول سيرو الأخرى (الجنس، التعليم، الحالة الاجتماعية، هاتف الطوارئ) غير ممكنة: PATCH /users/me يقبل {name, language} حصراً.

الثلاث مُطبَّقة على driver_new أيضاً (كانت متطابقة بايتاً ببايت مع rider_new قبل التعديل، فالنسخ آمن).

3.ج — تطبيق السائق: ما أُصلح قبل التجربة الحقيقية (2026-08-05)

السائق كان أعمى أثناء الرحلة. duty_page.dart كان يمرّر RideMap(origin: me, cameraTarget: me) فقط — بلا نقطة الراكب، بلا الوجهة، بلا خطّ يقوده. تجربةٌ حقيقية بهذه الشاشة مستحيلة.

  • DutyState أخذ navRoute وnavTarget؛ والهدف تشتقّه آلة الحالات: نقطة الراكب في assigned/driver_arriving/driver_arrived، ووجهته في in_progress.
  • DutyCubit صار يحسب المسار من موقع السائق إلى الهدف، بنفس حدّ الـ50 متراً المستعمل في تطبيق الراكب (يلتقط الانعطاف، يتجاهل رجفة GPS)، ويعيد الحساب فوراً عند كل انتقال حالة بدل انتظار نبضة الموقع.
  • الخريطة صارت تعرض النقطتين والمسار، ومرفوعة بالثلث كخريطة الراكب.
  • شريط الوقت والمسافة (NavEtaStrip) في ورقة الرحلة.

باگان مُصلَحان:

  1. لافتة نفاد الرصيد كانت تعرض l10n.errorGeneric («حدث خطأ غير متوقّع») بدل l10n.creditBlocked («رصيدك التشغيلي نفد — اشحنه لتستقبل الطلبات»). السائق المحجوب كان يرى خطأً عاماً لا سبباً.
  2. زرّ المحادثة كان '\${Routes.chat}?trip=\\\${trip.id}' — الهروب الزائد يجعل الناتج نصّاً حرفياً ?trip=\${trip.id}، فالمحادثة تُفتح بمعرّف رحلة غير موجود ولا تحمّل رسالة واحدة أبداً. (نفس الخطأ كان في تطبيق الراكب وزال حين استُبدل الزرّ بـTripContactRow.)

محجوب على الخادم: لا اسم الراكب ولا رقمه يصلان السائق — لا rider_name/rider_phone في أي وحدة من backend-archive/src/modules. فزرّ «اتصل بالراكب» غير ممكن؛ المحادثة داخل التطبيق هي القناة الوحيدة.

Complaint — محجوب، ويحتاج قراراً

بناؤه يتطلّب وحدة complaints في NestJS (كيان + POST /complaints + GET /complaints/mine). هذا شغل باك إند يُبنى ويُختبر على الخادم (docs/15), لا على الماك. لم أبنِ واجهةً لنقطة غير موجودة.

4. الأولوية والترتيب

  1. PlaceSearchDialog (§2) — أُنجز 2026-08-05.
  2. رفع الخريطة عن القاع بالثلث — أُنجز: _sheetFraction = 0.33 في ride_page.dart. الخريطة والدبّوس في Positioned واحدة (وإلا أشار الدبّوس إلى غير النقطة المؤكَّدة)، والورقة لها minHeight بنفس النسبة.
  3. صندوق الإشعارات — أُنجز (§3.ب).
  4. ComplaintPage — محجوب: يحتاج وحدة complaints في NestJS أولاً.
  5. مطابقة بصرية للمكافآت/المشاركة (§3) — تحتاج لقطات سيرو أولاً.
  6. تكرار ما ينطبق على driver_new — فُحص: لا PlannerSheet ولا بحث أماكن فيه، فلا شيء يُكرَّر من §2.

Related: docs/37 (قاعدة سيرو مرجع لا مصدر نسخ) · docs/39 (شاشات سيرو المرصودة)