Now I have a comprehensive understanding of the entire codebase. Let me compose the implementation plan.

---

Implementation Plan: NPHIES eClaims Integration for Moon ERP LIS

Overview

This plan details the implementation of five frontend screens that integrate Saudi Arabia's NPHIES (National Platform for Health Information Exchange) eClaims platform into the existing Moon ERP Laboratory Information System. The backend APIs are already built and tested. All UI is gated behind a nphies_enabled feature flag -- when disabled, zero NPHIES code renders. The design follows the existing LIS design language: dark blue (#1a237e) + gold (#ffc107), Arabic-first RTL, standalone components, PrimeNG 21, signals-based state.

Requirements

Architecture Changes

New Files to Create

FilePurpose
`src/app/core/services/nphies.service.ts`Central NPHIES API service (config, eligibility, preauth, claim, transactions, poll, cancel)
`src/app/core/models/nphies.model.ts`TypeScript interfaces for all NPHIES entities
`src/app/features/lis/nphies-settings/nphies-settings.component.ts`Settings page (connection + code mapping)
`src/app/features/lis/nphies-settings/nphies-settings.component.html`Settings template
`src/app/features/lis/nphies-settings/nphies-settings.component.scss`Settings styles
`src/app/features/lis/nphies-log/nphies-log.component.ts`Transaction log page
`src/app/features/lis/nphies-log/nphies-log.component.html`Log template
`src/app/features/lis/nphies-log/nphies-log.component.scss`Log styles

Existing Files to Modify

FileChange
`src/app/core/models/lis-patient.model.ts`Add `national_id_type`, `passport_country` fields
`src/app/core/models/lis-invoice.model.ts`Add NPHIES claim fields (`nphies_claim_status`, `nphies_preauth_ref`, `nphies_covered_amount`, `nphies_copay_amount`)
`src/app/features/lis/lis-standalone.routes.ts`Add routes for `/lab/settings/nphies` and `/lab/nphies-log`
`src/app/features/lis/lis-layout/lis-layout.component.ts`Add NPHIES nav items (conditional on feature flag)
`src/app/features/lis/patients/lis-patients.component.ts`Add Saudi ID type selector, conditional fields
`src/app/features/lis/patients/lis-patients.component.html`Add NPHIES form fields
`src/app/features/lis/request-wizard-v2/request-wizard-v2.component.ts`Add eligibility check when insurance selected
`src/app/features/lis/request-wizard-v2/request-wizard-v2.component.html`Add eligibility banner
`src/app/features/lis/invoices/lis-invoices.component.ts`Add preauth/claim buttons and NPHIES columns
`src/app/features/lis/invoices/lis-invoices.component.html`Add NPHIES table columns and action dialogs
`src/app/features/lis/dashboard/lis-dashboard.component.ts`Add NPHIES KPI card
`src/app/features/lis/dashboard/lis-dashboard.component.html`Add NPHIES stat card
`src/assets/i18n/ar.json`Add all `LIS.NPHIES.*` Arabic translations
`src/assets/i18n/en.json`Add all `LIS.NPHIES.*` English translations

---

Implementation Steps

Phase 0: Foundation (Service + Models + Feature Flag)

Step 0.1: Create NPHIES Models

File: src/app/core/models/nphies.model.ts

Action: Define all TypeScript interfaces for the NPHIES domain.


// Interfaces to define:

export type NphiesIdType = 'national-id' | 'iqama' | 'passport' | 'visa' | 'border-number';

export type NphiesTransactionType = 'eligibility' | 'preauth' | 'claim' | 'cancel' | 'poll';

export type NphiesTransactionStatus = 'sent' | 'success' | 'error' | 'timeout';

export type NphiesClaimStatus = 'pending' | 'authorized' | 'claimed' | 'rejected' | 'paid';

export interface NphiesConfig {
  provider_license: string;
  facility_nphies_id: string;
  sandbox_mode: boolean;
  certificate_path: string | null;
  key_path: string | null;
  nphies_enabled: boolean;
}

export interface NphiesEligibilityRequest {
  patient_id: number;
  insurer_code?: string;
  service_date?: string;
}

export interface NphiesEligibilityResponse {
  eligible: boolean;
  payer_name: string | null;
  plan: string | null;
  network: string | null;
  member_id: string | null;
  coverage_amount: number | null;
  disposition: string | null;
  raw_response?: any;
}

export interface NphiesPreAuthRequest {
  invoice_id: number;
  investigation_ids?: number[];
}

export interface NphiesPreAuthResponse {
  preauth_ref: string;
  status: 'approved' | 'denied' | 'pended';
  valid_from: string | null;
  valid_to: string | null;
  approved_amount: number;
  items: NphiesAdjudicationItem[];
  disposition: string | null;
  raw_response?: any;
}

export interface NphiesClaimRequest {
  invoice_id: number;
  preauth_ref?: string;
}

export interface NphiesClaimResponse {
  claim_id: string;
  status: 'complete' | 'error' | 'partial';
  total_submitted: number;
  total_benefit: number;
  total_copay: number;
  payment_date: string | null;
  items: NphiesAdjudicationItem[];
  disposition: string | null;
  raw_response?: any;
}

export interface NphiesAdjudicationItem {
  sequence: number;
  investigation_name?: string;
  submitted: number;
  eligible: number;
  benefit: number;
  copay: number;
}

