PLAN v2 · بعد ملاحظات الإدارة

منظومة استلام ونقل عيّنات الـB2B — سلسلة حفظ (Chain of Custody) قابلة للتهيئة لكل معمل خارجي

تمت الموافقة على المقترح الأساسي (لوحة استلام المندوب). النسخة دي بتعدّل الخطة على الملاحظات الجديدة: تفعيل/إيقاف المسار لكل معمل B2B على حِدة · تعدّد المناديب (many-to-many) · دور Courier جديد · تطبيق مندوب موبايل-فيرست بسكان كاميرا سريع — مع توافق خلفي كامل (المعامل غير المفعّلة تفضل زي ما هي).

🗓️ 2026-06-09🧬 Moon ERP — LIS / B2B Logisticsمعتمد على فحص الكود الفعليPer-tenant config · RBAC · Mobile-first
المحتوى: ملخص تعديلات v2 الوضع الحالي ① الإعداد لكل B2B ② تعدّد المناديب ③ دورة الحياة ④ دور Courier ⑤ تطبيق المندوب (موبايل) ⑥ Backend ⑦ حالات حدّية + توافق ⑧ الخطة والتقدير ⑨ القرارات

Δ ملخص تعديلات النسخة v2 (مقابل v1)

🔧 الإعداد per-B2B-lab: مسار المندوب بقى إعداد على مستوى كل معمل خارجي (مش toggle عام) — معمل مفعّل ومعمل لأ، ومع اختيار أجزاء من المسار.
👥 تعدّد المناديب: علاقة many-to-many — المعمل ليه أكتر من مندوب، والمندوب يخدم أكتر من معمل، مع scoping للرؤية.
🎯 التوزيع (مين بيستلم): نموذج «اسحب + اسكان = ملكية» — مفيش حد بيخصّص مندوب لأوردر؛ الأوردرات تظهر لكل المناديب المؤهّلين، واللي يعمل اسكان بتتسجّل عليه وتتحسب عليه. (الأهلية بيحدّدها المعمل الداخلي.)
🔑 دور Courier: نوع صلاحيات/دور جديد مخصّص للمندوب.
📱 موبايل-فيرست: شاشات المندوب responsive للموبايل + سكان كاميرا سريع للباركود/الكود + feedback فوري (beep + اهتزاز).
↩️ توافق خلفي: المعامل غير المفعّلة → سلوك النهاردة بالظبط (auto-collect مباشر). صفر مخاطرة على المعامل الحالية.

0 الوضع الحالي (الخلاصة المعتمدة من فحص الكود)

لما المعمل الخارجي يعمل طلب من بوابته، الكود بيعمل العيّنات بحالة Collected + collected_at=now() على طول (ExternalLabPortalRequestController::store:204) فتظهر في الاستقبال الداخلي فوراً — من غير تسجيل لخطوة النقل ولا مين استلم العيّنة فعلياً.

المخاطرة: الموظّف ممكن «يستلم» العيّنة في النظام قبل وصولها الفعلي — مفيش سلسلة حفظ (مين شالها/إمتى/الحرارة/المسؤولية وهي بره). جدول lab_sample_custody_logs + CustodyAction.Transferred موجود ومصمّم لده بس بيتسجّل يدوي. (CustodyAction.php:5-27)

1 الإعداد لكل معمل B2B على حِدة — قلب التعديل

المسار مش on/off عام — هو سياسة على مستوى كل معمل خارجي (per-tenant policy). كل معمل ليه إعداده الخاص، ومنفصل تماماً عن الباقي.

مكان الإعداد + شكله

DB عمود JSON pickup_config على جدول lab_external_labs (الموجود أصلاً). افتراضياً null = المسار متوقّف = سلوك النهاردة.

