مسارات التحاليل في العيادة — تحليل عميق

تذكرة ISS-2026-0179 · مشروع «مون3 حسابات» (288) · تحليل قبل التنفيذ — لم يُكتب أي كود
Modules/Clinic + Modules/LIS فاتورة بلا تحليل — نزيف مالي النتيجة لا تصل الاستقبال 3 تصحيحات لقاعدة المعرفة 2026-07-14

١. المشكلة كما طرحتَها

بنصّك: «في إضافة سيرفس ممكن أختار تحليل وأعمله سعر — فده تعارض: موجود من الزيارة على طول وموجود من السيرفس، واللي في السيرفس مش بتظهر في الزيارة. المشكلة التانية: لما التحاليل تطلع نتيجتها إزاي أصلاً الريسبشن هيعرف ويطبع؟ الزبون هيجي للريسبشن يسأله. خلينا نحصر الحالات: إما الزبون جاي عايز تحاليل بس، أو هو كاشف عندنا والدكتور كتبله التحاليل.»

ترجمة لمطلبين وسؤال:

  1. التعارض: ليه فيه طريقتين لطلب نفس التحليل، وليه اللي في كتالوج الخدمات ما بيظهرش في الزيارة؟
  2. النتيجة: لما التحليل يخلص — إزاي موظف الاستقبال يعرف ويطبع للمريض؟
  3. الحصر: إيه الحالات بالظبط، وكل حالة بتمشي إزاي؟

المنهج: قرأت قاعدة المعرفة أولاً، ثم فحصت الكود بثلاثة وكلاء متوازيين على أعلى موديل، ثم تحققت من بياناتك الفعلية على moonui3_dev_be. كل ادعاء تحته مسار ملف ورقم سطر.

٢. الخلاصة التنفيذية — أربعة اكتشافات

🔴 الاكتشاف الأول — نزيف مالي حقيقي: فاتورة بلا تحليل.
شاشة الاستقبال بتسمح تضيف سطر تحليل من كتالوج خدمات العيادة، وبتحاسب المريض عليه — والطلب عمره ما بيوصل المعمل. السبب: AddServiceOrderLine.php مافيهوش أي فرع لـline_type = lab، فـfulfillment_type بيفضل null ومفيش LabRequest بيتعمل إطلاقاً. المريض دفع، والمعمل مايعرفش إن فيه شغل.
🔴 الاكتشاف الثاني — سيناريو «تحاليل بس» مقطوع تماماً عن النتائج.
آلية رجوع النتيجة للعيادة مبنية وشغالة ومختبَرة — لكنها محروسة بشرطين قاتلين:
// MarkEncounterLabComplete.php:22
if (! $request->encounter_id) { return; }   // ← حارس ١
...
->where('status', OrderLineStatus::Ordered->value)  // ← حارس ٢
وبياناتك الفعلية بتقول: طلبك LabRequest #1encounter_id = NULL (زيارة تحاليل-بس مالهاش encounter — بالتصميم)، وبند العيادة حالته billed (لأنك دفعت). يعني الحارسين الاتنين بيمنعوا التحديث. نفس الحارس حرفياً في DropLabReportOnEncounter — فالتقرير كمان ما بينزلش.
🟠 الاكتشاف الثالث — السبب الجذري للتعارض: lab_mode منفَّذ في الواجهة فقط.
النظام مصمّم على مفتاح clinic.lab_mode = متكامل (المعمل بتاعنا) / مستقل (معمل بره). المبدّل موجود ومنفَّذ في الفرونت (lab-orders.component.ts:101,283,457) — لكن الباك إند عمياء عنه تماماً: ClinicCatalogMode::isStandalone() عنده صفر استدعاءات، ومفيش أي if في الباك بيغيّر سلوك بناءً عليه. النتيجة: الكتالوجان مفتوحان في نفس اللحظة — وده حرفياً «التعارض» اللي بتشوفه.
🟠 الاكتشاف الرابع — قفل صلاحيات يمنع الطباعة من العيادة أصلاً.
طباعة تقرير التحليل محمية بصلاحية lis.result-reports.generate. وشاشة أدوار العيادة مقفولة على البادئة clinic.% (ClinicRoleController.php:35,106) — يعني مستحيل تدّي دور «استقبال» الصلاحية دي من شاشة العيادة. أي زر طباعة نضيفه هيرجّع 403 ما لم تُحل. نفس الفخ اللي حصل مع تخصصات الأطباء ومع طرق الدفع.