export interface NphiesTransaction {
  id: number;
  type: NphiesTransactionType;
  bundle_id: string;
  status: NphiesTransactionStatus;
  patient_id: number | null;
  patient?: { id: number; name: string; name_ar: string; mrn: string };
  lab_request_id: number | null;
  request_json: any;
  response_json: any;
  error_message: string | null;
  created_at: string;
  updated_at: string;
}

Why: Type safety for all NPHIES API interactions. Defined early so all consumers can import.

Dependencies: None.

Risk: Low. Pure type definitions.

Complexity: Low.

---

Step 0.2: Create NPHIES Service

File: src/app/core/services/nphies.service.ts

Action: Create the central NphiesService following the existing service pattern.

Methods:

The service also exposes a shared nphiesEnabled signal that is loaded once and cached:


readonly nphiesEnabled = signal(false);
private configLoaded = false;

loadFeatureFlag(): void {
  if (this.configLoaded) return;
  this.getConfig().subscribe({
    next: (res) => {
      this.nphiesEnabled.set(res.data.nphies_enabled);
      this.configLoaded = true;
    },
    error: () => this.nphiesEnabled.set(false),
  });
}

API base: ${environment.apiUrl}/nphies

Why: Single service for all NPHIES API interactions. The shared nphiesEnabled signal is the authoritative source of the feature flag, injectable anywhere.

Dependencies: Step 0.1 (models).

Risk: Low. Standard service pattern identical to 65+ existing services.

Complexity: Low-Medium.

---

Step 0.3: Update Patient Model

File: src/app/core/models/lis-patient.model.ts

Action: Add three fields to LisPatient and CreateLisPatient:

On LisPatient:


national_id_type: NphiesIdType | null;   // new
passport_country: string | null;          // new

On CreateLisPatient:


national_id_type?: NphiesIdType;   // new
passport_country?: string;         // new

