تحقيق — شاشة إضافة زيارة: القسم الإجباري + غياب اختيار «داخلي / خارجي» للتحاليل

تذكرة البورتال ISS-2026-0173 · مشروع «مون3 حسابات» (288) · العميل: حازم2 · النوع: bug
الشاشة: /app/clinic/booking Modules/Clinic + Modules/LIS عَرَض (ب): عيب حقيقي — قابل للإصلاح عَرَض (أ): فيتشر — غير مبني إطلاقاً 2026-07-13

١. العرض / الأعراض

العميل بلّغ عن مشكلتين في نفس الشاشة. التحقيق أثبت إنهما مشكلتان مختلفتان جذرياً — واحدة عيب حقيقي في كود موجود، والتانية قدرة غير مبنية من الأساس.

عَرَض (ب) الشاشة بتفرض «قسم» على زيارة تحاليل/أشعة

المتوقعإضافة تحليل أو أشعة من غير ما النظام يسأل عن قسم — القسم مفهوم خاص بالأطباء والكشف.
الفعليلما الزيارة تبقى تحاليل/أشعة فقط (من غير كشف)، بيظهر حقل «قسم الزيارة» إجباري وبيمنع الحفظ لحد ما تختار قسم.
إعادة الإنتاج١) افتح /app/clinic/booking · ٢) اختر مريض · ٣) ماتضفش أي كشف · ٤) ضيف تحليل (أو أشعة) بس · ٥) → يظهر «قسم الزيارة» بعلامة إجباري وزر الحفظ متعطّل.
ملاحظة مهمةلو ضفت كشف + تحليل مع بعض، الحقل بيختفي خالص — لأن القسم بييجي من سطر الكشف. فالمشكلة محصورة في سيناريو «تحاليل/أشعة بس».
الخطورةمتوسطة — احتكاك تشغيلي يومي + بيانات قسم مغلوطة على الزيارة (بتأثر على تقرير «الإيراد حسب القسم»). مفيش خسارة مالية مباشرة.

عَرَض (أ) مفيش اختيار «التحليل يتعمل عندنا / بره» + سعر مختلف

المطلوبعند إضافة تحليل في الزيارة: أختار هيتعمل عندنا (يظهر في الـ LIS بتاعنا ويدخل دورة العمل) ولا بره (مايظهرش عندنا) — وبسعر مختلف.
الفعليمفيش أي اختيار. أي تحليل يتضاف من الحجز بيتولّد حتمياً كطلب في الـ LIS. والقرار «داخلي/خارجي» متحدّد مسبقاً في كتالوج التحليل نفسه (is_outsourced) — مش قرار وقت الطلب.
الخطورةليست باج — قدرة غير موجودة. مفيش عمود يستقبل الاختيار حتى لو الواجهة بعتته، ومفيش مفهوم «سعر بيع مختلف للخارجي» في النظام كله.
الحكم على الحجم (Size Triage): (ب) = تعديل مضبوط في مكانه (شاشة واحدة + حارس واحد + قيد عمود واحد). (أ) = فيتشر — بتحقّق ٥ من ٥ معايير: بتمسّ موديولين (Clinic + LIS) · محتاجة أعمدة/migration جديدة · أكتر من جولة تسليم · قدرة جديدة مش ضبط قدرة قايمة · بتلمس التسعير والمحاسبة [FIN]. → لازم تتفصل في تذكرة مستقلة بعقد نطاق قبل أي تنفيذ.

٢. خط التتبّع (hop by hop، بأدلة)

مسار (ب) — من الشاشة لحد قيد قاعدة البيانات

