لماذا تفشل طباعة الباركود بـ «لا توجد عينات» لطلبات معلّقة بينما تنجح لطلبٍ آخر بنفس الحالة؟
تحقيق جذري — موديول المختبر (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)
pending
pending
pending
المصدر (source)
walk_in
walk_in
walk_in
أنشأه المستخدم (created_by)
19 — Aghsan adel
19 — Aghsan adel
3 — Ahmed
وقت الإنشاء
18:54:33
19:05:29
19:12:03
عدد الفحوصات (investigations)
24
8
3
فحوصات بلا specimen_type
0
0
0
فحوصات بلا lab_section
0
0
0
أقسام مميّزة
3 (1,2,5)
2 (2,5)
1 (5)
عدد صفوف lab_samples
0
0
2
نتيجة طباعة الباركود
✗ «لا توجد عينات»
✗ «لا توجد عينات»
✓ تطبع
ملاحظة مهمة لإزالة اللبس: عينات الطلب الناجح 00358 لها
collected_at = NULL و status = pending —
أي أنها لم تُجمَّع فعلياً؛ فقط صفوف العينات موجودة. كلمة «collected» في رسالة الخطأ
تسمية مضلِّلة: البوابة في الواجهة لا تتحقق من «الجمع»، بل فقط من وجود صفوف عينات للطلب.
تفاصيل عيّنات الطلب الناجح 00358 (الدليل المرئي)
id
barcode
parent_id
lab_section_id
specimen_type_id
status
collected_at
created_by
created_at
93 (الأنبوب الأب)
26061800128
NULL
NULL
NULL
pending
NULL
3
19:12:06
94 (الأنبوب الابن)
260618001285
93
5
2
pending
NULL
3
19: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 لا يولّد العينات عند إنشاء الطلب
Modules/LIS/app/Actions/CreateLabRequest.php — إجراء إنشاء الطلب: صفر إشارات لـ LabSample. يُنشئ فقط فاتورة اختيارياً (auto_generate_invoice، الأسطر 294‑300) ثم يُحمّل علاقة samples (السطر ~317) التي ستكون فارغة.
Modules/LIS/app/Providers/EventServiceProvider.php — كل المستمعين (LabSampleCollected / LabSampleReceived / LabResult*) يعملون بعد وجود العينة، ولا يوجد مستمع على «إنشاء الطلب».
ب) ما الذي يُنشئ العينات فعلاً (مسارات صريحة)
الصف
الملف:السطر
الوصف
الأنبوب الأب (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 ينجح: أنشأه المستخدم 3 (Ahmed)؛ سلسلة onOrderSuccess() نفّذت بنجاح → store(auto_split) أنشأ الأب 93 → aliquot() أنشأ الابن 94 (القسم 5)، كلها في 19:12:06 (بعد 3 ثوانٍ من الطلب). كلا الصفّين created_by=3.
00356/00357 يفشلان: أنشأهما المستخدم 19 (Aghsan adel) عبر مسار لم تكتمل فيه خطوة توليد العينات (إنهاء/خطأ متصفح/غياب صلاحية lis.samples.create/انتقال قبل وصول الـ POST) → لم يُستدعَ store/aliquot → صفر عينات.
باختصار: 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)
«collected» تسمية مضلِّلة: البوابة تتحقق من وجود الصفوف لا من الجمع؛ 00358 ينجح رغم أن أنابيبه collected_at=NULL.
لا يوجد إعداد (setting) يولّد العينات: لا lis.auto_generate_samples ولا ما يشبهه. lis.auto_generate_invoice يخصّ الفاتورة فقط. lis.require_payment_before_sample يَحكم الجمع لا التوليد.
بوابة aliquot:LabSampleService.php:582 يُصفّي الفحوصات بـ investigation?->lab_section_id؛ إن لم يكن لأيٍّ منها قسم → aliquot_no_billable_investigations (582‑602). الفحوص الخارجية/المؤجَّلة تُستبعَد. (غير منطبق هنا — كل الفحوص لها قسم.)
specimen_type_id ليس بوابة صارمة: القيمة NULL ترجع لـ specimen القسم الأساسي (584‑595) أو specimen الأب.
قاعدتان مختلفتان: الحيّة = moonui_dev_be (التي عليها هذه البيانات). لا تُجري اختبارات/مسح عليها — غير مسجّلة في binlog.
تأكيد عدم الحذف:with-trashed = 0 لـ 46/47 — العينات لم تُنشأ، ولم تُحذف.