٣. الوضع الحالي — مسارا الطلب

المسار (أ): من كتالوج الـLIS مباشرة شغال

المحطةالدليل
الاختيارcreate-visit.component.tsGET /lis/investigations?with_prices=1 — كتالوج المعمل مباشرة
التسعيرLabPriceResolver (بُني في ISS-2026-0176): B2B ← قائمة الدكتور ← القائمة الافتراضية ← الكتالوج
الفوترةOrderLabFromEncounter::core()ServiceOrderLine واحد: line_type=lab · price_source='lis' · fulfillment_type='lab_request'
طلب المعملCreateLabRequest::handleFromArrayLabRequest بـsource=clinic · فاتورة LIS مقموعة (العيادة تملك الفوترة)
العيّنةLabSampleService::generateForRequestلحظة إنشاء الطلب، قبل أي دفع (CreateLabRequest.php:586). مفيش أي حارس دفع.

المسار (ب): من كتالوج خدمات العيادة مكسور

المحطةالدليل
الإنشاءشاشة clinic/services بتسمح بنوع lab (clinic-service-list.component.ts:180) — في أي وضع، بدون أي منع. وحقل lis_investigation_id = خانة رقم عارية يكتب فيها المستخدم ID بإيده (.html:482) — بدون بحث ولا تحقق.
السعربيتزرع تلقائياً في clinic_service_prices (SeedBaseServicePrice.php) — سعر تاني مستقل تماماً عن سعر المعمل.
الظهورمش في شاشة الحجز (مستبعدة صراحةً: create-visit.component.ts:492) · ❌ مش في شاشة الطبيب في الوضع المتكامل · ✅ في الاستقبال · ✅ في مصفوفة التسعير · ✅ في العقود التأمينية · ✅ في التقارير
🔴 التنفيذلا شيء. AddServiceOrderLine بيخزّن السطر ويحاسب — ومفيش LabRequest. التحليل عمره ما يوصل المعمل.
الجسر lis_investigation_id عمود ميت. بحث شامل في الباك والفرونت: كل الإشارات إليه كتابة أو عرضصفر استعلام يقرأه (->where('lis_investigation_id', …) = لا يوجد). فحتى لو ربطت الخدمة بالتحليل صح، الربط مالوش أي أثر.

٤. المشكلة (١): التعارض — بالأرقام

لو عملت خدمة اسمها CBC نوع تحليل بسعر 200، وعندك في المعمل CBC بسعر 150 من قائمة الأسعار:

البُعدالنتيجة
الكتالوج🔴 صفّان مستقلانclinic_services وlab_investigations. مفيش أي قيد تفرّد ولا كشف تكرار بينهم.
السعر🔴 سعران حيّان. 200 عبر PricingResolver · 150 عبر LabPriceResolver. المطبَّق يعتمد على أي شاشة استُخدمت — لا على البيانات.
الطبيبفي الوضع المتكامل (الافتراضي عندك) بيشوف CBC بتاع المعمل فقط. خدمة الـ200 مالهاش أي مدخل — وده حرفياً «اللي في السيرفس مش بتظهر في الزيارة».
الاستقبال🔴 يقدر يضيف CBC بطريقتين على نفس الزيارة → احتمال فاتورة مزدوجة على نفس التحليل.
المعمل🔴 CBC اللي اتضاف كخدمة عيادة عمره ما هيوصل الـworklist.
التقارير🔴 انقسام مؤكد. سطر المعمل clinic_service_id = null ⇒ خارج تجميع القسم · سطر الخدمة ⇒ داخله. نفس التحليل في bucketين. وزيادة: DecideClinicalIntent.php:110 بيسجّل التحليل المستقل بـline_type='clinic_service' مش 'lab' ⇒ بيختفي من أي فلتر تحاليل.

🔧 ثلاثة تصحيحات لقاعدة المعرفة (اكتشفها التحقيق)

