تحليل عميق: تحليل TSH «الشبح» في الطلب LR-2026-00256

Moon ERP — وحدة المختبر (LIS) · تقرير تشخيص ومعالجة جذرية · 2026-06-03
🐞 مشكلة سلامة بيانات (Data Integrity) — قابلة للتكرار

الملخّص التنفيذي

في الطلب LR-2026-00256 ظهر تحليل TSH (هرمون الغدة الدرقية) في شاشة التجميع (worklist) بحالة «بانتظار السحب» رغم إن المستخدم لم يطلبه. التحقيق أثبت إن TSH اتحطّ على الطلب وقت إنشائه بالظبط مع باقي التحاليل، وإنه تسرّب من باقة «فحص شامل» بسبب خلل في منطق إضافة/حذف الباقات في الـ wizard مدعوم بإن endpoint قائمة الباقات لا يرجّع تحاليل الباقة. النتيجة: تحليل «يتيم» (orphan) في قسم لم يُجمَّع، فيفضل عالق pending ويظهر في التجميع للأبد.

الخلاصة: مش غلطة مستخدم — ده باج معماري قابل للتكرار في منطق الباقات. والحل دائم لازم يكون في الـ wizard + الـ backend.

1 العرَض (Symptom)

2 الأدلة (Evidence) — من قاعدة البيانات والـ API مباشرة

دليل 1: TSH موجود على الطلب من لحظة الإنشاء (مش مضاف لاحقاً)

كل الـ 22 تحليل (ومنهم TSH) اتسجّلوا بنفس الطابع الزمني بالظبط:

-- lab_request_investigations (request 67)
created_at = 2026-06-03 01:27:50  →  invs = 1,2,3,4,7,16,17,22,23,24,25,26,39,40,93,94,95,745,746,747,748,749
-- مجموعة زمنية واحدة = كله اتبعت في طلب إنشاء واحد (مفيش إضافة لاحقة)

دليل 2: TSH هو التحليل الوحيد العالق pending — في قسم مختلف لم يُجمَّع

التحليلpivotالقسم (section_id)الحالةملاحظة
CBC + باقي 20 تحليل1 و 2receivedاتجمّعوا 01:37
TSH4335pendingupdated_at = created_at → اتساب من غير ما حد يلمسه
TSH في قسم 5 (الهرمونات) — المستخدم جمّع عيّنات قسم 1 و2 بس، فعيّنة TSH عمرها ما اتعملت → عالقة بانتظار السحب.

دليل 3: TSH ليس عضو في باقة CBC (مش panel expansion)

CBC panel members = WBC,RBC,HGB,HCT,MCV,MCH,MCHC,RDW,PLT,PDW,MPV,Neutrophil,Lymphocytes,Monocytes,Eosinophils,Basophils
TSH (17) ∉ أعضاء CBC  →  مش جاي من توسعة البانل

دليل 4: TSH عضو في باقة «فحص شامل» ✅ (مصدر التسرّب)

الباقةأعضاؤها
فحص شامل (id 1)CBC, FBS, TSH, TG, HDL, LDL, URIC, SGOT, SGPT, UA
باقة الغدة الدرقية (id 2)… TSH …
المطابقة الحاسمة: تحاليل الطلب = {CBC, FBS, TG, HDL, LDL, TSH} = أعضاء «فحص شامل» ناقص {URIC, SGOT, SGPT, UA}. يعني المستخدم تعامل مع باقة «فحص شامل»، شال 4 تحاليل مش عايزهم، لكن TSH فضل متعلّق.

دليل 5: مفيش باقة مُسجّلة على الطلب — TSH اتبعت كتحليل «فردي»

lab_requests(67): source=walk_in, package_discount = 0.000, notes=NULL
→ عند الإرسال selectedPkgIds كانت فاضية. الباقة مش متسجّلة، لكن تحاليلها اتبعتت كأفراد.

دليل 6 (السبب التقني الأعمق): endpoint قائمة الباقات لا يرجّع التحاليل

GET /lis/packages          → كل الباقات: investigations = NULL
GET /lis/packages/1        → investigations = [CBC, FBS, TG, HDL, LDL, TSH] ✅
// القايمة فاضية من التحاليل → الـ wizard مضطر يجيبها async وقت الضغط (getById)

3 السبب الجذري (Root Cause) — في كود الـ wizard

في request-wizard-v2.component.ts منطق togglePackage():

// إضافة الباقة — لأن القايمة بترجع investigations=null، بيروح للمسار async:
this.pkgSvc.getById(pkg.id).subscribe(res => {
  for (const inv of res.data.investigations) newTestIds.add(inv.id);  // يضيف TSH وكله
  this.selectedTestIds.set(newTestIds);
});