Why: Backend already has these columns (from task #1462). Frontend model must match.

Dependencies: Step 0.1.

Risk: Low. Adding optional fields is backward-compatible.

---

Step 0.4: Update Invoice Model

File: src/app/core/models/lis-invoice.model.ts

Action: Add NPHIES fields to LisInvoice:


nphies_claim_status: NphiesClaimStatus | null;
nphies_preauth_ref: string | null;
nphies_covered_amount: string | null;
nphies_copay_amount: string | null;

Why: Backend already has these columns (from task #1466). Frontend must render them.

Dependencies: Step 0.1.

Risk: Low.

---

Step 0.5: Initialize Feature Flag in LIS Layout

File: src/app/features/lis/lis-layout/lis-layout.component.ts

Action: Inject NphiesService and call loadFeatureFlag() in ngOnInit(). This makes nphiesService.nphiesEnabled() available to all child components via injection.

Add two conditional nav items:

1. In the fin (Financial) nav group: add { label: 'NAV.LIS_NPHIES_LOG', icon: 'pi pi-history', route: '/lab/nphies-log' } -- conditionally pushed only when nphiesService.nphiesEnabled() is true.

2. In a new settings nav group (or existing if one exists): add { label: 'NAV.LIS_NPHIES_SETTINGS', icon: 'pi pi-sliders-h', route: '/lab/settings/nphies' }.

The nav group array should be converted to a computed signal so it reacts to the feature flag:


private nphiesService = inject(NphiesService);

navGroups = computed<NavGroup[]>(() => {
  const base = [...this.baseNavGroups];
  if (this.nphiesService.nphiesEnabled()) {
    // Inject NPHIES items into Financial group
    const fin = base.find(g => g.id === 'fin');
    if (fin) {
      fin.items = [...fin.items, { label: 'NAV.LIS_NPHIES_LOG', icon: 'pi pi-history', route: '/lab/nphies-log' }];
    }
    // Add settings item
    base.push({
      id: 'nphies',
      label: 'LIS.NAV.NPHIES',
      icon: 'pi pi-shield',
      items: [
        { label: 'NAV.LIS_NPHIES_SETTINGS', icon: 'pi pi-sliders-h', route: '/lab/settings/nphies' },
        { label: 'NAV.LIS_NPHIES_LOG', icon: 'pi pi-history', route: '/lab/nphies-log' },
      ],
    });
  }
  return base;
});

Why: Central feature flag loading. All child components can inject NphiesService and check nphiesEnabled() without additional API calls.

Dependencies: Step 0.2.

Risk: Medium. Must not break existing navigation. The nav groups array is currently a static property; converting to a computed signal requires updating the template to call navGroups() instead of navGroups.

Complexity: Medium.

---

Step 0.6: Add Routes

File: src/app/features/lis/lis-standalone.routes.ts

Action: Add two new route entries in the children array:


{
  path: 'settings/nphies',
  loadComponent: () =>
    import('./nphies-settings/nphies-settings.component').then(
      (m) => m.NphiesSettingsComponent
    ),
},
{
  path: 'nphies-log',
  loadComponent: () =>
    import('./nphies-log/nphies-log.component').then(
      (m) => m.NphiesLogComponent
    ),
},

Also update the breadcrumb map in lis-layout.component.ts to include:


'settings': 'LIS.NAV.SETTINGS',
'nphies': 'LIS.NPHIES.TITLE',
'nphies-log': 'NAV.LIS_NPHIES_LOG',

Why: Lazy-loaded routes following existing pattern.

Dependencies: None (components do not need to exist yet for route definition).

Risk: Low.

---

Step 0.7: Add Translation Keys

Files: src/assets/i18n/ar.json, src/assets/i18n/en.json

Action: Add all translation keys needed across the 5 screens. The complete list:


// Arabic (ar.json) — inside "LIS" object
"NPHIES": {
  "TITLE": "NPHIES التأمين الصحي",
  "SETTINGS": "إعدادات NPHIES",
  "TRANSACTION_LOG": "سجل المعاملات",
  "ENABLED": "NPHIES مفعّل",
  "DISABLED": "NPHIES معطّل",

  "CONFIG": {
    "PROVIDER_LICENSE": "ترخيص مقدم الخدمة",
    "FACILITY_ID": "معرّف المنشأة في NPHIES",
    "SANDBOX_MODE": "الوضع التجريبي (Sandbox)",
    "SANDBOX_HINT": "تفعيل الاتصال بالبيئة التجريبية بدلاً من الإنتاجية",
    "CERTIFICATE": "شهادة الأمان (PEM)",
    "PRIVATE_KEY": "المفتاح الخاص (PEM)",
    "UPLOAD_CERT": "رفع الشهادة",
    "UPLOAD_KEY": "رفع المفتاح",
    "TEST_CONNECTION": "اختبار الاتصال",
    "TESTING": "جارٍ اختبار الاتصال...",
    "CONNECTION_OK": "الاتصال ناجح",
    "CONNECTION_FAIL": "فشل الاتصال",
    "SAVE_CONFIG": "حفظ الإعدادات",
    "CERT_UPLOADED": "تم رفع الشهادة بنجاح",
    "KEY_UPLOADED": "تم رفع المفتاح بنجاح"
  },

  "CODE_MAPPING": {
    "TITLE": "ربط أكواد SBSCS",
    "INVESTIGATION": "الفحص",
    "SBSCS_CODE": "كود SBSCS",
    "LOINC_CODE": "كود LOINC",
    "NO_CODE": "غير مربوط",
    "SAVE": "حفظ الأكواد",
    "UNMAPPED_COUNT": "{{count}} فحص بدون كود"
  },

  "ELIGIBILITY": {
    "CHECKING": "جارٍ التحقق من التغطية التأمينية...",
    "ELIGIBLE": "مغطّى",
    "NOT_ELIGIBLE": "غير مغطّى",
    "PAYER": "شركة التأمين",
    "PLAN": "الخطة",
    "NETWORK": "الشبكة",
    "MEMBER_ID": "رقم العضوية",
    "COVERAGE_AMOUNT": "مبلغ التغطية",
    "OVERRIDE": "متابعة بدون NPHIES",
    "RETRY": "إعادة التحقق",
    "ERROR": "خطأ في التحقق من التغطية",
    "TIMEOUT": "انتهت مهلة الاتصال بـ NPHIES"
  },

  "PREAUTH": {
    "REQUEST": "طلب موافقة مسبقة",
    "REQUESTING": "جارٍ إرسال طلب الموافقة...",
    "APPROVED": "معتمد",
    "DENIED": "مرفوض",
    "PENDED": "قيد المراجعة",
    "REF": "رقم الموافقة",
    "VALID_FROM": "صالح من",
    "VALID_TO": "صالح حتى",
    "APPROVED_AMOUNT": "المبلغ المعتمد",
    "SELECT_TESTS": "اختر الفحوصات لطلب الموافقة"
  },

  "CLAIM": {
    "SUBMIT": "إرسال مطالبة",
    "SUBMITTING": "جارٍ إرسال المطالبة...",
    "SUBMITTED": "تم إرسال المطالبة",
    "COVERED": "المبلغ المغطّى",
    "COPAY": "حصة المريض",
    "TOTAL_SUBMITTED": "المبلغ المقدّم",
    "TOTAL_BENEFIT": "المبلغ المعتمد",
    "PAYMENT_DATE": "تاريخ الدفع المتوقع",
    "VIEW_FHIR": "عرض FHIR Bundle"
  },

  "STATUS": {
    "PENDING": "معلّق",
    "AUTHORIZED": "معتمد",
    "CLAIMED": "تم المطالبة",
    "REJECTED": "مرفوض",
    "PAID": "مدفوع",
    "SENT": "مرسل",
    "SUCCESS": "ناجح",
    "ERROR": "خطأ",
    "TIMEOUT": "انتهت المهلة"
  },

  "LOG": {
    "TITLE": "سجل معاملات NPHIES",
    "TYPE": "نوع المعاملة",
    "BUNDLE_ID": "معرّف الحزمة",
    "DATE": "التاريخ",
    "PATIENT": "المريض",
    "REQUEST": "الطلب",
    "STATUS": "الحالة",
    "VIEW_JSON": "عرض JSON",
    "REQUEST_JSON": "طلب FHIR",
    "RESPONSE_JSON": "استجابة FHIR",
    "NO_TRANSACTIONS": "لا توجد معاملات",
    "FILTER_TYPE": "تصفية حسب النوع",
    "FILTER_STATUS": "تصفية حسب الحالة",
    "FILTER_DATE": "تصفية حسب التاريخ",
    "EXPORT": "تصدير"
  },

  "DASHBOARD": {
    "TITLE": "NPHIES اليوم",
    "ELIGIBILITY_COUNT": "تحقق تأمين",
    "PREAUTH_COUNT": "موافقة مسبقة",
    "CLAIM_COUNT": "مطالبة مرسلة",
    "COVERED_AMOUNT": "مبلغ مغطّى"
  },

  "PATIENT": {
    "ID_TYPE": "نوع الهوية",
    "NATIONAL_ID": "الهوية الوطنية",
    "IQAMA": "الإقامة",
    "PASSPORT": "جواز السفر",
    "VISA": "تأشيرة",
    "BORDER_NUMBER": "رقم حدود",
    "ID_NUMBER": "رقم الهوية",
    "PASSPORT_COUNTRY": "بلد جواز السفر",
    "ID_REQUIRED": "رقم الهوية مطلوب عند تفعيل NPHIES",
    "NATIONAL_ID_HINT": "10 أرقام، يبدأ بـ 1",
    "IQAMA_HINT": "10 أرقام، يبدأ بـ 2"
  }
}

English translations follow the same key structure with English values.

Why: All text must be translatable. Arabic is the primary language.

Dependencies: None.

Risk: Low.

---

Phase 1: Screen 1 -- NPHIES Settings Page

Route: /lab/settings/nphies

File: src/app/features/lis/nphies-settings/nphies-settings.component.ts

Task: #1468

1.1 User Story

Who: Lab administrator / IT manager.

When: During initial NPHIES setup, or when changing connection details, or when mapping SBSCS codes to investigations.

Why: To configure the connection to Saudi Arabia's NPHIES eClaims platform, upload security certificates, verify connectivity, and map billing codes.

1.2 UI Layout


┌─────────────────────────────────────────────────────────────────────────┐
│ PageHeader: "إعدادات NPHIES" [حفظ الإعدادات]                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ┌─── Tab 1: الاتصال ──────────────────────────────────────────────────┐ │
│ │                                                                      │ │
│ │ ┌─ Connection Card ──────────────────────────────────────────────┐   │ │
│ │ │  Provider License:  [________________]                         │   │ │
│ │ │  Facility NPHIES ID: [________________]                        │   │ │
│ │ │  Sandbox Mode:      [toggle switch]  "البيئة التجريبية"       │   │ │
│ │ └────────────────────────────────────────────────────────────────┘   │ │
│ │                                                                      │ │
│ │ ┌─ Certificate Card ─────────────────────────────────────────────┐   │ │
│ │ │  Certificate (PEM):  [cert.pem ✓]  [رفع شهادة جديدة]         │   │ │
│ │ │  Private Key (PEM):  [key.pem ✓]   [رفع مفتاح جديد]          │   │ │
│ │ └────────────────────────────────────────────────────────────────┘   │ │
│ │                                                                      │ │
│ │ ┌─ Test Connection Card ─────────────────────────────────────────┐   │ │
│ │ │  [🔗 اختبار الاتصال]                                          │   │ │
│ │ │                                                                │   │ │
│ │ │  ✓ الاتصال ناجح — Sandbox (2026-04-02 14:23)                  │   │ │
│ │ │  OR                                                            │   │ │
│ │ │  ✗ فشل الاتصال: Certificate expired                           │   │ │
│ │ └────────────────────────────────────────────────────────────────┘   │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│ ┌─── Tab 2: ربط الأكواد ──────────────────────────────────────────────┐ │
│ │                                                                      │ │
│ │ ┌─ Summary Banner ───────────────────────────────────────────────┐   │ │
│ │ │  ⚠ 15 فحص بدون كود SBSCS من أصل 120                          │   │ │
│ │ └────────────────────────────────────────────────────────────────┘   │ │
│ │                                                                      │ │
│ │ ┌─ Search ───────────────────────────────────────────────────────┐   │ │
│ │ │  [🔍 البحث...] [تصفية: الكل | بدون كود | مربوط]              │   │ │
│ │ └────────────────────────────────────────────────────────────────┘   │ │
│ │                                                                      │ │
│ │ ┌─ Table ────────────────────────────────────────────────────────┐   │ │
│ │ │  الفحص      │ الكود   │ SBSCS           │ LOINC     │ الحالة │   │ │
│ │ │─────────────┼─────────┼─────────────────┼───────────┼────────│   │ │
│ │ │  CBC        │ HEM001  │ [920010101    ] │ 57021-8   │ ✓      │   │ │
│ │ │  TSH        │ HOR005  │ [___________  ] │ 11580-8   │ ⚠      │   │ │
│ │ │  ...        │         │                 │           │        │   │ │
│ │ └────────────────────────────────────────────────────────────────┘   │ │
│ │                                                                      │ │
│ │ [حفظ الأكواد]                                                        │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘

1.3 UX Flow

1. Page loads. Skeleton placeholder shown (400px height, matching existing pattern).

2. Parallel API calls: GET /api/nphies/config and investigation listAll. Both complete before rendering.

3. Tab 1 -- Connection: Form fields pre-populated from config. Sandbox toggle with gold-on-dark-blue styling. Certificate upload uses native file input styled as PrimeNG button. Upload immediately calls POST /api/nphies/config/upload-certificate with FormData. On success: green check icon next to filename. On error: red cross with error detail.

4. Test Connection button: Calls POST /api/nphies/config/test-connection. While loading, button shows spinner and text "جارٍ اختبار الاتصال...". On success: green banner with checkmark, timestamp, and mode (Sandbox/Production). On error: red banner with error message and retry suggestion. The result persists on screen until another test.

5. Save Config: Calls PUT /api/nphies/config. Standard toast notification on success/error.

6. Tab 2 -- Code Mapping: Table loads all investigations. SBSCS column has inline-editable p-inputText (click to focus, Tab to next row). Filter dropdown: All / Unmapped / Mapped. Search filters by investigation name or code. Unmapped rows show warning icon in Status column. Bulk save calls the investigations update endpoint for each changed row (parallel, batched in groups of 10).

7. Unmapped counter: Orange banner at top: "15 فحص بدون كود SBSCS من أصل 120". Updates in real-time as user fills in codes.

1.4 Visual Design

1.5 Component Architecture


@Component({
  selector: 'app-nphies-settings',
  standalone: true,
  imports: [ /* PrimeNG modules, TranslateModule, etc. */ ],
  providers: [MessageService],
})
export class NphiesSettingsComponent implements OnInit {
  private nphiesService = inject(NphiesService);
  private investigationService = inject(LisInvestigationService);
  private messageService = inject(MessageService);
  lang = inject(LanguageService);

  // State signals
  loading = signal(true);
  saving = signal(false);
  testingConnection = signal(false);
  connectionResult = signal<{ success: boolean; message: string; timestamp: string } | null>(null);
  uploadingCert = signal(false);
  uploadingKey = signal(false);
  activeTab = signal(0);

  // Config form
  config = signal<NphiesConfig | null>(null);
  providerLicense = signal('');
  facilityId = signal('');
  sandboxMode = signal(true);
  certUploaded = signal(false);
  keyUploaded = signal(false);

  // Code mapping
  investigations = signal<LisInvestigation[]>([]);
  codeChanges = signal<Map<number, string>>(new Map());
  codeFilter = signal<'all' | 'unmapped' | 'mapped'>('all');
  codeSearch = signal('');
  savingCodes = signal(false);

  // Computed
  unmappedCount = computed(() => /* ... */);
  filteredInvestigations = computed(() => /* ... */);
  hasUnsavedChanges = computed(() => this.codeChanges().size > 0);
}

1.6 RTL/Arabic Considerations

1.7 Responsive Behavior

1.8 Accessibility

1.9 Edge Cases

---

Phase 2: Screen 2 -- Patient Saudi ID Fields

File: src/app/features/lis/patients/lis-patients.component.ts + .html

Task: #1469

2.1 User Story

Who: Lab receptionist registering a new patient.

When: During patient creation or edit, when NPHIES is enabled.

Why: NPHIES requires a Saudi identifier (National ID, Iqama, Passport, or Visa number) for every patient to check insurance eligibility.

2.2 UI Layout

The existing patient form dialog currently has fields: name_ar, name, date_of_birth, gender, national_id, blood_group, phone, email, address, address_ar, medical_history, insurance_contract_id.

New fields inserted after national_id (wrapped in @if (nphiesEnabled())):


┌─── Existing Patient Dialog ──────────────────────────────────────┐
│                                                                    │
│  الاسم عربي: [________]   الاسم انجليزي: [________]              │
│  تاريخ الميلاد: [____]    الجنس: [ذكر ▾]                         │
│                                                                    │
│  ┌─── NPHIES Section (only when enabled) ─────────────────────┐   │
│  │  نوع الهوية: [الهوية الوطنية ▾]                             │   │
│  │  رقم الهوية: [1234567890_____]  "10 أرقام، يبدأ بـ 1"      │   │
│  │                                                              │   │
│  │  (if passport selected):                                     │   │
│  │  بلد جواز السفر: [المملكة المتحدة ▾]                        │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                    │
│  فصيلة الدم: [____]  الهاتف: [________]                          │
│  ...                                                               │
└────────────────────────────────────────────────────────────────────┘

Badge in patient table (new column after national_id, only when NPHIES enabled):


│ ... │ نوع الهوية │ رقم الهوية   │ ...
│ ... │ 🇸🇦 وطنية  │ 1234567890  │ ...
│ ... │ 📋 إقامة   │ 2098765432  │ ...
│ ... │ ✈ جواز     │ AB123456   │ ...

2.3 UX Flow

1. When NPHIES is enabled, the form shows a visually distinct section with a subtle blue-gray background and a shield icon header.

2. ID Type dropdown: 5 options. Defaults to "الهوية الوطنية" (National ID). Changing the type resets the ID number field and updates validation.

3. ID Number input: Real-time validation as user types.

4. Validation hint text: Below the input in muted text. Changes based on selected type. Red when invalid, muted when neutral.

5. Required when NPHIES enabled: The national_id field becomes Validators.required. If user tries to save without it, the field highlights red with "رقم الهوية مطلوب عند تفعيل NPHIES".

6. Passport country: Only visible when type is passport. PrimeNG Select with country list from src/assets/data/countries.json (already exists in the app for other features).

7. Patient table: New column showing ID type as a colored tag badge. Only renders when NPHIES enabled.

8. Patient chip in wizard: The patient selection chip in the request wizard should show the ID type badge when NPHIES is enabled.

2.4 Visual Design

2.5 Component Architecture

Add to existing LisPatientsComponent:


// New signals
nphiesEnabled = computed(() => this.nphiesService.nphiesEnabled());

idTypeOptions = [
  { label: 'LIS.NPHIES.PATIENT.NATIONAL_ID', value: 'national-id' },
  { label: 'LIS.NPHIES.PATIENT.IQAMA', value: 'iqama' },
  { label: 'LIS.NPHIES.PATIENT.PASSPORT', value: 'passport' },
  { label: 'LIS.NPHIES.PATIENT.VISA', value: 'visa' },
  { label: 'LIS.NPHIES.PATIENT.BORDER_NUMBER', value: 'border-number' },
];

countries = signal<{ label: string; value: string }[]>([]);
showPassportCountry = computed(() => this.form?.get('national_id_type')?.value === 'passport');

Modify initForm() to add:


national_id_type: ['national-id'],
passport_country: [null],

Add a custom validator on the national_id field that reads the national_id_type value:


private idNumberValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const type = this.form?.get('national_id_type')?.value;
    const val = control.value;
    if (!val) return this.nphiesService.nphiesEnabled() ? { required: true } : null;
    switch (type) {
      case 'national-id': return /^1\d{9}$/.test(val) ? null : { pattern: 'NATIONAL_ID_HINT' };
      case 'iqama': return /^2\d{9}$/.test(val) ? null : { pattern: 'IQAMA_HINT' };
      case 'passport': return val.length >= 5 && val.length <= 20 ? null : { minlength: true };
      default: return null;
    }
  };
}

