لماذا تفشل طباعة الباركود بـ «لا توجد عينات» لطلبات معلّقة بينما تنجح لطلبٍ آخر بنفس الحالة؟

تحقيق جذري — موديول المختبر (LIS) · قاعدة بيانات التطوير moonui_dev_be · 2026‑06‑18
السبب الجذري: توليد العينات منفصل عن إنشاء الطلب ويعمل في الواجهة (fire‑and‑forget) LR‑00356 / 00357 = 0 عينة LR‑00358 = 2 عينة

1) العَرَض والمقارنة بين الطلبات الثلاثة

في صفحة الطلبات (Requests)، زر طباعة ملصقات الباركود يفشل ويظهر تنبيه LIS.SAMPLES.NO_SAMPLES («لا توجد عينات») لطلبين معلّقين، بينما ينجح لطلب ثالث بنفس الحالة تماماً (pending). الفرق الوحيد الفعلي: وجود صفوف عينات في جدول lab_samples من عدمه.

الحقل LR‑2026‑00356 (id 46) LR‑2026‑00357 (id 47) LR‑2026‑00358 (id 48)
الحالة (status)pendingpendingpending
المصدر (source)walk_inwalk_inwalk_in
أنشأه المستخدم (created_by)19 — Aghsan adel19 — Aghsan adel3 — Ahmed
وقت الإنشاء18:54:3319:05:2919:12:03
عدد الفحوصات (investigations)2483
فحوصات بلا specimen_type000
فحوصات بلا lab_section000
أقسام مميّزة3 (1,2,5)2 (2,5)1 (5)
عدد صفوف lab_samples002
نتيجة طباعة الباركود✗ «لا توجد عينات»✗ «لا توجد عينات»✓ تطبع
ملاحظة مهمة لإزالة اللبس: عينات الطلب الناجح 00358 لها collected_at = NULL و status = pending — أي أنها لم تُجمَّع فعلياً؛ فقط صفوف العينات موجودة. كلمة «collected» في رسالة الخطأ تسمية مضلِّلة: البوابة في الواجهة لا تتحقق من «الجمع»، بل فقط من وجود صفوف عينات للطلب.

تفاصيل عيّنات الطلب الناجح 00358 (الدليل المرئي)

idbarcodeparent_idlab_section_idspecimen_type_idstatuscollected_atcreated_bycreated_at
93 (الأنبوب الأب)26061800128NULLNULLNULLpendingNULL319:12:06
94 (الأنبوب الابن)2606180012859352pendingNULL319:12:06

parent barcode (26061800128) + section suffix → child barcode (260618001285) · أُنشئتا بعد 3 ثوانٍ من إنشاء الطلب (19:12:03 → 19:12:06)

2) السبب الجذري (بالأدلة file:line)

السبب الجذري: صفوف lab_samples (الأنابيب/الباركود) لا تُنشأ تلقائياً عند إنشاء طلب المختبر. الـ Backend لا يولّد أي عينة عند إنشاء الطلب (لا Observer، لا Event/Listener، ولا أي LabSample::create داخل إجراء إنشاء الطلب). توليد العينات هو خطوة منفصلة تشغّلها الواجهة (Front‑end) بعد نجاح الطلب، عبر استدعاء POST /lis/samples (auto_split) ثم POST /lis/samples/{id}/aliquot. هذه الخطوة fire‑and‑forget ومنفصلة عن حفظ الطلب: فإن لم تُنفَّذ (أو فشلت في المتصفح)، يُحفَظ الطلب «pending» بلا أي عينات — فتفشل بوابة طباعة الباركود لأنها تتحقق فقط من وجود الصفوف.

أ) الـ Backend لا يولّد العينات عند إنشاء الطلب

ب) ما الذي يُنشئ العينات فعلاً (مسارات صريحة)

الصفالملف:السطرالوصف
الأنبوب الأب (parent) LabSampleController.php:178 / 207 store(): مع auto_split=true يُنشئ أنبوباً أباً واحداً (parent_id/lab_section_id = NULL, status=pending) + باركود عبر generateBarcode(). ثم يملأ pivot عبر SampleInvestigationService::createForSample() (~238).
الأنابيب الأبناء (children) LabSampleService.php:533‑700 (إنشاء عند 635) aliquot(): يجمّع فحوصات الطلب حسب (lab_section_id, specimen_type_id) ويُنشئ أنبوباً ابناً لكل مجموعة (parent_id + lab_section_id مضبوطان). وهو exactly الأنبوب 94 للطلب 00358.
الباركود LabSampleService.php:726 generateBarcode(): باركود الابن = باركود الأب + لاحقة القسم (مطابق لـ 00358).

ج) مَن يُطلق هاتين الخطوتين؟ الواجهة فقط (المعالج النشِط v2)

معالج الطلب النشِط في التطبيق هو request‑wizard‑v2 (الموصول في app.routes.ts:204‑205 و lis-standalone.routes.ts:142‑152؛ معالج v1 القديم لم يعد مُوجّهاً إليه من أي مسار). بعد نجاح الطلب يستدعي:

onOrderSuccess()request-wizard-v2.component.ts:1714‑1779
1) sampleSvc.create({ lab_request_id, patient_id, auto_split:true, investigation_ids }) (السطر 1737) ← الأب.
2) لكل أبٍ غير خارجي: sampleSvc.aliquot(s.id) ← الأبناء.
3) ثم يطبع الملصقات وينتقل للصفحة التالية.
الثغرة الحرجة: هذه السلسلة fire‑and‑forget في المتصفح ومنفصلة عن حفظ الطلب. في فرع الخطأ (السطر ~1772) يُظهر toast ثم ينتقل بعيداً على أي حال (navigateAfterSuccess()). أي إنهاء/خطأ/إغلاق قبل وصول هذين الـ POST → طلب محفوظ «pending» بلا عينات.