#المحطةاللي اتشاف
١create-visit.component.html:155-168حقل «قسم الزيارة» بـ class="fieldlbl required" — بيظهر تحت شرط @if (requiresTopDeptPicker()).
٢create-visit.component.ts:459-463الشرط نفسه:
requiresTopDeptPicker = computed(() =>
  this.consultations().length === 0 &&
  (this.labTests().length > 0 || this.radProcedures().length > 0)
);
٣create-visit.component.ts:543بيوقف الحفظ فعلياً: if (this.requiresTopDeptPicker() && !this.visitDeptId()) return false; جوّه canSubmit.
٤create-visit.component.ts:1321-1323بيتبعت للـ BE على مستوى الزيارة (مش على بند التحليل): department_id: requiresTopDeptPicker() ? visitDeptId() : undefined.
٥CreateVisitRequest.php:92-96الحارس في الباك — وبيقول السبب بنفسه في تعليق:
// appointments.department_id is NOT NULL. A consultation supplies it from
// the doctor/dept; a lab/rad-only visit must carry an explicit department.
if (empty($consultations) && $this->input('department_id') === null) {
    $v->errors()->add('department_id', 'department_id is required when there are no consultations.');
}
٦2026_06_22_110001_create_appointments_table.php:20-21الجذر النهائي:
// department_id → departments (HRM), always required
$table->unsignedBigInteger('department_id')->index();   // ← NOT NULL
// doctor_id → lab_doctors, NULLABLE (department-only booking)
$table->unsignedBigInteger('doctor_id')->nullable()->index();
٧قاعدة البيانات الحيّة moonui3_dev_beتحقّق مباشر: appointments.department_id → nullable=NO · appointments.doctor_id → nullable=YES. القيد حقيقي، مش نظري.

مسار (أ) — من الحجز لحد الـ LIS

#المحطةاللي اتشاف
١clinic-lab-order.service.ts:10-17الـ DTO اللي بيغذّي منتقي التحاليل في العيادة فيه بس: id, code, name_ar, name_en, price, section. بيرمي is_outsourced وdefault_external_lab_id — رغم إنهما موجودان في موديل الـ LIS الأصلي (lis-investigation.model.ts:114-116).
٢create-visit.component.ts:1341-1348الـ payload بيبعت { investigation_id, price } بس. صفر flag داخلي/خارجي.
٣CreateVisit.php:120-125لو فيه تحاليل → بينادي OrderLabFromEncounter مباشرة، بدون أي شرط.
٤OrderLabFromEncounter.php:65-80بيبني payload لـ LIS من غير external_lab_id ولا أي flag، وبينادي CreateLabRequest::handleFromArray().
٥CreateLabRequest.php:263-291سلسلة التسعير: سعر صريح ← تسعير inbound لعميل B2B ← قائمة أسعار الدكتور ← investigation->price. is_outsourced مش مقروء ولا مرة (grep = صفر).
٦LabSampleService.php:593, 623-624هنا بس بيتفرّع — على investigation->is_outsourced (خاصية الكتالوج): الداخلي → أنبوبة → كانبان · الخارجي → عينة منفصلة لكل معمل + إحالة.
٧lab_request_investigations (migration 2026_03_01_000009:14-27)مفيش أي عمود is_outsourced / external_lab_id على سطر الطلب — يعني مفيش مكان أصلاً تخزّن فيه اختيار المستخدم.

٣. السبب الجذري

السبب الجذري لـ (ب) — مؤكَّد على ٤ مستويات

عمود appointments.department_id اتعمل NOT NULL على افتراض إن «كل حجز بينتمي لقسم» — والافتراض ده بيتكسر في زيارة تحاليل/أشعة بس، فالقيد بيرتدّ لفوق ويطلع للمستخدم كحقل قسم إجباري بلا أي معنى إكلينيكي.

الدليل الحاسم إن ده عيب تصميمي مش قاعدة مقصودة:

الخلاصة: الشاشة بتفرض على المستخدم يدخل بيانات بتتخزّن على الزيارة، مالهاش أي أثر على التسعير أو التنفيذ، وبتلوّث تقرير «الإيراد حسب القسم» (ClinicReportService.php:30-31 بيعمل JOIN على القسم). قيمة سالبة صافية.

السبب الجذري لـ (أ) — مش باج: قدرة غير مبنية

مطلب العميل فيه جزأين منفصلين، والاتنين غير موجودين — بس لأسباب مختلفة:

الجزءالحالة الفعليةالدليل
الاختيار اللحظي (عندنا/بره وقت الطلب) الـ outsourcing في النظام = خاصية ثابتة على كتالوج التحليل (is_outsourcedمش قرار وقت الطلب. لو التحليل معلّم outsourced → بيروح بره حتمياً بدون اختيار؛ ولو مش معلّم → مفيش أي طريقة من شاشة العيادة تبعته بره. الفرع الوحيد: LabSampleService.php:593,623 (على الكتالوج). وlab_request_investigations مفيهوش عمود يستقبل الاختيار.
السعر المختلف للتحليل الخارجي غير موجود إطلاقاً. جدول lab_external_lab_pricing موجود وفيه price + direction — لكنه بيوصف علاقتنا المالية بالمعمل الخارجي (outbound = تكلفة بندفعها له · inbound = إيراد منه كعميل B2B) — مش سعر بيع للمريض. PricingResolver عنده ٤ محاور (doctor→grade→department→base) — مفيش محور external. وأي send-out بيكتب السعر على referral->tests بس، ومش بيلمس lab_request_investigations.price ولا ServiceOrderLine.unit_price — يعني المريض بيفضل مدفوع عليه السعر الداخلي.
نتيجة خطيرة موجودة اليوم (مستقلة عن التذكرة): لو تحليل اتبعت بره بعد ما المريض دفع (send-out من الكانبان)، تكلفة المعمل الخارجي بتتسجّل، لكن سعر المريض ما بيتغيّرش. فالربح الحقيقي على البند ده مش متعكس في أي مكان. ده مش عَرَض شكا منه العميل — ده اكتشاف جانبي من التحقيق، ومحتاج قرار.

٤. ليه حصلت — الآلية

العَرَضالآليةالدليل الزمني
(ب) افتراض خاطئ عند التصميم (مش انحدار). جدول appointments اتصمّم حوالين نموذج «العيادة = أقسام وأطباء»، فالقسم اعتُبِر البُعد الأساسي الثابت (والطبيب هو المتغيّر الاختياري). وقت بناء الحجز، حالة «زيارة تحاليل/أشعة من غير كشف» — اللي ملهاش قسم طبيعي — ما دخلتش في الحسبان. لما الحالة دي اتبنت بعدين، الحل كان تمرير القيد للمستخدم (خليه يختار قسم) بدل ما القيد نفسه يتراجع. القيد اتولد في 2ccf66017 (2026-06-22) — «P3 scheduling & booking BE». والتعليق فوق العمود مكتوب فيه حرفياً «always required» — يعني اتكتب عن قصد تحت الافتراض ده.
(أ) فجوة نطاق موثّقة، مش خطأ. نموذج الـ outsourcing في LIS اتبنى لحالة استخدام المعمل المستقل (تحاليل معيّنة إحنا أصلاً مابنعملهاش، فبتتعلّم في الكتالوج مرة واحدة). حالة العيادة — نفس التحليل ممكن يتعمل جوه أو بره حسب اختيار لحظي، وبسعر مختلف — حالة استخدام مختلفة تماماً ما اتصمّمتش أبداً. الـ KB نفسه بيسجّل الفجوة: clinic.lab_mode / rad_mode مُعرَّفين كـ«فجوة F3». وتصحيح للـ KB: التحقيق أثبت إنهم مبنيين نص بناء ومقطوعين — فيه seeder (:120,:136) وقارئ كامل (ClinicCatalogMode.php) لكن isStandalone() صفر استدعاءات، والمستهلك الوحيد سطر يتيم في EncounterResource.php:130-131.

٥. كل الأبعاد + الأشقاء الكامنة

البُعدالفحص
البياناتنظيفة — مفيش صفوف فاسدة تحتاج إصلاح. على moonui3_dev_be: clinic_services = 0 صف · lab_external_labs = 0 · تحاليل is_outsourced=1 = 0. يعني الإصلاح مايحتاجش أي migration إصلاح بيانات — بس خلّي بالك: على تثبيت عميل حقيقي فيه بيانات، الزيارات القديمة اللي اتحطلها قسم عشوائي عشان الشاشة فرضته هتفضل بقسمها المغلوط في تقرير الإيراد.
مسار الكودالحارس في CreateVisitRequest هو المستهلك الوحيد للقيد في مسار الحجز. AddServiceOrderLineRequest.php:29 بيعرّف department_id كـ nullable — فمسار «إضافة بند» مش متأثر أصلاً.
الإعداداتclinic.lab_mode / rad_mode مبنيين ومقطوعين (فوق). تحكّم شبه ميت — المدير يقدر يغيّره ومحصلش حاجة في مسار الحجز.
الصلاحياتغير متأثرة بـ (ب). أما (أ) فالـ send-out محكوم بصلاحية lis.samples.send-external — أي اختيار «بره» من العيادة هيحتاج قرار: نفس الصلاحية ولا جديدة؟
الفلوس [FIN](ب) لا يمسّ الفلوس — القسم مش محور تسعير للتحاليل أصلاً. (أ) [FIN] بالكامل — سعر بيع مختلف + تكلفة معمل خارجي + هامش ربح + قيود GL. أي شغل فيه لازم يعدّي على مراجعة مالية.
i18n / RTLمفتاحان قائمان: CLINIC_VISIT.VISIT_DEPARTMENT وCLINIC_VISIT.VALIDATION_DEPT — لازم يتشالوا/يتعدّلوا مع إصلاح (ب) في ar.json + en.json.
حالات حدّيةزيارة «كشف + تحليل» مش متأثرة (القسم بييجي من الكشف). المتأثر بس: «تحاليل و/أو أشعة من غير كشف».

الأشقاء الكامنة (نفس النمط في أماكن تانية)

المكانالحالةالحكم
reception-order.component.tsبتعمل نفس الشغل (تضيف بنود تحاليل/أشعة لطلب) — ومابتسألش عن قسم خالص. والعقد المشترك AddOrderLine (clinic-order.service.ts:81-88) مفيهوش department_id أصلاً.سليمة — ودي الدليل الحاسم إن سلوك شاشة الحجز شاذ، مش قاعدة في النظام
lab-orders / rad-orders (encounter)القسم بيظهر كـ تصنيف اختياري (nullable) عند إنشاء خدمة standalone بسرعة — مش شرط.سليمة
CreateVisitRequestالمستهلك الوحيد للقيد.المتأثر الوحيد
خلاصة سوّاح الأشقاء: شاشة الحجز هي الوحيدة في النظام اللي بتفرض قسم لإضافة تحاليل/أشعة. مفيش نمط مكرّر محتاج نفس الإصلاح في مكان تاني — الإصلاح موضعي ومحدود.

٦. الحلول المقترحة

لـ (ب) — القسم الإجباري

#الحلالمقايضة
١ الموصى بهخلّي appointments.department_id nullable (migration للأمام)، شيل الحارس من CreateVisitRequest:92-96، وشيل الـ picker الإجباري من الشاشة (requiresTopDeptPicker + canSubmit:543). بيصلح فئة المشكلة مش النسخة. بيوفّق الجدول مع تصميمه هو (doctor_id nullable بالفعل) ومع clinic_services.department_id (nullable). تكلفة: migration + مراجعة أي كود بيفترض القسم موجود على الـ appointment (تقرير الإيراد لازم يتعامل مع NULL كـ«بدون قسم»).
٢ سيبه إجباري بس حطّ قسم افتراضي مخفي («المعمل/الأشعة») بدل ما تسأل المستخدم. أسرع وبدون migration — لكنه بيداري العَرَض ويكذب على البيانات: بيخترع قسم مش حقيقي وبيلوّث تقرير الإيراد بشكل ممنهج. مش موصى به.
٣ سيب القيد وخلّي الحقل اختياري في الشاشة بس. مستحيل — العمود NOT NULL على مستوى قاعدة البيانات، فالحفظ هيرمي خطأ 500. مرفوض تقنياً.

لـ (أ) — الداخلي/الخارجي + السعر

مش هينفّذ في التذكرة دي. ده فيتشر بيمسّ موديولين + schema + تسعير + محاسبة. الموصى به: تذكرة مستقلة بعقد نطاق. الشكل المعماري المرشّح (للنقاش مع العميل، مش قرار نهائي):
  1. عمود قرار على سطر الطلبlab_request_investigations.fulfillment (internal|external) + external_lab_id، بحيث القرار يبقى لحظي وقابل للتخزين بدل ما يكون خاصية كتالوج ثابتة. الديفولت = قيمة الكتالوج (is_outsourced) — فالسلوك الحالي مايتغيّرش لحد ما المستخدم يغيّر بإيده.
  2. محور تسعير جديد — سعر بيع للمريض لما البند خارجي (مش تكلفة المعمل — دي موجودة). القرار الجوهري للعميل: السعر الخارجي يتحدّد إزاي؟ (سعر ثابت تاني للتحليل؟ نسبة فوق تكلفة المعمل؟ إدخال يدوي وقت الحجز؟)
  3. توصيل الفرعLabSampleService يقرأ قرار السطر بدل خاصية الكتالوج، فالبند الخارجي مايدخلش الكانبان ويولّد إحالة.

٧. خطة الإصلاح (WPs) — لعَرَض (ب) فقط

WPالنطاقالريبو / الملفاتالإثباتأعلام
WP1 migration للأمام: appointments.department_idnullable. + مراجعة كل قارئ بيفترضه موجود (خصوصاً ClinicReportService «الإيراد حسب القسم») عشان يتعامل مع NULL كـ«بدون قسم» بدل ما يسقط الصف. BE — migration جديدة · ClinicReportService.php:30-31 Pest: زيارة تحاليل-فقط تتحفظ بـ department_id = null · تقرير الإيراد مايفقدش الصف migration
WP2 شيل الحارس اللي بيفرض القسم لما مفيش كشف. BE — CreateVisitRequest.php:92-96 Pest: يفشل قبل الإصلاح، ينجح بعده — POST زيارة تحاليل-فقط بدون department_id → 201 مش 422
WP3 شيل الـ picker الإجباري من الشاشة: requiresTopDeptPicker + حارس canSubmit + validationHint + الحقل في الـ HTML + مفتاحَي i18n. FE — create-visit.component.ts:459,543,559,1321 · .html:155-168 · ar.json/en.json ng build أخضر + إعادة إنتاج يدوية: زيارة تحاليل-فقط تتحفظ من غير ما تسأل عن قسم

الترتيب إلزامي: WP1 قبل WP2 (لو شِلت الحارس والعمود لسه NOT NULL → خطأ 500 عند الحفظ).

٨. قرارات تحتاج المالك

#القرارالتوصية
١ نفصل عَرَض (أ) — الداخلي/الخارجي + السعر — في تذكرة فيتشر مستقلة بعقد نطاق، ونصلّح (ب) بس في التذكرة الحالية؟ نعم. (أ) بيحقّق ٥/٥ معايير الفيتشر وبيمسّ الفلوس. خلطه مع (ب) = التذكرة تفضل مفتوحة أسابيع وإصلاح بسيط يتعطّل بلا داعي.
٢ الزيارات القديمة اللي اتحطلها قسم عشوائي عشان الشاشة فرضته — نسيبها ولا نصفّيها؟ نسيبها. على moonui3 مفيش بيانات أصلاً (0 صف). على تثبيت عميل حقيقي = قرار منفصل، وإصلاحها محتاج معرفة أنهي قسم كان «حقيقي» — غالباً غير قابل للاسترجاع. الأهم إننا نوقف النزيف الجديد.
٣ [FIN] اكتشاف جانبي: لما تحليل يتبعت بره بعد الدفع، تكلفة المعمل بتتسجّل والسعر المدفوع مابيتغيّرش → الهامش الحقيقي مش متعكس. نتعامل معاه؟ يتسجّل ويتعالج داخل تذكرة الفيتشر (أ) — هو نفس الجذر (مفيش مفهوم سعر خارجي). مش هيتصلح لوحده هنا.
٤ clinic.lab_mode / rad_modeتحكّم مبني ومقطوع (المدير يغيّره ومحصلش حاجة). نوصّله ولا نخفيه؟ يتسجّل كدَيْن، ويُحسم مع الفيتشر (أ) — لأنه نفس المنطقة (مصدر كتالوج التحاليل + مسار التنفيذ). خارج نطاق إصلاح (ب).