Also modify onSave() to include the new fields in the payload.

2.6 RTL/Arabic Considerations

2.7 Responsive Behavior

2.8 Accessibility

2.9 Edge Cases

---

Phase 3: Screen 3 -- Eligibility Check in Request Wizard Step 3

File: src/app/features/lis/request-wizard-v2/request-wizard-v2.component.ts + .html

Task: #1470

3.1 User Story

Who: Lab receptionist creating a new lab request.

When: In the request wizard, after selecting a patient with insurance and choosing insurance as the billing type.

Why: To verify in real-time that the patient's insurance covers the ordered tests before proceeding, avoiding claim rejections later.

3.2 UI Layout

The eligibility banner inserts into the existing wizard layout, in the billing section (between the insurance contract dropdown and the financial summary):


┌─── Request Wizard V2 ────────────────────────────────────────────────┐
│                                                                        │
│  [Patient Section]  [Test Selection]                                   │
│                                                                        │
│  ── Billing Section ──                                                 │
│  Client Type: [فرد | تأمين ✓ | مختبر خارجي]                          │
│  Insurance Contract: [التعاونية — Gold ▾]                              │
│                                                                        │
│  ┌─── NPHIES Eligibility Banner ───────────────────────────────────┐   │
│  │                                                                  │   │
│  │  STATE A: Checking                                               │   │
│  │  ⟳ جارٍ التحقق من التغطية التأمينية...                         │   │
│  │  ███████░░░ (animated bar)                                       │   │
│  │                                                                  │   │
│  │  STATE B: Eligible                                               │   │
│  │  ✓ مغطّى                                                        │   │
│  │  التعاونية │ Gold-A │ VIP │ العضوية: MEM-001                    │   │
│  │  التغطية: 500,000 ر.س                                          │   │
│  │                                                                  │   │
│  │  STATE C: Not Eligible                                           │   │
│  │  ✗ غير مغطّى                                                    │   │
│  │  "Patient coverage not found or inactive"                        │   │
│  │  [إعادة التحقق] [متابعة بدون NPHIES]                           │   │
│  │                                                                  │   │
│  │  STATE D: Error / Timeout                                        │   │
│  │  ⚠ خطأ في التحقق — انتهت مهلة الاتصال                          │   │
│  │  [إعادة التحقق] [متابعة بدون NPHIES]                           │   │
│  │                                                                  │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                        │
│  ── Financial Summary ──                                               │
│  ...                                                                   │
└────────────────────────────────────────────────────────────────────────┘