د) بوابة طباعة الباركود تتحقق فقط من وجود الصفوف

printLabels()requests/lis-requests.component.ts:994‑1005
sampleService.listByFilter({ lab_request_id, per_page:50 }) → إذا allSamples.length === 0 → toast severity:'warn'، detail = LIS.SAMPLES.NO_SAMPLES، ثم return. لا يُنشئ عينات ولا يطبع — يقرأ فقط. الشرط الوحيد هو وجود الصفوف (وليس status ولا collected_at).

3) دورة حياة توليد العينات (متى/ماذا يُنشئ الأنابيب)

1. إنشاء الطلب    CreateLabRequest.php — يحفظ الطلب (+ فاتورة اختيارية). لا عينات إطلاقاً.
2. توليد الأنابيب (نقطة الفشل)    الواجهة onOrderSuccess() تستدعي POST /lis/samples (auto_split) → الأب  ثم  POST /lis/samples/{id}/aliquot → الأبناء. الصفوف تُكتب بـ status=pending, collected_at=NULL. «الأنبوب موجود + الباركود مُولّد».
3. طباعة الباركود    printLabels() يقرأ الصفوف الموجودة فقط. يحتاج خطوة 2 أن تكون تمّت.
4. الجمع (لاحقاً، اختياري لطباعة الباركود)    LabSampleService::collect() (POST /lis/samples/{id}/collect) يقلب status→Collected ويختم collected_at/collected_by. هذه خطوة مستقلة عن طباعة الباركود.
الوضع الطبيعي المقصود: طلب «pending» تكون أنابيبه مُولّدة لكن غير مجموعة (collected_at=NULL) — وهو بالضبط حال 00358. الشاذّ هنا هو 00356/00357 (pending بلا أنابيب)، وليس 00358. تأكيد إضافي: with-trashed = 0 لـ 46/47 — العينات لم تُنشأ أصلاً، ولم تُحذف.

4) لماذا يختلف 00358؟

الفارق ليس في الحالة، ولا في تهيئة الفحوصات (specimen/section). تأكدنا من قاعدة البيانات الحيّة: جميع فحوصات 46/47/48 مهيّأة بالكامل (specimen_type و lab_section غير NULL، و 46/47 أغنى بالأقسام من 48). إذاً تهيئة الفحوصات ليست السبب.

الفارق الحقيقي مُرتبط ارتباطاً تامّاً بـ هل نُفِّذت خطوة توليد العينات للطلب أم لا، وهو ما يطابق created_by:

باختصار: 00358 مرّ بخطوة توليد العينات؛ 00356/00357 توقّفا عند حفظ الطلب فقط. والبوابة في الواجهة تبحث عن صفوف مسؤول عن إنشائها إجراءٌ مستقلٌّ تماماً.

5) خيارات الإصلاح + التوصية

#الخيارالوصفالتقييم
أ حلٌّ يدوي فوري سريع للطلبين العالقين: نفّذ POST /lis/samples (auto_split) + aliquot يدوياً (أو من شاشة العينات/إعادة فتح المعالج) لتوليد الأنابيب. يعالج العَرَض فوراً لكنه لا يمنع التكرار.
ب توليد العينات في الـ Backend عند إنشاء الطلب موصى به اجعل توليد الأنبوب الأب + aliquot() يحدث داخل CreateLabRequest.php (أو عبر Event/Listener على «طلب أُنشئ») داخل نفس الـ transaction. يُعاد استخدام LabSampleService::aliquot() الموجود. يضمن وجود الأنابيب لأي طلب بغضّ النظر عن مسار الإنشاء أو فشل المتصفح. جذري ودائم.
ج اجعل printLabels() يولّد العينات عند غيابها بدل التنبيه عند length===0، يستدعي إنشاء+aliquot ثم يطبع («اطبع قبل الجمع»). يحل الطباعة فقط؛ تبقى مسارات أخرى بلا عينات. أقل شمولاً من (ب).
د تشديد سلسلة الواجهة + صلاحيات منع الانتقال قبل تأكيد إنشاء العينة؛ إظهار خطأ صريح؛ التأكد أن المستخدم 19 يملك lis.samples.create. تحسين دفاعي مكمّل، لكنه يبقي المنطق في الواجهة (هشّ).
هـ إصلاح تهيئة الفحوصات غير منطبق: تهيئة specimen/section سليمة على الطلبات الثلاثة (تم التحقق).

التوصية

اعتمد الخيار (ب): نقل توليد العينات إلى الـ Backend عند إنشاء الطلب — وهو يتماشى مع قاعدة المشروع «المنطق في الـ Backend أولاً، والواجهة تستهلك فقط». بذلك يحصل كل طلب على أنابيبه ضمن نفس الـ transaction، مستقلاً عن أي مسار إنشاء أو فشل في المتصفح، ويظل printLabels() مجرد قارئ.
كـ إصلاح فوري طبّق (أ) على الطلبين 00356/00357 لإلغاء العَرَض الحالي، و(د) كتقوية دفاعية للواجهة حتى يصل (ب).

6) ملاحظات بيانات/تهيئة (Gotchas)