// حذف الباقة — بيعتمد على pkg.investigations (اللي ممكن تكون null أو لسه ماوصلتش):
for (const inv of pkg.investigations) testIds.delete(inv.id);  // لو null → ميشيلش حاجة!

المشاكل الثلاثة المتراكمة

  1. قائمة الباقات بدون تحاليل ⟵ الإضافة async (getById)، والحذف متزامن على pkg.investigationsعدم تماثُل + سباق زمني (race).
  2. selectedTestIds عبارة عن Set مسطّح مفيهوش أي ربط «التحليل ده جاي من باقة كذا» ⟵ لو الباقة اتشالت أو اتعدّلت، تحاليلها ممكن تتعلّق يتيمة (orphans) من غير ما النظام يعرف يشيلها.
  3. الـ UI ما بيعرضش بوضوح تحاليل الباقة كعناصر قابلة للحذف ⟵ المستخدم مايشوفش TSH علشان يشيله.

سيناريو إعادة الإنتاج (الأكثر ترجيحاً)

1. المستخدم يضغط باقة «فحص شامل» (add) → getById يطلق async… (لسه ماوصلش)
2. يضغط الباقة تاني بسرعة (remove) → pkg.investigations لسه nullميشيلش أي تحليل + يشيل الباقة من pkgIds
3. getById يرجع متأخر → يضيف كل تحاليل الباقة (ومنها TSH) لـ selectedTestIds — من غير باقة!
4. المستخدم يشيل اللي يعرفهم (URIC, SGOT, SGPT, UA) يدوي… لكن TSH يفوته
5. الإرسال: {CBC, FBS, TG, HDL, LDL, TSH}، packages=[] → package_discount=0 ✅ (مطابق للداتا بالظبط)

ملاحظة: حتى من غير «الضغط المزدوج»، أي تعديل على باقة قائمتها بدون تحاليل يقدر ينتج نفس اليُتم بسبب اعتماد الحذف على pkg.investigations غير المضمونة.

4 ليه بيظهر في الوورك-ليست بالذات؟

لمّا التحليل اليتيم (TSH) يبقى في قسم غير الأقسام اللي المستخدم جمّعها، عيّنته عمرها ما تتعمل، فيفضل pending للأبد ⟵ شاشة التجميع شغلتها تعرض كل pending ⟵ TSH يظهر «بانتظار السحب» كل مرة، ومايختفيش لأنه فعلاً موجود على الطلب.

5 الحل الدائم (Permanent Fix)

أولاً — Backend (الأرخص والأقوى)

  1. خلّي قائمة الباقات ترجّع تحاليلها (GET /lis/packages يضمّ investigations أو على الأقل investigation_ids). ده يلغي السباق الزمني لأن الـ wizard هيبقى عارف محتوى الباقة فوراً من غير async.

ثانياً — Frontend (request-wizard-v2)

  1. تتبّع مصدر الباقة: استبدال الاعتماد على pkg.investigations بـ Map<packageId, number[]> مخزّن وقت الإضافة، فالإضافة والحذف متماثلين 100% ومستقلين عن أي داتا بايتة.
  2. إضافة atomic: تعطيل زرّار الباقة أثناء تحميل تفاصيلها (منع الـ double-click race)، أو تحميل تحاليل كل الباقات مرّة واحدة عند فتح الـ wizard (forkJoin).
  3. عرض كل التحاليل المختارة (ومنها أعضاء الباقات) في قائمة Step-2 ولكل واحد زرّار حذف (×)، + شاشة مراجعة نهائية بقائمة التحاليل قبل التأكيد ⟵ المستخدم يشوف TSH ويقدر يشيله.

ثالثاً — تنظيف فوري للبيانات

  1. إزالة TSH اليتيم من الطلب الحالي:
    DELETE /lis/requests/67/investigations/17   // يشيل pivot 433 (TSH)
  2. (اختياري) فحص باقي الطلبات بحثاً عن تحاليل pending «يتيمة» في قسم مختلف عن باقي الطلب وتنظيفها.
ملاحظة إضافية لقيتها: getById(باقة 1) رجّع 6 تحاليل بينما جدول lab_package_investigations فيه 10 صفوف للباقة — في تضارب في داتا الباقة نفسها (غالباً 4 أعضاء غير نشطين/محذوفين). يستحق مراجعة منفصلة لباقة «فحص شامل».

6 الخطوة التالية

جاهز أنفّذ بالترتيب ده فور موافقتك:

  1. تنظيف فوري: شيل TSH من LR-2026-00256 (يختفي من التجميع حالاً).
  2. Backend: قائمة الباقات ترجّع التحاليل.
  3. Frontend: تتبّع مصدر الباقة (Map) + إضافة atomic + قائمة مراجعة قابلة للحذف.

قول لي «ابدأ» وأبدأ بالتنظيف الفوري ثم الإصلاح الجذري.