3.3 UX Flow

1. Auto-trigger: When nphiesEnabled() is true AND user selects insurance contract AND patient has a national_id, the eligibility check fires automatically. No manual trigger needed.

2. Trigger conditions:

3. Loading state: Animated shimmer bar with "جارٍ التحقق من التغطية التأمينية..." text. Duration typically 1-3 seconds.

4. Success (Eligible): Green banner. Shows payer name, plan, network, member ID, coverage amount. The banner has a subtle green glow effect. The information persists and scrolls with the form.

5. Success (Not Eligible): Red banner. Shows disposition message from API. Two action buttons: "إعادة التحقق" (retry) and "متابعة بدون NPHIES" (override). Override sets a flag eligibilityOverridden = true that allows the wizard to proceed.

6. Error/Timeout: Amber banner. Shows error message. Same two action buttons. The wizard can still proceed (eligibility is not a hard gate -- it is informational).

7. Override behavior: When user clicks "متابعة بدون NPHIES", the banner collapses to a single-line notice: "⚠ تم تجاوز التحقق من التغطية" in muted amber text. The request proceeds without NPHIES data.

8. Request submission: If eligible, the eligibility response data is stored on the request (backend handles this). The insurer_code from the eligibility response is passed along.

3.4 Visual Design

3.5 Component Architecture