#ادعاء الـKBالواقع في الكود
F2clinic_service مش في whitelist النيةغلط — موجود ومقبول (StoreClinicalIntentRequest.php:55)
F3مبدّل مصدر الـpicker غير موجودغلط — موجود بالكامل في الفرونت (lab-orders.component.ts:101,283)
F1الجسر lis_investigation_id غير مستهلكمؤكد — عمود ميت، صفر قراءات

الفجوة الحقيقية ليست «المبدّل غير موجود» — بل «المبدّل في الواجهة والباك إند لا تعرف عنه شيئاً».

٥. المشكلة (٢): النتيجة لا تصل الاستقبال

الخبر الجيد: الباك إند مبني بالكامل

الحدث (LIS)المستمع (العيادة)بيعمل إيه
LabResultReleasedDropLabReportOnEncounterينشئ ClinicalDocument (تقرير تحليل) + يقلب النية لـresulted
LabRequestCompletedMarkEncounterLabCompleteيقلب بند الخدمة ordered → performed
LabResultCorrectedRefreshEncounterLabReportيحدّث التقرير

الخبر السيئ: أربع حلقات مقطوعة فوقها

#الحلقةالدليل
١ الحارس المزدوج يقتل سيناريو «تحاليل بس» تماماً MarkEncounterLabComplete.php:22 بيرجع فوراً لو encounter_id فاضي — وزيارة تحاليل-بس مالهاش encounter (بياناتك: LabRequest #1 → encounter_id = NULL). وحتى مع encounter، بيحدّث ordered بس — وبندك billed. نفس الحارس في DropLabReportOnEncounter ⇒ ولا تقرير.
٢ صفر سطح عرض في العيادة قائمة تنقّل العيادة فيها Worklist للأشعة — ولا شيء للمعمل. بحث شامل: الطابور · الداشبورد · الاستقبال · الكاشير · ملف المريض ⇒ صفر عمود/بادج/عدّاد لحالة التحاليل. والـClinicalDocument اللي بيتعمل ⇒ صفر مستهلك في الواجهة.
٣ حقول ميتة — الواجهة تنتظر بيانات لا تُرسَل شاشة الطبيب بتحاول تعرض النتيجة (lab-orders.component.html:246@if (item.result_status)) والموديل معرّفهم — لكن LabOrderResource.php:26-34 مابيرجّعش result_status ولا result_value. الشارة دي عمرها ما هتظهر.
٤ قفل صلاحيات على الطباعة الطباعة محمية بـlis.result-reports.generate، وشاشة أدوار العيادة مقفولة على clinic.%مستحيل منح الصلاحية لدور استقبال403.
الخلاصة: الاستقبال لا يملك أي وسيلة — لا شاشة، ولا تنبيه، ولا عمود، ولا زر طباعة — ليعرف أن النتيجة جاهزة. والزبون بيقف قدامه يسأل. والحالة الأسوأ هي بالظبط حالتك الأساسية: «الزبون جاي عايز تحاليل بس» — دي مقطوعة من أول خطوة.

٦. حصر الحالات — كما طلبت

البُعد (أ) تحاليل فقط (ب) الدكتور طلب (ج) خدمة عيادة نوع lab (د) LIS مباشر
مين بيطلبالاستقبال — شاشة الحجزالدكتور «نية» → الاستقبال ينفّذالاستقبال — من كتالوج الخدماتالمعمل مباشرة (walk-in)
الكتالوجكتالوج LISكتالوج LISكتالوج العيادةكتالوج LIS
التسعيرقائمة الأسعارقائمة الأسعارسعر الخدمة (مستقل)الكتالوج = 0.000 ← القائمة الافتراضية لا تُطبَّق
فاتورةالعيادةالعيادةالعيادةفاتورة LIS + كاشير LIS
يوصل المعمل؟✅ نعم✅ نعم🔴 لا — أبداً✅ نعم
encounter_idNULLموجودNULL
النتيجة ترجع؟🔴 لا (حارس encounter)للداتا نعم — للشاشة لايدخلها الدكتور يدوياًلا (خارج العيادة)
الاستقبال يعرف؟🔴 لا🔴 لا🔴 لا

لاحظ: الحالة (ج) و(د) لم تذكرهما — لكنهما موجودتان في النظام فعلاً، و(ج) هي مصدر نزيف الفواتير.

٧. الفجوة — موجود مقابل مطلوب

البندموجود اليومالمطلوبلازم يتغيّر
وضع المعملمبدّل في الواجهة · الباك عمياءمصدر حقيقة واحدالباك يفرض الوضع: يمنع خدمات lab في المتكامل، ويمنع بنود LIS في المستقل
سطر تحليل من كتالوج العيادة🔴 يتحاسب ولا يصل المعمليا يوصل يا يترفضيُمنع في المتكامل · وفي المستقل يمشي على مسار النتيجة اليدوية
الجسر lis_investigation_idعمود ميت + خانة رقم عاريةيا يُستهلك يا يُحذفقرار — انظر §11
رجوع النتيجةمبني — لكن محروس بـencounter_idيشتغل لكل زيارةالربط بـservice_order_line بدل الـencounter
حالة النتيجة على البندعمود فوترة (ordered/billed) يُساء استخدامهحالة نتيجة مستقلةحقل جديد (result_status/result_ready_at) — لا تُخلط بالفوترة
سطح الاستقبالصفر«نتائج جاهزة» + طباعةشاشة/تبويب + بادج على الطابور
صلاحية الطباعةمقفولة على clinic.%الاستقبال يطبعصلاحية عيادة تفتح تقرير المعمل (نفس نمط ISS-2026-0177)
الباقات (packages)🔴 لا تمر على LabPriceResolver ⇒ تُسعَّر من الكتالوج (0.000)نفس التسعيرذيل مفتوح من ISS-2026-0176

٨. الحالات الحدّية

الحالةالسلوك اليوم
المريض مادفعش والعيّنة اتولّدت🔴 العيّنة والباركود بيتولّدوا لحظة الطلب — قبل أي دفع، ومفيش أي حارس. المعمل ممكن يشتغل على مريض مادفعش.
المريض دفع الكشف ثم الدكتور طلب تحاليل🔴 لو أوردر الزيارة اتقفل بالدفع → بيتفتح أوردر جديدالمريض يرجع للكاشير يدفع تاني. لو «دفع لاحقاً» → نفس الأوردر ودفعة واحدة.
دفع ومشي والنتيجة اتأخرتمفيش أي رابط. لو زيارة تحاليل-بس ⇒ صفر أثر في العيادة — المتابعة من المعمل أو بوابة المريض فقط.
بوابة المريض✅ شغالة — لو lis.auto_publish_on_release مفعّل، المريض بيستلم لينك ويشوف نتيجته بنفسه. الاستقبال برضه مايعرفش.

٩. معاينة الواجهة المتوقّعة

الشكل المقترح — للموافقة على هيئة الشاشة قبل أي كود واجهة.

١) شاشة جديدة: «نتائج التحاليل» في قائمة العيادة