pickup_config (JSON على كل external lab): { "enabled": true, // المفتاح الرئيسي — متوقّف = auto-collect زي دلوقتي "mode": "full" | "simple", // full: استلام→نقل→تسليم | simple: استلام واحد فقط "require_portal_ready": false, // المعمل الخارجي يعلّم «جاهزة» الأول من بوابته "track_temperature": true, // المندوب يسجّل حرارة النقل (عيّنات مبرّدة) "require_handover_confirm": true, // تأكيد التسليم للاستقبال "dispatch": "pull" // pull = اسكان=ملكية (الافتراضي) | assigned = توزيع صريح بمدير }

مصفوفة التهيئة — أمثلة واقعية

المعمل الخارجيenabledmodeportal_readytemperatureالسلوك الناتج
معمل النور (قريب، ثقة)falseauto-collect مباشر (زي النهاردة)
معمل الشفاء (بعيد، مندوب)truefullfalsetrueمسار مندوب كامل + سلسلة حرارة
معمل الحياة (تعاون كامل)truefulltruetrueالمعمل يعلّم «جاهزة» → مندوب → تسليم
معمل المستقبل (خفيف)truesimplefalsefalseخطوة استلام واحدة بس (تأكيد المندوب = وصلت)

التوافق الخلفي مضمون: أي معمل enabled=false (أو null) بيمشي في الكود القديم بالظبط — العيّنات Collected فوراً. مفيش أي تأثير على المعامل الشغّالة دلوقتي.

الواجهة (تهيئة الإعداد)

FE تاب جديد «منظومة الاستلام/المناديب» في صفحة تعديل المعمل الخارجي (External Labs): مفتاح التفعيل + اختيار الـmode + الخيارات + تعيين المناديب (نقطة 2). كله مكان واحد لكل معمل.

2 المناديب: الأهلية + التوزيع (مين بيستلم؟)

المطلوب: المعمل الواحد ممكن يكون ليه أكتر من مندوب، والمندوب الواحد ممكن يخدم أكتر من معمل (Many-to-Many). والسؤال المحوري: مين بيختار إن مندوب معيّن هو اللي هيروح يستلم؟ — الإجابة في «نموذج التوزيع» تحت.

النموذج (ERD)

┌───────────────────┐ ┌──────────────────────────────┐ ┌───────────────┐ │ lab_external_labs │ M───N │ lab_external_lab_couriers │ N───M │ users │ │ (المعمل B2B) │◄──────►│ (جدول الربط — pivot) │◄──────►│ (المندوب) │ │ + pickup_config │ │ external_lab_id, user_id │ │ role: courier │ └───────────────────┘ │ is_active, assigned_at, │ └───────────────┘ │ assigned_by, (branch_id?) │ └──────────────────────────────────┘ قواعد: • مندوب يشوف فقط: المعامل المُعيَّن عليها (عبر الـpivot) + الحالة enabled. • المعمل ممكن يكون عليه 0..N مندوب. • لو 0 مندوب والمسار مفعّل → fallback: pool عام يشوفه كل مَن عنده دور courier (config: open_pool). • custody log بيسجّل المندوب الفعلي (to_user_id) — مين استلم بالظبط من المتعدّدين.

سيناريوهات

معمل ← مناديب كتير

معمل الشفاء عليه 3 مناديب (مناطق/أوقات مختلفة) — أي واحد فيهم يشوف عيّناته ويستلم. أول مَن يستلم يتسجّل عليه في الـcustody.

مندوب ← معامل كتير

مندوب «أحمد» مسؤول عن 4 معامل في منطقته — شاشته بتجمّع عيّنات الـ4 معامل، مرتّبة بالمعمل/المسافة.

تعارض/تزامن

مندوبين بيفتحوا نفس المعمل: الاستلام بيتعمل بـoptimistic-lock على العيّنة — أول اسكان يكسب، التاني يشوف «اتستلمت بواسطة X».

نموذج التوزيع — «اسحب + اسكان = ملكية» (Pull + Scan-to-Claim) ⭐

السؤال «مين بيعيّن المندوب؟» بيتحلّ على مستويين منفصلين — وده اللي إنت وصفته بالظبط:

① الأهلية — مين يقدر يخدم المعمل (إعداد لمرة واحدة)

المعمل الداخلي (الأدمن/المدير) هو اللي بيحدّد قائمة المناديب المؤهّلين لكل معمل خارجي (عبر الـpivot) — مش المعمل الخارجي. ده بيتعمل مرة في إعداد المعمل.

② الاستلام الفعلي — اسكان = ملكية ومسؤولية (مفيش dispatcher)

مفيش حد بيخصّص مندوب معيّن لأوردر معيّن. الأوردرات الجاهزة بتظهر لـكل المناديب المؤهّلين للمعمل على شاشاتهم. اللي يروح ويعمل اسكان للأمبولة → هي بتتسجّل عليه (custody عليه + بتتحسب في إحصائياته). الاسكان نفسه = الملكية.

المعمل الخارجي «الشفاء» عليه 3 مناديب مؤهّلين (A, B, C) └─ الأوردرات الجاهزة (at_external_lab) بتظهر للتلاتة على شاشاتهم └─ المندوب B راح المعمل وعمل اسكان لـ8 أمبولات → الـ8 اتسجّلوا على B (custody to_user = B + عدّاد B += 8) → A و C شاشتهم بتحدّث فوراً: «اتستلمت بواسطة B» (optimistic-lock) └─ المسؤولية واضحة: B هو اللي ماسك الـ8 دول دلوقتي وهو اللي هيسلّمهم

المحاسبة (اتحسبت عليه): من lab_sample_custody_logs.to_user_id — تقرير لكل مندوب: كام عيّنة استلم، من أنهي معمل، إمتى، الحرارة، وكام سلّم. مؤشّر أداء ومسؤولية واضح.

وضع اختياري (Explicit Dispatch): لو معمل عايز توزيع صريح — إعداد "dispatch":"assigned": مدير بيخصّص الـrun لمندوب محدّد وبس هو اللي يشوفه. الافتراضي يفضل "pull" (اسكان = ملكية) — اللي إنت طلبته.

3 دورة الحياة — مشروطة بالإعداد

لو المعمل enabled=false: [طلب البوابة] ──> collected ──> [استقبال] ──> delivered ... (سلوك النهاردة، مفيش تغيير) لو enabled=true, mode=full: [طلب البوابة] ──> at_external_lab │ (لو require_portal_ready) المعمل الخارجي: «جاهزة للاستلام» ▼ المندوب يسكان عند المعمل ──> in_transit + custody Transferred (from=Lab, to=Courier, temp?) ▼ المندوب يسلّم للاستقبال ──> collected + custody Received (to=Reception) ▼ [الاستقبال الحالي زي ما هو] ──> delivered ─> ready ─> ... لو enabled=true, mode=simple: [طلب البوابة] ──> at_external_lab ──[المندوب يستلم]──> collected (خطوة واحدة) ──> [استقبال]

الثابت: العيّنة مابتدخلش worklist الاستقبال إلا لما توصل collected — والاستقبال والكانبان والنتائج مايتلمسوش نهائياً. الفرق كله «قبل» الاستقبال.

4 دور Courier + الصلاحيات

Role preset «مندوب / Courier»

  • صلاحية جديدة: lis.samples.pickup (استلام/نقل/تسليم).
  • + lis.samples.view (مقيّدة بالمعامل المُعيّنة).
  • مايشوفش الاستقبال/الكانبان/النتائج/الأسعار — مساحته محصورة في الاستلام بس.
  • يُضاف لقائمة الـpresets الموجودة (reception/cashier/phlebotomist/…).

الرؤية (Scoping)

  • المندوب يشوف عيّنات المعامل المُعيَّن عليها فقط (عبر الـpivot).
  • اختياري: + scoping بالفرع (LisDataScope الموجود).
  • الأدمن/المدير يشوف الكل + يعيّن المناديب.

5 تطبيق المندوب — موبايل-فيرست + سكان سريع

المبادئ

  • موبايل-فيرست: الشاشة مصمّمة للموبايل أساساً (المندوب في الشارع) — أزرار كبيرة، RTL، أوفلاين-تولرنت.
  • سكان كاميرا: فتح الكاميرا → سكان الباركود (نفس الـsymbology الموجود) → استلام فوري بدون كتابة.
  • سريع: optimistic update + beep + اهتزاز عند كل سكان ناجح، وبانر أحمر لو العيّنة مش من معامله/مش جاهزة.
  • عدّاد حيّ: «استلمت 8 / 12» لكل معمل — يعرف خلّص ولا لسه.
  • أوفلاين: لو النت ضعف، السكانات تتخزّن وتتزامن أول ما يرجع (PWA queue) — اختياري مرحلة لاحقة.

يعيد استخدام منطق السكان + الـbeep (Web Audio) الموجود في صفحات التجميع/الاستقبال — مش من الصفر.

🚚 استلام — معمل الشفاء
📷 امسح الأمبولة
8 / 12 اتستلمت
260609-045 · TSH
260609-046 · CBC
! مش من معاملك
تسليم للاستقبال (8)

نموذج مبدئي لشاشة المندوب

6 Backend — Schema + Endpoints

DB Migrations

  • pickup_config (JSON) على lab_external_labs.
  • جدول lab_external_lab_couriers (external_lab_id, user_id, is_active, assigned_by/at).
  • حالتين جداد في SampleStatus: at_external_lab · in_transit + دوال الانتقال.
  • (اختياري) picked_up_at/by على lab_samples للعرض السريع.
  • صلاحية lis.samples.pickup + preset «courier».

BE Endpoints

  • تعديل portal store: فرع على lab.pickup_config.enabledat_external_lab أو Collected (التوافق الخلفي). store:204
  • GET /lis/pickups — عيّنات معامل المندوب (مجمّعة بالمعمل، فلتر بالحالة).
  • POST /lis/samples/{id}/pickup → in_transit + custody.
  • POST /lis/samples/{id}/handover → collected + custody.
  • POST /lis/pickups/scan — سكان سريع: بيسجّل العيّنة على المندوب اللي عامل الاسكان (to_user = المستخدم الحالي) + optimistic-lock ضد التزامن.
  • GET /lis/couriers/me/pickups + تقرير: عدّاد/سجل كل مندوب (اتحسبت عليه) من custody logs.
  • إدارة: GET/POST/DELETE /lis/external-labs/{id}/couriers + حفظ pickup_config.
  • (اختياري) بوابة: POST /requests/{id}/ready-for-pickup.
  • كل العمليات تكتب lab_sample_custody_logs تلقائياً (مش يدوي).

7 حالات حدّية + التوافق الخلفي

الحالةالمعالجة
معمل مش مفعّلسلوك النهاردة بالظبط — auto-collect → استقبال (صفر تغيير).
عيّنة ضاعت في الطريقمن in_transit → lost (custody + سبب) → recollect.
استلام جزئيالباقي يفضل at_external_lab؛ العدّاد يبيّن المتبقّي.
مندوبين متزامنينoptimistic-lock على العيّنة — أول استلام يكسب، التاني يشوف «اتستلمت بواسطة X».
المعمل عمل طلب وألغاهالإلغاء (LIS-120) يفضل شغّال قبل الاستلام (نوسّع الشرط «before pickup»).
0 مندوب ومسار مفعّلconfig open_pool: يشوفه أي courier؛ أو تنبيه للأدمن يعيّن مندوب.
الطلب الداخلي (المسار ب)مايتأثرش — Pending→Collected زي ما هو.

8 خطة التنفيذ + التقدير

مرحلة 1

البنية: per-lab config + couriers pivot + states DB BE

pickup_config + جدول المناديب + الحالتين + الصلاحية + دور courier. فرع portal store (التوافق الخلفي).

مرحلة 2

Endpoints المندوب + custody تلقائي + إدارة التعيين BE

pickups/pickup/handover/scan + couriers CRUD + حفظ الإعداد. (B اختياري: ready-for-pickup.)

مرحلة 3

إعداد المعمل (أدمن) + تعيين المناديب FE

تاب «منظومة الاستلام» في External Labs: التفعيل + الـmode + الخيارات + اختيار المناديب.

مرحلة 4

تطبيق المندوب موبايل-فيرست + سكان FE

Board بالمعامل + سكان كاميرا + beep/اهتزاز + تاب «في الطريق» + تسليم + عرض سلسلة الحفظ.

مرحلة 5

الحالات الحدّية + تحقّق end-to-end BE FE

lost/partial/concurrency/cancel، صلاحيات الدور، اختبار لكل mode + التوافق الخلفي.

الجزءالحجم التقديري
Backend (config + pivot + states + endpoints + custody + role)متوسط–كبير
Frontend — إعداد الأدمن + تعيين المناديبمتوسط
Frontend — تطبيق المندوب موبايل + سكانمتوسط
بوّابة B2B (ready-for-pickup، اختياري)صغير

9 القرارات المطلوبة قبل البدء

  1. الإعداد الافتراضي للمعامل الجديدة: متوقّف (آمن، توافق خلفي) — صح؟
  2. نموذج التوزيع pull (اسكان = ملكية) كافتراضي — صح؟ وفيه معمل محتاج explicit dispatch (توزيع بمدير)؟
  3. عند 0 مندوب مؤهّل ومسار مفعّل: open pool (أي مندوب) ولا إجباري التعيين الأول؟
  4. require_portal_ready (المعمل الخارجي يعلّم «جاهزة» من بوابته) — نعمله من البداية ولا مرحلة لاحقة؟
  5. الأوفلاين/PWA queue للمندوب — مرحلة 4 ولا لاحقاً؟
  6. أبدأ بالكل بالترتيب، ولا مرحلة مرحلة (مرحلة 1+2 الباك الأول)؟

🎯 الخلاصة: منظومة سلسلة حفظ قابلة للتهيئة لكل معمل، بتعدّد مناديب، دور مخصّص، وتطبيق موبايل سريع — مبنية على ~80% بنية موجودة (custody logs/reception/roles/branches/portal)، وبتوافق خلفي كامل يحمي المعامل الشغّالة. جاهز أبدأ التنفيذ بعد موافقتك على القرارات فوق.