Add to existing RequestWizardV2Component:


// New signals
eligibilityState = signal<'idle' | 'checking' | 'eligible' | 'not-eligible' | 'error'>('idle');
eligibilityData = signal<NphiesEligibilityResponse | null>(null);
eligibilityError = signal<string | null>(null);
eligibilityOverridden = signal(false);
private eligibilityTrigger$ = new Subject<void>();

// New computed
showEligibilityBanner = computed(() => {
  return this.nphiesService.nphiesEnabled()
    && this.selectedInsContractId() !== null
    && this.selectedPatient()?.national_id;
});

In ngOnInit(), set up the debounced trigger:


this.eligibilityTrigger$.pipe(
  debounceTime(500),
  takeUntil(this.destroy$),
).subscribe(() => this.checkEligibility());

Watch for changes to patient and insurance contract that should trigger a check:


// Called when patient or insurance contract changes
private triggerEligibilityIfNeeded(): void {
  if (!this.showEligibilityBanner()) {
    this.eligibilityState.set('idle');
    return;
  }
  this.eligibilityOverridden.set(false);
  this.eligibilityTrigger$.next();
}

private checkEligibility(): void {
  const patient = this.selectedPatient();
  if (!patient?.national_id) return;

  this.eligibilityState.set('checking');
  this.eligibilityData.set(null);
  this.eligibilityError.set(null);

  this.nphiesService.checkEligibility({
    patient_id: patient.id,
    service_date: new Date().toISOString().split('T')[0],
  }).subscribe({
    next: (res) => {
      this.eligibilityData.set(res.data);
      this.eligibilityState.set(res.data.eligible ? 'eligible' : 'not-eligible');
    },
    error: (err) => {
      this.eligibilityError.set(err.error?.message || 'Connection error');
      this.eligibilityState.set('error');
    },
  });
}

