Now I have a comprehensive understanding of the entire codebase. Let me compose the implementation plan.
---
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.
nphies_enabled controls all NPHIES UI visibility (fetched from GET /api/nphies/config)LIS.NPHIES.* namespace| File | Purpose |
| `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 |
| File | Change |
| `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 |
---
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.
---
File: src/app/core/services/nphies.service.ts
Action: Create the central NphiesService following the existing service pattern.
Methods:
getConfig(): Observable> updateConfig(data: Partial): Observable> testConnection(): Observable> uploadCertificate(file: File, type: 'cert' | 'key'): Observable> checkEligibility(data: NphiesEligibilityRequest): Observable> requestPreAuth(data: NphiesPreAuthRequest): Observable> submitClaim(data: NphiesClaimRequest): Observable> cancelTransaction(transactionId: number): Observable> pollPending(): Observable> getTransactions(page: number, perPage: number, filters?: Record): Observable> getTransaction(id: number): Observable> 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.
---
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.
---
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.
---
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.
---
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.
---
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.
---
Route: /lab/settings/nphies
File: src/app/features/lis/nphies-settings/nphies-settings.component.ts
Task: #1468
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.
┌─────────────────────────────────────────────────────────────────────────┐
│ 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. 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.
#f8fafc (matching LIS pages)border-radius: 16px, border: 1px solid rgba(0,0,0,0.05), box-shadow: 0 1px 3px rgba(0,0,0,0.04)font-size: 0.9rem, font-weight: 700, color: #334155, icon in gold (#ffc107) inset-inline-start of textTabView with two tabs, gold underline for active tabToggleSwitch. When ON, show amber badge "تجريبي" next to itp-message component. severity="success" for OK, severity="error" for fail. Includes timestamp.p-table with scrollable, scrollHeight="450px". SBSCS input: width: 140px, monospace font, text-align: center. Status column: green checkmark (pi pi-check-circle, color: #10b981) or amber warning (pi pi-exclamation-triangle, color: #f59e0b).background: #fef3c7, border-inline-start: 4px solid #f59e0b, border-radius: 8px, padding: 12px 16px
@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);
}
direction: ltr; text-align: center on the inputdir="ltr" on filename display elements with for attributearia-label="Upload PEM certificate file"aria-live="polite" region---
File: src/app/features/lis/patients/lis-patients.component.ts + .html
Task: #1469
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.
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 │ ...
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.
1. Regex: /^1\d{9}$/2. Regex: /^2\d{9}$/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.
background: #f0f4ff (very light blue), border-radius: 12px, padding: 16px, border: 1px solid #e0e7ff, margin: 8px 0font-size: 0.8rem, font-weight: 600, color: #475569, icon: pi pi-shield in color: #1a237ebackground: #dbeafe, color: #1d4ed8, text: "وطنية"background: #d1fae5, color: #047857, text: "إقامة"background: #fef3c7, color: #b45309, text: "جواز"background: #f3e8ff, color: #7c3aed, text: "تأشيرة"background: #fee2e2, color: #b91c1c, text: "حدود"font-size: 0.7rem, margin-top: 4px, normal: color: #94a3b8, error: color: #ef4444font-family: 'JetBrains Mono', monospace (fallback: monospace), direction: ltr, text-align: start, letter-spacing: 1pxAdd 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.
aria-labelaria-describedbyaria-label for screen readers (e.g., "نوع الهوية: الهوية الوطنية")null. When NPHIES enabled and editing, prompt to fill it---
File: src/app/features/lis/request-wizard-v2/request-wizard-v2.component.ts + .html
Task: #1470
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.
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 ── │
│ ... │
└────────────────────────────────────────────────────────────────────────┘
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.
border-radius: 12px, padding: 16px 20px, transition: all 0.3s easebackground: #f0f4ff, border: 1px solid #bfdbfe. Shimmer animation on a progress bar (CSS keyframes, width: 60%, animated left-right).background: linear-gradient(135deg, #ecfdf5, #d1fae5), border: 1px solid #6ee7b7. Checkmark icon: color: #059669, font-size: 1.5rem. Payer/plan/network shown as inline pill badges with background: white, border-radius: 20px, padding: 4px 12px, font-size: 0.75rem.background: linear-gradient(135deg, #fef2f2, #fee2e2), border: 1px solid #fca5a5. X icon: color: #dc2626.background: #fffbeb, border: 1px solid #fde68a. Warning icon: color: #f59e0b.background: transparent, color: #92400e, font-size: 0.75rem, font-style: italicmax-height transition or Angular @trigger animation)font-weight: 800, color: #047857, font-size: 1.1remAdd 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();
}
dir="ltr" spanrole="status" and aria-live="polite" so screen readers announce changesaria-busy="true"switchMap pattern -- cancel previous on new trigger)---
File: src/app/features/lis/invoices/lis-invoices.component.ts + .html
Task: #1471
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.
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):
| Button | Icon | Label | When Visible |
| PreAuth | `pi pi-bolt` | طلب موافقة | Insurance invoice + no preauth yet |
| Claim | `pi pi-send` | إرسال مطالبة | Has preauth ref + results released |
| View FHIR | `pi pi-code` | عرض FHIR | Any 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] [إغلاق] │
└────────────────────────────────────────────────────────────────────────┘
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.
p-tag):pending (معلّق): severity="warn", icon pi pi-clockauthorized (معتمد): severity="info", icon pi pi-check, shows truncated PA refclaimed (تم المطالبة): severity="success", icon pi pi-sendrejected (مرفوض): severity="danger", icon pi pi-timespaid (مدفوع): severity="success", icon pi pi-check-circlenull (no NPHIES): Shows dash "-"font-weight: 600, text-align: center. Covered in green, Copay in amber. Format: 132.000 (3 decimal places for SAR).width: 700px, maximizable: true. Test list uses p-table with checkboxes. Approval result section has left border: 4px solid #10b981 for approved, 4px solid #ef4444 for denied.font-family: 'JetBrains Mono', 'Fira Code', monospace, font-size: 0.75rem, line-height: 1.5, background: #1e1e2e (dark), color: #cdd6f4. Scrollable container with max-height: 500px. Syntax highlighting via CSS classes: .json-key { color: #89b4fa }, .json-string { color: #a6e3a1 }, .json-number { color: #cba6f7 }, .json-boolean { color: #f38ba8 }.p-button with text and rounded styles. Colors: PreAuth = blue, Claim = gold (#ffc107), FHIR = gray.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
}
}
direction: ltr) since JSON is Englisharia-labelledby with headers
- Copy button provides toast feedback (not just clipboard change)
- Color-coded badges also have text labels -- never rely on color alone
4.9 Edge Cases
- Invoice has no insurance: PreAuth and Claim buttons hidden. NPHIES columns show dash.
- PreAuth already submitted: Button disabled with tooltip "تم طلب الموافقة المسبقة مسبقاً"
- Claim submitted before results released: Backend returns 422. Show error: "يجب تحرير النتائج قبل إرسال المطالبة"
- PreAuth denied, want to retry: Show "إعادة الطلب" button that clears previous result and re-submits
- Network timeout during submission: Show error with retry option. Invoice table shows "pending" status (not "error" -- the backend may still process).
- FHIR JSON is empty (no transactions yet): FHIR viewer button hidden
- Invoice has preauth but no claim yet: Show "Claim" button. FHIR viewer shows preauth FHIR only.
- Very long FHIR bundle (>500 lines): Virtual scrolling not needed -- JSON viewer is scrollable with fixed height
---
Phase 5: Screen 5 -- Transaction Log + Dashboard Widget
Files: src/app/features/lis/nphies-log/nphies-log.component.ts + dashboard/lis-dashboard.component.ts
Task: #1472
5.1 User Story
Log page -- Who: Lab administrator or billing manager.
When: To audit NPHIES transactions, debug failed requests, track daily activity.
Why: Regulatory compliance, troubleshooting, financial reconciliation.
Dashboard widget -- Who: Any lab user.
When: On the main dashboard, at a glance.
Why: Quick view of today's NPHIES activity volume and covered amounts.
5.2 UI Layout -- Transaction Log Page
┌─────────────────────────────────────────────────────────────────────────┐
│ PageHeader: "سجل معاملات NPHIES" [تحديث] [تصدير Excel] │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Summary Cards ──────────────────────────────────────────────────────┐│
│ │ [تحقق تأمين: 15] [موافقة مسبقة: 12] [مطالبة: 8] [خطأ: 2] ││
│ └──────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Filters ────────────────────────────────────────────────────────────┐│
│ │ [النوع ▾] [الحالة ▾] [من تاريخ] [إلى تاريخ] [بحث مريض...] [مسح] ││
│ └──────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Table ──────────────────────────────────────────────────────────────┐│
│ │ التاريخ │ النوع │ المريض │ الطلب │ الحالة │ [👁] ││
│ │──────────────┼──────────────┼────────────┼─────────┼────────┼───────││
│ │ 14:23 04/02 │ 🟢 تحقق تأمين│ أحمد خالد │ REQ-045 │ ✓ ناجح │ [👁] ││
│ │ 14:20 04/02 │ 🔵 موافقة │ فاطمة حسن │ REQ-044 │ ✓ ناجح │ [👁] ││
│ │ 14:15 04/02 │ 🟡 مطالبة │ خالد عبدالله│ REQ-043 │ ✗ خطأ │ [👁] ││
│ │ ... │ │ │ │ │ ││
│ └──────────────────────────────────────────────────────────────────────┘│
│ │
│ [< 1 2 3 ... >] │
│ │
│ ── JSON Viewer (expandable below table, or side panel) ── │
│ ┌──────────────────────────────────────────────────────────────────────┐│
│ │ [طلب FHIR] [استجابة FHIR] ││
│ │ ││
│ │ { "resourceType": "Bundle", ... } ││
│ │ ││
│ │ [نسخ JSON] [طي] ││
│ └──────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────┘
5.3 UI Layout -- Dashboard Widget
Inserted as a conditional card in the existing stat-cards grid:
@if (nphiesEnabled()) {
<div class="stat-card stat-indigo clickable" (click)="drillDown('nphies-log')">
<div class="stat-icon-wrap"><i class="pi pi-shield"></i></div>
<div class="stat-info">
<span class="stat-value">{{ nphiesTodayCount() }}</span>
<span class="stat-label">{{ 'LIS.NPHIES.DASHBOARD.TITLE' | translate }}</span>
</div>
<div class="stat-decoration"></div>
<i class="stat-arrow pi pi-arrow-right"></i>
</div>
}
When the stat-card is hovered, show a tooltip breakdown:
NPHIES اليوم
تحقق تأمين: 15
موافقة مسبقة: 12
مطالبة مرسلة: 8
مبلغ مغطّى: 4,250 ر.س
5.4 UX Flow -- Transaction Log
1. Page loads with summary cards showing today's counts (loading skeleton first).
2. Table loads page 1 of transactions (25 per page, server-side pagination).
3. Filters: Type dropdown (eligibility, preauth, claim, cancel, poll), Status dropdown (sent, success, error, timeout), date range picker, text search (patient name or request number). All filters trigger a re-fetch via API query params.
4. Row click: Expands a detail panel below the row (accordion-style) showing the FHIR JSON viewer with two tabs (request/response).
5. Summary cards update: When filters change, summary cards show filtered counts (from API response metadata or computed client-side from current page -- prefer API-side if available).
6. Export: Calls the transactions API with format=excel parameter. Downloads file.
7. Auto-refresh: The page refreshes every 60 seconds (using setInterval). A subtle "آخر تحديث: 14:25" timestamp shows in the header. Manual refresh button also available.
5.5 UX Flow -- Dashboard Widget
1. On dashboard load, if nphiesEnabled(), make an additional API call: GET /api/nphies/transactions?date=today&per_page=1 (just to get summary counts from the response -- or a dedicated summary endpoint if available).
2. Display total transaction count as the stat-card value.
3. Click navigates to /lab/nphies-log.
4. Tooltip (via PrimeNG pTooltip) shows breakdown of counts by type and covered amount.
5.6 Visual Design -- Transaction Log
- Summary cards: 4 small cards in a horizontal row. Each card:
width: fit-content, min-width: 140px, padding: 12px 20px, border-radius: 12px, display: flex, align-items: center, gap: 8px.
- Eligibility:
background: #ecfdf5, icon pi pi-check-circle in green
- PreAuth:
background: #eff6ff, icon pi pi-bolt in blue
- Claim:
background: #fefce8, icon pi pi-send in amber
- Error:
background: #fef2f2, icon pi pi-times-circle in red
- Filter bar: Same pattern as other LIS pages. Horizontal flex row with gap. Dropdowns use PrimeNG
Select. Date picker uses PrimeNG DatePicker with range mode. Search input with pi pi-search icon.
- Table: PrimeNG
p-table with paginator, lazy, rows=25. Type column shows colored dot + text. Status column shows badge (same palette as invoice NPHIES status).
- Type colors:
eligibility = green dot, preauth = blue dot, claim = amber dot, cancel = red dot, poll = gray dot
- Expandable row: Uses PrimeNG row expansion template. FHIR viewer same style as invoice dialog viewer (dark background, monospace, syntax highlighted).
- Timestamp format: Arabic locale
ar-SA, relative time when <24h ("قبل ساعتين"), full date otherwise
5.7 Visual Design -- Dashboard Widget
- Uses existing
.stat-indigo color scheme (gradient: #818cf8 to #6366f1)
- Icon:
pi pi-shield
- Matches all other stat-cards exactly
5.8 Component Architecture -- Transaction Log
@Component({
selector: 'app-nphies-log',
standalone: true,
imports: [ /* ... */ ],
providers: [MessageService],
})
export class NphiesLogComponent implements OnInit, OnDestroy {
private nphiesService = inject(NphiesService);
lang = inject(LanguageService);
// State
loading = signal(true);
tableData = signal<NphiesTransaction[]>([]);
totalRecords = signal(0);
first = 0;
rows = 25;
// Filters
typeFilter = signal<string | null>(null);
statusFilter = signal<string | null>(null);
dateFrom = signal<Date | null>(null);
dateTo = signal<Date | null>(null);
searchQuery = signal('');
// Summary
summaryLoading = signal(true);
todayCounts = signal<{ eligibility: number; preauth: number; claim: number; error: number }>({
eligibility: 0, preauth: 0, claim: 0, error: 0,
});
// Detail viewer
expandedRowId = signal<number | null>(null);
selectedTransaction = signal<NphiesTransaction | null>(null);
fhirTab = signal(0);
// Auto-refresh
private refreshInterval: any;
lastRefresh = signal<Date>(new Date());
// Filter options (same pattern as other LIS pages)
typeOptions = [ /* ... */ ];
statusOptions = [ /* ... */ ];
}
5.9 Component Architecture -- Dashboard Addition
Add to existing LisDashboardComponent:
nphiesEnabled = computed(() => this.nphiesService.nphiesEnabled());
nphiesTodayCount = signal(0);
nphiesTodayBreakdown = signal<{ eligibility: number; preauth: number; claim: number; covered: number }>({
eligibility: 0, preauth: 0, claim: 0, covered: 0,
});
In the existing forkJoin data loading block, add a conditional call:
if (this.nphiesService.nphiesEnabled()) {
this.nphiesService.getTransactions(1, 1, { date: today }).subscribe({
next: (res) => {
// Extract counts from meta or data
this.nphiesTodayCount.set(res.meta?.total || 0);
},
error: () => {},
});
}
Add drillDown('nphies-log') case to navigate to /lab/nphies-log.
5.10 RTL/Arabic Considerations
- Summary card numbers are large and center-aligned
- Date/time in Arabic locale (
ar-SA)
- Filter bar flows RTL naturally
- FHIR JSON viewer always LTR
- Pagination controls respect RTL (next/prev arrows flip)
5.11 Responsive Behavior
- Desktop: Summary cards in horizontal row, table full-width
- Tablet: Summary cards wrap to 2x2 grid, table scrollable
- Mobile: Summary cards stack vertically, table shows essential columns only (date, type, status), detail in expandable row
5.12 Accessibility
- Summary cards have
aria-label describing the count
- Table rows are focusable, Enter expands detail
- FHIR viewer has
aria-label="FHIR JSON Bundle viewer"
- Auto-refresh can be paused (Escape key or pause button)
5.13 Edge Cases
- No transactions yet: Empty state illustration: "لا توجد معاملات NPHIES بعد" with a muted shield icon
- Transactions API returns error: Show error state with retry, same pattern as dashboard
- Very large JSON (>100KB): Lazy-render the JSON viewer -- only parse/highlight when expanded
- Date filter spans multiple days: Summary cards show totals for filtered range (not just today)
- Transaction in "sent" status (no response yet): Show amber "مرسل" badge, response tab shows "في انتظار الاستجابة..."
---
Testing Strategy
Since tests are disabled in this project (skipTests: true in angular.json), testing is manual:
- Manual testing checklist per screen:
1. NPHIES disabled: verify zero NPHIES UI renders
2. NPHIES enabled: verify all UI appears
3. Arabic layout: verify RTL alignment, Arabic text
4. English layout: verify LTR switch works
5. API success: verify happy path
6. API error (simulate via browser DevTools network throttling or offline)
7. API timeout (simulate via DevTools slow 3G)
8. Mobile responsive: Chrome DevTools mobile view
- Integration testing with NPHIES Simulator:
- Simulator at
https://moonui.elbaset.com/nphies/ (or local http://localhost:3456)
- Test patients:
1234567890 (Ahmad, Tawuniya Gold), 2098765432 (Fatima, Bupa Silver)
- Test full flow: eligibility -> preauth -> claim -> transaction log
Risks & Mitigations
Risk Severity Mitigation
**Feature flag loading race condition**: Components render before config API returns High The `NphiesService.nphiesEnabled()` signal defaults to `false`. The layout component loads the flag in `ngOnInit()`. All `@if (nphiesEnabled())` blocks simply do not render until the signal turns `true`. No flash of content.
**Request wizard becomes too complex**: Already 950+ lines, adding eligibility logic makes it heavier Medium Extract the eligibility banner into a standalone child component `NphiesEligibilityBannerComponent` that takes `patient` and `insurerCode` as inputs and manages its own state. Parent only listens to an `eligibilityResult` output.
**Invoice page action buttons clutter**: Adding 3 more buttons to each row Medium Use a single "NPHIES" split-button (PrimeNG `SplitButton`) that groups all NPHIES actions in one dropdown. Or use icon-only buttons with tooltips (smaller footprint).
**FHIR JSON rendering performance**: Large bundles could freeze the UI Low Use `JSON.stringify(obj, null, 2)` and render as `` with CSS-based highlighting. Avoid DOM-heavy libraries. Set `max-height: 500px` with overflow scroll.
**Backend API not returning expected fields**: New NPHIES fields on invoice model may not be populated yet Medium Use optional chaining and nullish coalescing everywhere. Hide NPHIES columns/buttons when data is null.
**Concurrent eligibility checks**: User rapidly changes patient/insurance Medium Use `switchMap` on the debounced trigger to auto-cancel previous in-flight requests.
Success Criteria
- [ ]
nphies_enabled=false: Zero NPHIES UI visible anywhere in the LIS
- [ ]
nphies_enabled=true: All 5 screens render correctly in Arabic RTL
- [ ] Settings page: can save config, upload certificates, test connection, map SBSCS codes
- [ ] Patient form: Saudi ID fields appear, validation works per ID type, data saves correctly
- [ ] Request wizard: eligibility check auto-triggers for insured patients, shows result banner, override works
- [ ] Invoices: PreAuth dialog submits and shows approval, Claim dialog submits and shows adjudication, FHIR viewer displays JSON
- [ ] Dashboard: NPHIES KPI card appears with today's count, navigates to log
- [ ] Transaction log: paginated table with filters, expandable FHIR viewer, summary cards
- [ ] All text is translatable (Arabic + English)
- [ ] Responsive on tablet (768px) and mobile (375px)
- [ ] No console errors, no TypeScript compilation errors
- [ ] Builds successfully with
npx ng build --base-href /app/
Implementation Order (Recommended)
Order Phase Task Estimated Time Dependencies
1 Phase 0 Models + Service + Feature Flag + Routes + Translations 2-3 hours None
2 Phase 1 NPHIES Settings Page 3-4 hours Phase 0
3 Phase 2 Patient Saudi ID Fields 2-3 hours Phase 0
4 Phase 3 Eligibility in Request Wizard 3-4 hours Phase 0, Phase 2
5 Phase 4 PreAuth + Claim on Invoices 4-5 hours Phase 0
6 Phase 5 Transaction Log + Dashboard Widget 3-4 hours Phase 0
Total estimated: 17-23 hours of implementation.
Each phase is independently deliverable and testable. Phase 0 is the only hard dependency -- all other phases can proceed in parallel after Phase 0 is complete. Phases 2 and 3 have a soft dependency (eligibility works better when patients have Saudi IDs), but can be built independently.
---
Key File Paths Summary
New files to create:
/home/moonui/public_html/moon-erp/src/app/core/models/nphies.model.ts
/home/moonui/public_html/moon-erp/src/app/core/services/nphies.service.ts
/home/moonui/public_html/moon-erp/src/app/features/lis/nphies-settings/nphies-settings.component.ts
/home/moonui/public_html/moon-erp/src/app/features/lis/nphies-settings/nphies-settings.component.html
/home/moonui/public_html/moon-erp/src/app/features/lis/nphies-settings/nphies-settings.component.scss
/home/moonui/public_html/moon-erp/src/app/features/lis/nphies-log/nphies-log.component.ts
/home/moonui/public_html/moon-erp/src/app/features/lis/nphies-log/nphies-log.component.html
/home/moonui/public_html/moon-erp/src/app/features/lis/nphies-log/nphies-log.component.scss
Existing files to modify:
/home/moonui/public_html/moon-erp/src/app/core/models/lis-patient.model.ts -- add national_id_type, passport_country
/home/moonui/public_html/moon-erp/src/app/core/models/lis-invoice.model.ts -- add 4 NPHIES fields
/home/moonui/public_html/moon-erp/src/app/features/lis/lis-standalone.routes.ts -- add 2 routes
/home/moonui/public_html/moon-erp/src/app/features/lis/lis-layout/lis-layout.component.ts -- feature flag init, conditional nav items, breadcrumb map
/home/moonui/public_html/moon-erp/src/app/features/lis/patients/lis-patients.component.ts -- Saudi ID fields, custom validator
/home/moonui/public_html/moon-erp/src/app/features/lis/patients/lis-patients.component.html -- NPHIES form section
/home/moonui/public_html/moon-erp/src/app/features/lis/request-wizard-v2/request-wizard-v2.component.ts -- eligibility check logic
/home/moonui/public_html/moon-erp/src/app/features/lis/request-wizard-v2/request-wizard-v2.component.html -- eligibility banner
/home/moonui/public_html/moon-erp/src/app/features/lis/invoices/lis-invoices.component.ts -- preauth/claim dialogs, FHIR viewer
/home/moonui/public_html/moon-erp/src/app/features/lis/invoices/lis-invoices.component.html -- NPHIES columns, dialogs
/home/moonui/public_html/moon-erp/src/app/features/lis/dashboard/lis-dashboard.component.ts -- NPHIES KPI card
/home/moonui/public_html/moon-erp/src/app/features/lis/dashboard/lis-dashboard.component.html -- NPHIES stat-card
/home/moonui/public_html/moon-erp/src/assets/i18n/ar.json -- ~100 new translation keys
/home/moonui/public_html/moon-erp/src/assets/i18n/en.json -- ~100 new translation keys
Reference files (read-only context):
/home/moonui/public_html/moon-erp/docs/NPHIES-INTEGRATION.md -- backend API spec and flow diagrams
/home/moonui/nphies-simulator/server.js -- simulator response structures for testing
/home/moonui/public_html/moon-erp/src/app/features/lis/CLAUDE.md -- LIS module architecture reference