العميل بلّغ عن مشكلتين في نفس الشاشة. التحقيق أثبت إنهما مشكلتان مختلفتان جذرياً — واحدة عيب حقيقي في كود موجود، والتانية قدرة غير مبنية من الأساس.
| المتوقع | إضافة تحليل أو أشعة من غير ما النظام يسأل عن قسم — القسم مفهوم خاص بالأطباء والكشف. |
|---|---|
| الفعلي | لما الزيارة تبقى تحاليل/أشعة فقط (من غير كشف)، بيظهر حقل «قسم الزيارة» إجباري وبيمنع الحفظ لحد ما تختار قسم. |
| إعادة الإنتاج | ١) افتح /app/clinic/booking · ٢) اختر مريض · ٣) ماتضفش أي كشف · ٤) ضيف تحليل (أو أشعة) بس · ٥) → يظهر «قسم الزيارة» بعلامة إجباري وزر الحفظ متعطّل. |
| ملاحظة مهمة | لو ضفت كشف + تحليل مع بعض، الحقل بيختفي خالص — لأن القسم بييجي من سطر الكشف. فالمشكلة محصورة في سيناريو «تحاليل/أشعة بس». |
| الخطورة | متوسطة — احتكاك تشغيلي يومي + بيانات قسم مغلوطة على الزيارة (بتأثر على تقرير «الإيراد حسب القسم»). مفيش خسارة مالية مباشرة. |
| المطلوب | عند إضافة تحليل في الزيارة: أختار هيتعمل عندنا (يظهر في الـ LIS بتاعنا ويدخل دورة العمل) ولا بره (مايظهرش عندنا) — وبسعر مختلف. |
|---|---|
| الفعلي | مفيش أي اختيار. أي تحليل يتضاف من الحجز بيتولّد حتمياً كطلب في الـ LIS. والقرار «داخلي/خارجي» متحدّد مسبقاً في كتالوج التحليل نفسه (is_outsourced) — مش قرار وقت الطلب. |
| الخطورة | ليست باج — قدرة غير موجودة. مفيش عمود يستقبل الاختيار حتى لو الواجهة بعتته، ومفيش مفهوم «سعر بيع مختلف للخارجي» في النظام كله. |
| # | المحطة | اللي اتشاف |
|---|---|---|
| ١ | 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. القيد حقيقي، مش نظري. |
| # | المحطة | اللي اتشاف |
|---|---|---|
| ١ | 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 على سطر الطلب — يعني مفيش مكان أصلاً تخزّن فيه اختيار المستخدم. |
NOT NULL على افتراض إن «كل حجز بينتمي لقسم» — والافتراض ده بيتكسر في زيارة تحاليل/أشعة بس، فالقيد بيرتدّ لفوق ويطلع للمستخدم كحقل قسم إجباري بلا أي معنى إكلينيكي.
الدليل الحاسم إن ده عيب تصميمي مش قاعدة مقصودة:
مطلب العميل فيه جزأين منفصلين، والاتنين غير موجودين — بس لأسباب مختلفة:
| الجزء | الحالة الفعلية | الدليل |
|---|---|---|
| الاختيار اللحظي (عندنا/بره وقت الطلب) | الـ 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 — يعني المريض بيفضل مدفوع عليه السعر الداخلي. |
| العَرَض | الآلية | الدليل الزمني |
|---|---|---|
| (ب) | افتراض خاطئ عند التصميم (مش انحدار). جدول 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. مرفوض تقنياً. |
| WP | النطاق | الريبو / الملفات | الإثبات | أعلام |
|---|---|---|---|---|
| WP1 | migration للأمام: appointments.department_id → nullable. + مراجعة كل قارئ بيفترضه موجود (خصوصاً 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 — تحكّم مبني ومقطوع (المدير يغيّره ومحصلش حاجة). نوصّله ولا نخفيه؟ | يتسجّل كدَيْن، ويُحسم مع الفيتشر (أ) — لأنه نفس المنطقة (مصدر كتالوج التحاليل + مسار التنفيذ). خارج نطاق إصلاح (ب). |