overrideEligibility(): void {
  this.eligibilityOverridden.set(true);
  this.eligibilityState.set('idle');
}

retryEligibility(): void {
  this.eligibilityTrigger$.next();
}

3.6 RTL/Arabic Considerations

3.7 Responsive Behavior

3.8 Accessibility

3.9 Edge Cases

---

Phase 4: Screen 4 -- PreAuth + Claim on Invoices

File: src/app/features/lis/invoices/lis-invoices.component.ts + .html

Task: #1471

4.1 User Story

Who: Lab billing clerk / receptionist.

When: After an invoice is created (for insured patients), to request prior authorization before testing, and to submit the claim after results are released.

Why: NPHIES requires prior authorization for certain tests and claim submission after service delivery. This is how the lab gets paid by insurance.

4.2 UI Layout

New columns added to the invoice table (only when NPHIES enabled):


│ Invoice# │ Patient │ Date │ Type │ Status │ Total │ NPHIES Status │ Covered │ Copay │ Actions │
│ INV-001  │ أحمد    │ 04/02 │ تأمين │ مرسل  │ 165   │ 🟡 معلّق      │ -       │ -     │ [⚡][📤][👁]│
│ INV-002  │ فاطمة   │ 04/02 │ تأمين │ مرسل  │ 220   │ 🔵 معتمد PA#  │ -       │ -     │ [📤][👁] │
│ INV-003  │ خالد    │ 04/01 │ تأمين │ مدفوع │ 300   │ 🟢 مطالبة     │ 240     │ 60    │ [👁]    │
│ INV-004  │ سارة    │ 04/01 │ فرد   │ مرسل  │ 100   │ -             │ -       │ -     │ [...]   │

Action buttons per row (conditional):

ButtonIconLabelWhen Visible
PreAuth`pi pi-bolt`طلب موافقةInsurance invoice + no preauth yet
Claim`pi pi-send`إرسال مطالبةHas preauth ref + results released
View FHIR`pi pi-code`عرض FHIRAny invoice with NPHIES transaction

PreAuth Dialog:


┌─── طلب موافقة مسبقة — فاتورة INV-002 ──────────────────────────────┐
│                                                                        │
│  المريض: فاطمة حسن إبراهيم    │    التأمين: بوبا العربية             │
│                                                                        │
│  ┌─ الفحوصات ──────────────────────────────────────────────────────┐   │
│  │  ☑ CBC (تعداد الدم)          │ 65.000 ر.س  │ 920010101          │   │
│  │  ☑ TSH (هرمون الغدة)         │ 85.000 ر.س  │ 920030201          │   │
│  │  ☑ Lipid Panel               │ 70.000 ر.س  │ 920020101          │   │
│  │──────────────────────────────────────────────────────────────────│   │
│  │  المجموع: 220.000 ر.س                                          │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                        │
│  [إلغاء]                                          [طلب الموافقة المسبقة]│
│                                                                        │
│  ── After Response ──                                                  │
│  ┌─ نتيجة الموافقة ────────────────────────────────────────────────┐   │
│  │  ✓ معتمد                                                        │   │
│  │  رقم الموافقة: PA-1712045600000                                 │   │
│  │  صالح حتى: 2026-05-02                                          │   │
│  │  المبلغ المعتمد: 220.000 ر.س                                   │   │
│  │                                                                  │   │
│  │  الفحص          │ المقدّم    │ المعتمد                           │   │
│  │  CBC             │ 65 ر.س   │ 65 ر.س ✓                         │   │
│  │  TSH             │ 85 ر.س   │ 85 ر.س ✓                         │   │
│  │  Lipid Panel     │ 70 ر.س   │ 70 ر.س ✓                         │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└────────────────────────────────────────────────────────────────────────┘