🧪 نتائج التحاليل — الاستقبالاليوم · 4 جاهزة
✅ جاهزةحازم محمد — CBC، ZincLR-2026-00001
✅ جاهزةسارة أحمد — Vitamin DLR-2026-00004
⏳ تحت التشغيلمحمود علي — Lipid ProfileLR-2026-00006
🧾 لسه ما اتجمّعتش عيّنةمنى خالد — CBCLR-2026-00007

الملفات: شاشة جديدة تحت features/clinic/lab-results/ + عنصر في clinic-layout.component.ts (زي «Radiology Worklist» الموجود).

٢) بادج على شاشة الطابور — قبل / بعد

قبل
الطابور
حازم محمدQ-03
بعد
الطابور
حازم محمد🧪 نتيجة جاهزةQ-03

٣) شاشة الخدمات — منع خدمة نوع «تحاليل» في الوضع المتكامل

قبل — النوع «تحاليل» متاح دايماً
خدمة جديدة
النوعكشف · خدمة · تحاليل · أشعة
رقم تحليل LIS[ خانة رقم عارية ]
بعد — محكوم بالوضع
خدمة جديدة
النوعكشف · خدمة · تحاليل · أشعة
⚠️ المعمل بتاعنا مفعّل — التحاليل تُطلب من كتالوج المعمل مباشرة، مش من هنا. لو عندك معمل بره، غيّر «وضع المعمل» من الإعدادات.

١٠. خطة التنفيذ (WPs)