Claim Dialog (similar structure, with adjudication breakdown showing submitted / eligible / benefit / copay per item).

FHIR Viewer Dialog:


┌─── عرض FHIR Bundle ─────────────────────────────────────────────────┐
│                                                                        │
│  [طلب FHIR]  [استجابة FHIR]    (two tabs)                            │
│                                                                        │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │ {                                                                │   │
│  │   "resourceType": "Bundle",                                      │   │
│  │   "id": "550e8400-e29b-41d4-a716-...",                          │   │
│  │   "type": "message",                                             │   │
│  │   "entry": [                                                     │   │
│  │     {                                                            │   │
│  │       "resource": {                                              │   │
│  │         "resourceType": "MessageHeader",                         │   │
│  │         ...                                                      │   │
│  │ (with syntax highlighting: keys=blue, strings=green,             │   │
│  │  numbers=purple, braces=grey)                                    │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                        │
│  [نسخ JSON]                                           [إغلاق]         │
└────────────────────────────────────────────────────────────────────────┘

4.3 UX Flow

PreAuth Flow:

1. User clicks "طلب موافقة" button on an insurance invoice row.

2. Dialog opens showing invoice details and test list. All tests pre-selected (checkboxes).

3. User can deselect tests (optional -- rare use case).

4. User clicks "طلب الموافقة المسبقة". Button shows spinner.

5. API call: POST /api/nphies/preauth { invoice_id, investigation_ids }.

6. Response replaces the test list with the approval result.

7. If approved: Green success section with ref number and per-item breakdown. Dialog stays open for review. "إغلاق" button dismisses.

8. If denied: Red section with rejection reason. Dialog stays open.

9. On close: Table refreshes. The NPHIES Status column updates.

Claim Flow:

1. User clicks "إرسال مطالبة" on an invoice that has preauth approval and released results.

2. Dialog opens showing the preauth ref, approved amount, and test list.

3. User clicks "إرسال المطالبة". Button shows spinner.

4. API call: POST /api/nphies/claim { invoice_id, preauth_ref }.

5. Response shows adjudication: per-item submitted/covered/copay breakdown.

6. Invoice table updates with new amounts in "Covered" and "Copay" columns.

FHIR Viewer:

1. User clicks "عرض FHIR" on any invoice with NPHIES transactions.

2. Dialog shows two tabs: Request JSON and Response JSON.

3. JSON is pretty-printed with syntax highlighting (CSS-based, not a library).

4. "نسخ JSON" copies to clipboard with toast confirmation.

4.4 Visual Design

4.5 Component Architecture

Add to existing LisInvoicesComponent:


// New signals
nphiesEnabled = computed(() => this.nphiesService.nphiesEnabled());

// PreAuth dialog
preAuthDialogVisible = signal(false);
preAuthInvoice = signal<LisInvoice | null>(null);
preAuthInvestigations = signal<any[]>([]);
preAuthSelectedIds = signal<Set<number>>(new Set());
preAuthLoading = signal(false);
preAuthResult = signal<NphiesPreAuthResponse | null>(null);

// Claim dialog
claimDialogVisible = signal(false);
claimInvoice = signal<LisInvoice | null>(null);
claimLoading = signal(false);
claimResult = signal<NphiesClaimResponse | null>(null);

// FHIR viewer
fhirDialogVisible = signal(false);
fhirRequest = signal<any>(null);
fhirResponse = signal<any>(null);
fhirActiveTab = signal(0);

// Methods
openPreAuth(invoice: LisInvoice): void { /* ... */ }
submitPreAuth(): void { /* ... */ }
openClaim(invoice: LisInvoice): void { /* ... */ }
submitClaim(): void { /* ... */ }
openFhirViewer(invoice: LisInvoice): void { /* ... */ }
copyFhirJson(): void { /* ... */ }

// Helpers
canRequestPreAuth(invoice: LisInvoice): boolean {
  return this.hasInsurance(invoice) && !invoice.nphies_preauth_ref && this.nphiesEnabled();
}
canSubmitClaim(invoice: LisInvoice): boolean {
  return !!invoice.nphies_preauth_ref && !invoice.nphies_claim_status?.includes('claimed')
    && this.nphiesEnabled();
}
hasFhirData(invoice: LisInvoice): boolean {
  return !!invoice.nphies_preauth_ref || !!invoice.nphies_claim_status;
}

For JSON syntax highlighting, create a pure pipe:


@Pipe({ name: 'jsonHighlight', standalone: true })
export class JsonHighlightPipe implements PipeTransform {
  transform(value: any): string {
    // Returns HTML string with <span class="json-*"> wrappers
  }
}

4.6 RTL/Arabic Considerations

4.7 Responsive Behavior

4.8 Accessibility