WPالنطاقالريبوالإثباتأعلام
WP1🔴 سدّ نزيف الفواتير: منع سطر line_type=lab من كتالوج العيادة في الوضع المتكامل (يترفض بـ422 برسالة واضحة). + منع إنشاء خدمة نوع lab في المتكامل.BE + FEتست: إضافة سطر lab من كتالوج العيادة في المتكامل → 422[FIN]
WP2الباك يفرض lab_mode: توصيل ClinicCatalogMode بمسارات الخدمات + الاستقبال + النية. مصدر حقيقة واحد.BEتستات لكل وضع
WP3🔴 رجوع النتيجة لكل زيارة: شيل حارس encounter_id — الربط بـfulfillment_id (موجود!). + حقل حالة نتيجة مستقل على البند (result_status / result_ready_at) بدل إساءة استخدام حالة الفوترة.BE + migrationتست: زيارة تحاليل-بس مدفوعة → النتيجة تُطلق → البند يتعلّم «جاهزة»migration
WP4سطح الاستقبال: شاشة «نتائج التحاليل» + بادج على الطابور + زر طباعة.BE + FEمعاينة §9
WP5فتح الطباعة للعيادة: صلاحية عيادة تسمح بتقرير المعمل (نفس نمط ISS-2026-0177: صلاحية OR على الكنترولر).BEتست: دور استقبال بلا صلاحيات معمل يطبع التقرير
WP6إصلاحات صغيرة: LabOrderResource يرجّع result_status/result_value (حقول ميتة) · DecideClinicalIntent:110 يكتب line_type='lab' للتحاليل.BEتستات
WP7الباقات: تمريرها على LabPriceResolver (ذيل مفتوح من ISS-2026-0176 — بتتسعّر 0.000 دلوقتي).BEتست تسعير باقة[FIN]

الترتيب: WP1 أولاً (نزيف مالي) → WP3 (النتيجة) → WP4+WP5 (السطح) → WP2 → WP6/WP7.

١١. قرارات تحتاج قرارك

#القرارتوصيتي
١ إنت شغال بأي وضع؟ المعمل بتاعنا (متكامل) ولا معمل بره (مستقل)؟ ولا الاتنين حسب العميل؟ متكامل (وده الافتراضي عندك). ساعتها خدمات نوع «تحاليل» تتمنع من كتالوج العيادة تماماً — والتحاليل تُطلب من كتالوج المعمل بس. ده بيقتل التعارض من جذوره.
٢ الجسر lis_investigation_id (العمود الميت): نستهلكه ولا نحذفه؟ نحذفه (أو نخفيه). لو الوضع متكامل، الخدمة نوع lab مالهاش لزوم أصلاً — فالجسر بيربط حاجة لحاجة مالهاش مكان. الإبقاء عليه = فخ للمستقبل.
٣ حالة النتيجة: حقل جديد مستقل، ولا نعيد استخدام حالة البند (performed حقل جديد. حالة البند حالة فوترة — خلطها بحالة النتيجة هو بالظبط سبب الحارس المكسور اليوم (بند مدفوع = billed ⇒ ما بيتحدّثش أبداً).
٤ سطح الاستقبال: شاشة مستقلة «نتائج التحاليل»، ولا بادج على الطابور بس؟ الاتنين — الشاشة للبحث والطباعة، والبادج عشان الموظف يشوف من غير ما يدور. المعاينة في §9.
٥ 🔴 الدفع المزدوج: لو المريض دفع الكشف والدكتور طلب تحاليل بعدها → بيتفتح أوردر جديد ويدفع تاني. مقبول؟ محتاج قرارك. الحل: نسمح بإعادة فتح الأوردر أو نجمع في فاتورة واحدة. ده سلوك مالي — مش هغيّره من غير موافقتك.
٦ 🔴 العيّنة قبل الدفع: العيّنة والباركود بيتولّدوا لحظة الطلب — قبل أي تحصيل. المعمل ممكن يشتغل على مريض مادفعش. نحط حارس؟ محتاج قرارك. فيه عيادات بتشتغل كده بقصد (خدمة أسرع). لو عايز حارس، يبقى إعداد: «التحليل ما يدخلش المعمل إلا بعد الدفع».
٧ الباقات بتتسعّر 0.000 (ذيل مفتوح من شغل التسعير السابق). نصلحها في نفس الدفعة؟ نعم — نفس المنطقة، وسهلة، وبتمنع فاتورة بصفر.