# Royal-GNSCO

Clinical Decision Intelligence

Reception

reception / recep123

Technician

tech / tech123

Doctor

doctor / doc123

Admin

admin / password

## Auth Terminal

Quick Role Login

Admin

Full Access

Reception

Front Desk

Technician

Lab Bench

Doctor

Sign & Review

Remember Me

Total Requests Today

### 0

All samples logged today

Finished Today

### 0

Released reports

Pending Today

### 0

Waiting validation

New Patients

### 0

Registered today

Today Revenue

### 0

SAR collected

Abnormal Results

### 0

Panic alerts

### Top Ordered Tests

### Weekly Revenue (SAR)

### Recent Requests

## Lab Requests

Advanced search • Status tabs • Worklist • Export

From Date
To Date
Accession / Request ID
Patient ID / File
Patient Mobile
Patient Name
Tests
Status
Priority
Worklist
Advanced Worklist Filters

## Add Lab Visit

Step 1: Patient • Step 2: Tests • Step 3: Pricing • Step 4: Confirm & Sample Split

1
Patient
2
Tests
3
Pricing
4
Confirm

#### Search Patient / بحث عن مريض

Patient Name / اسم المريض *
Mobile / رقم الجوال *
National ID / رقم الهوية *
Age / العمر
Gender / الجنس
Nationality / الجنسية *
Patient Type / نوع المريض
Contract Facility / جهة التعاقد
Medical History / التاريخ الطبي

### Step 2 • Tests Selection

Tests & Packages

#### Selected Tests

### Step 3 • Pricing Summary

Test / Package | Type | Price
--- | --- | ---

Insurance Company
Sub-Total: 0
Discount %:
Net Total: 0

### Step 4 • Confirm & Sample Split

#### Visit Summary

Patient: -

Mobile: -

Tests Count: 0

Net Total: 0 SAR

#### Sample Collection & Split

Primary Sample Type
Primary Tube
Sample Split (Secondary Aliquots)

## Laboratory Workflow

### New Samples

0

No samples in this lane.

### In Progress

0

Drag samples here when processing.

### Finished

0

Completed samples ready for sign-off.

Doctor: Not Selected

Current Patient

No Selection

### All Requests / Patients

Click any row to open result entry

### Sample Meta

Sample Status: Pending

TAT Target: 60 mins

Sample Log: No sample selected.

### Doctor

### Result Entry

Tests & Packages Search

Test | Result | Unit | Ref Range | Prev | Flags
--- | --- | --- | --- | --- | ---

## QC Module

### QC Overview

- Today QC Runs: 5
- Failed Rules: 1
- Pending Review: 2

### Recent QC Events

Overlay L1/L2/L3
Westgard evaluation uses historical z-scores per analyte + level. Warnings block release only when FAIL (or admin override with reason).

Monthly Summary (Mean±SD, CV%)

Machines & Rules
Implemented Westgard Rules:

- 1-2s (Warning)
- 1-3s (Reject)
- 2-2s (Reject)
- R-4s (Reject)
- 4-1s (Reject)
- 10x (Reject)
- 8x (Reject)
- 7T (Reject)

## Investigations

Test catalog • Reference ranges • Packages & offers • Excel import

### Test Catalog

Code | Test Name (EN) | Test Name (AR) | Lab Unit | Unit | Ref Range | Age/Gender | Price | Active | Actions
--- | --- | --- | --- | --- | --- | --- | --- | --- | ---

### Packages & Offers

### Import from Excel

Upload an Excel file (.xlsx) with columns: Code, NameEN, NameAR, Section, Unit, RefRange, AgeGroup, Gender, Price.

Drop Excel file here or click to upload.

Preview imported tests

Code | NameEN | Lab Unit | Unit | Price
--- | --- | --- | --- | ---

## Pricing Engine

### Master Price List

### External Labs Prices

## Patients

Searchable patient list • Full profile • Timeline

Name
File / ID
Mobile
Date of Birth

###

File: • Age: •

Mobile:

Last Visit:

#### Labs Timeline

#### Visits

Date | Visit/Request | Status | Action
--- | --- | --- | ---

#### Trend (HbA1c)

## Reports Center

From Date
To Date
Organization
Doctor
Status:

### Financial Report • Daily / Range

Total Invoices: 0
Total Amount: 0
Total Paid: 0
Total Deferred: 0

### B2B Statement

### Pending Tests Report

### Organization Complete Report

### Doctor Cash Transfer Report

## Settings

### Organizations

- Company A discount 5%
- Company B discount 10%

### Lab Devices

- Hematology Analyzer Online
- Chemistry Analyzer Online

### Program Settings

Lab Name
Stamp/Logo
Default Report Header Royal-GNSCO Laboratory - نتائج مختبر

## Roles & Access Control

Manage user roles and permissions. (Mock UI - backend integration required)

### Admin

Full system access, user management, all modules.

### Web Results Publishing

Enabled
Endpoint URL
API Key (optional)

When enabled, published results will also be POSTed as JSON to the endpoint.

### Reception

Patient registration, visit creation, print labels.

### Technician

Result entry, QC, rerun, panic reporting.

## System Events

Full audit of critical actions (login, result changes, panic, reject, finalize, exports).

## Collection & Sampling Unit

تأكيد البيانات، سحب العينات، وطباعة الباركود

بانتظار السحب: 3
AS

#### Ahmed Al-Saudi

SID: #24-90881 | ID: 1092***88

الجنسية: سعودي
نوع المريض: B2B (Company A)

تاريخ طبي:

حساسية من اللاتكس - Latex Allergy

##### اختيار الفحوصات للسحب (Select to Collect)

CBC (Lavender) EDTA
Glucose (Gray) Fluoride
Vitamin D (Red) Serum

تأجيل للسحب لاحقاً (Split)

## Receiving Area (Receiving Area)

امسح الباركود لتأكيد وصول العينة للمختبر

رقم العينة | المريض | وقت السحب | الإجراء
--- | --- | --- | ---

## Lab Machine Interfacing

ASTM / HL7 Protocol Management & Real-time Data Bridge

### Active Analyzers

ONLINE

#### Cobas 6000 (Chemistry)

IP: 192.168.1.105 | Port: 5000

IDLE

#### Sysmex XN-1000

IP: 192.168.1.110 | Port: 5001

### Live Communication Log (ASTM/HL7)

Streaming Raw Data...

[2026-02-28 15:20:01] INIT_CONN: Attempting handshake with Cobas 6000...

[2026-02-28 15:20:03] ASTM_RECV: H|\^&|||LIS||||||P|1

[2026-02-28 15:20:04] ASTM_RECV: P|1||PID12345||DOE^JOHN||19800101|M

[2026-02-28 15:20:05] RESULT_IN: SID: #24-9011 | GLU: 115 mg/dL | CREAT: 1.1 mg/dL

[2026-02-28 15:20:06] DB_UPDATE: Automatically synced results to Patient File F-2001.

Data Encryption: AES-256 Enabled

## Patient Portal (Preview)

Only published results are visible to patients.

Patient File

Date | Test | Result | Unit | Status
--- | --- | --- | --- | ---
Enter patient file to view results.

#### Notifications

No notifications yet.

### New Investigation

Test Name (EN)
Test Name (AR)
Code
Lab Unit
Unit
Price (SAR)
Reference Range
Test Comments (per test)
Age Group
Gender
Active

### New Package / Offer

Patient Type / نوع المريض
Contract Facility / جهة التعاقد
Tests in Package
Package Price (SAR)
Status
// وظيفة إظهار/إخفاء جهة التعاقد function toggleContractFields() { const type = $('#p-type').val(); const wrapper = $('#contract-facility-wrapper'); if (type === 'b2b') { wrapper.removeClass('hidden'); // محاكاة جلب البيانات من إعدادات B2B const facilities = [ { id: 'ORG-001', name: 'Company A / شركة أ' }, { id: 'ORG-002', name: 'Company B / شركة ب' } ]; let options = 'Select Facility / اختر الجهة'; facilities.forEach(f => options += `${f.name}`); $('#p-contract-facility').html(options); } else { wrapper.addClass('hidden'); } } // إصلاح وظيفة الزر Next Step function wizNext() { // التحقق من الحقول المطلوبة في الخطوة الأولى if (wizardStep === 1) { const phone = $('#wiz-patient-mobile').val().trim(); const idNum = $('#wiz-patient-nid').val().trim(); const pname = $('#wiz-patient-name').val().trim(); if (!pname || !phone || !idNum) { toastr.error('Please fill required fields (Name, Phone & ID)'); return; } // إذا كان المريض B2B، يجب اختيار جهة التعاقد if ($('#p-type').val() === 'b2b' && !$('#p-contract-facility').val()) { toastr.error('Please select a Contract Facility'); return; } } // الانتقال للخطوة التالية if (wizardStep < 4) { wizardStep++; updateWizardUI(); } } // تحديث الواجهة عند التنقل function updateWizardUI() { $('.wizard-step').removeClass('active'); $(`#wiz-step-${wizardStep}`).addClass('active'); // تحديث شريط الخطوات (Dots) for (let i = 1; i <= 4; i++) { const dot = $(`#wiz-dot-${i}`); if (i < wizardStep) { dot.removeClass('bg-royal-600').addClass('bg-emerald-500'); } else if (i === wizardStep) { dot.addClass('bg-royal-600'); } } // تفعيل/تعطيل زر السابق $('#wiz-prev').prop('disabled', wizardStep === 1); } // وظيفة تأكيد سحب العينة (تحويلها إلى Collected) function confirmCollection(sid) { // التحقق من الفحوصات المختارة const selectedTestsCount = $('.test-check:checked').length; const totalTestsCount = $('.test-check').length; if (selectedTestsCount === 0) { toastr.warning('يرجى اختيار فحص واحد على الأقل لسحبه'); return; } let message = `تم سحب عدد (${selectedTestsCount}) فحص بنجاح`; let statusUpdate = "Collected"; if (selectedTestsCount < totalTestsCount) { message = `تم سحب جزء من الفحوصات بنجاح (Split Collection). الباقي بانتظار السحب.`; statusUpdate = "Partially Collected"; } Swal.fire({ title: 'تأكيد عملية السحب؟', text: `رقم العينة: ${sid} - الحالة: ${statusUpdate}`, icon: 'question', showCancelButton: true, confirmButtonColor: '#10b981', confirmButtonText: 'نعم، تم السحب والطباعة' }).then((result) => { if (result.isConfirmed) { // 1. محاكاة تحديث حالة العينة في النظام logEvent('SAMPLE_COLLECTION', { sid, status: statusUpdate, tests: selectedTestsCount }); // 2. طباعة الباركود تلقائياً عند التأكيد printSampleBarcodes(sid); // 3. إشعار النجاح toastr.success(message); // 4. تحديث الواجهة (حذف الكرت أو تحديثه) if (statusUpdate === "Collected") { $(`button[onclick="confirmCollection('${sid}')"]`).closest('.bg-white').fadeOut(); } } }); } // وظيفة طباعة الباركود function printSampleBarcodes(sid) { toastr.info(`جاري تحضير الباركود للعينة ${sid}...`); // هنا يتم استدعاء المكتبة الخاصة بالطباعة الحرارية console.log(`Printing Label for ${sid} at ${new Date().toLocaleTimeString()}`); } // وظيفة تحديث القائمة function refreshSamplingList() { toastr.info('جاري تحديث قائمة الانتظار...'); // منطق لجلب البيانات الجديدة من السيرفر } // وظيفة محاكاة استقبال البيانات من الأجهزة function simulateMachineResult(sid, testCode, value) { const timestamp = new Date().toLocaleTimeString(); const logEntry = `

[${timestamp}] RESULT_IN: SID: ${sid} | ${testCode}: ${value} | AUTO-SYNC SUCCESS

`; $('#machine-terminal').append(logEntry); // تمرير التلقائي لأسفل السجل const terminal = document.getElementById('machine-terminal'); terminal.scrollTop = terminal.scrollHeight; // تنبيه المستخدم بوصول نتيجة toastr.success(`New result received from Analyzer for SID: ${sid}`, 'Machine Interface'); } // إضافة جهاز جديد function addNewMachine() { Swal.fire({ title: 'Configure New Analyzer', html: `
`, confirmButtonText: 'Connect Analyzer', showCancelButton: true }).then((result) => { if (result.isConfirmed) { toastr.info('Testing Handshake...'); setTimeout(() => toastr.success('Machine Interfaced Successfully'), 1500); } }); } function clearTerminal() { $('#machine-terminal').html('

-- Terminal Cleared --

'); }
// وظيفة تبديل حقول العقد لـ B2B /** * Toggles the visibility of the Contract Facility field * based on B2B selection. */ function toggleContractFields() { const type = $('#p-type').val(); const wrapper = $('#contract-facility-wrapper'); if (type === 'b2b') { wrapper.removeClass('hidden'); } else { wrapper.addClass('hidden'); $('#p-contract-facility').val(''); // Reset facility on hide } } /** * Enhanced Saudi Invoicing Logic * Applies 15% VAT for Non-Saudis and B2B contracts. */ function updateSaudiPricing(subtotal) { const nationality = $('#p-nat-type').val(); const patientType = $('#p-type').val(); // Logic: VAT applies if Non-Saudi OR if it's a B2B corporate account const isVatApplicable = (nationality === 'non-saudi' || patientType === 'b2b'); const vatRate = isVatApplicable ? 0.15 : 0.00; const vatAmount = subtotal * vatRate; const total = subtotal + vatAmount; $('#inv-subtotal').text(subtotal.toFixed(2)); $('#inv-vat').text(vatAmount.toFixed(2)); $('#inv-total').text(total.toFixed(2)); // Log for audit trail console.log(`Pricing: Subtotal ${subtotal}, VAT ${vatAmount}, Total ${total}`); } // استلام العينة function receiveSample(sid) { toastr.success(`تم استلام العينة رقم ${sid} في المختبر`); // هنا يتم تحديث حالة العينة في الجدول }

### Results Entry

Patient
Release Status
CAP/CBAHI: Results cannot be shared before validation + release.
Actions
Finalize locks the draft snapshot; Publish is gated by Validate.

Unit | Test | Result | Flag | Reference | Comment
--- | --- | --- | --- | --- | ---

Calculated tests are auto-derived from other results and are read-only.
/* ===================== QC v18 Enhancements (Overrides) ===================== */ (function(){ // ---- store keys + migration const V16 = (window.QC_STORE_KEYS && window.QC_STORE_KEYS.machines) ? window.QC_STORE_KEYS : null; window.QC_STORE_KEYS_V18 = { machines: 'qc_machines_v18', analytes: 'qc_analytes_v18', runs: 'qc_runs_v18', lots: 'qc_lots_v18', overrides: 'qc_overrides_v18' }; function _lsGet(k){ try{return JSON.parse(localStorage.getItem(k)||'null');}catch(e){return null;} } function _lsSet(k,v){ localStorage.setItem(k, JSON.stringify(v)); } function qcMigrateToV18(){ const has = _lsGet(QC_STORE_KEYS_V18.machines); if(has && Array.isArray(has) && has.length) return; // migrate from v16 if available; otherwise keep empty (seed will fill) try{ if(V16){ const m = _lsGet(V16.machines) || []; const a = _lsGet(V16.analytes) || []; const r = _lsGet(V16.runs) || []; const o = _lsGet(V16.overrides) || []; _lsSet(QC_STORE_KEYS_V18.machines, m); _lsSet(QC_STORE_KEYS_V18.analytes, a.map(x=>({ ...x, rules: x.rules || ['1-2s','1-3s','2-2s','R-4s','4-1s','10x','8x','7T'] }))); _lsSet(QC_STORE_KEYS_V18.runs, r.map(x=>({ ...x, machineId: x.machineId || (x.machine||'') , lotId: x.lotId || '' }))); _lsSet(QC_STORE_KEYS_V18.overrides, o); } if(!_lsGet(QC_STORE_KEYS_V18.lots)) _lsSet(QC_STORE_KEYS_V18.lots, []); }catch(e){ // ignore } } // override qcLoad/qcSave to point to v18 keys without breaking older callers const _origQcLoad = window.qcLoad; const _origQcSave = window.qcSave; window.qcLoad = function(key, fallback){ qcMigrateToV18(); // map v16 keys -> v18 if(V16 && key===V16.machines) key = QC_STORE_KEYS_V18.machines; if(V16 && key===V16.analytes) key = QC_STORE_KEYS_V18.analytes; if(V16 && key===V16.runs) key = QC_STORE_KEYS_V18.runs; if(V16 && key===V16.overrides) key = QC_STORE_KEYS_V18.overrides; return _origQcLoad ? _origQcLoad(key, fallback) : (fallback); }; window.qcSave = function(key, value){ qcMigrateToV18(); if(V16 && key===V16.machines) key = QC_STORE_KEYS_V18.machines; if(V16 && key===V16.analytes) key = QC_STORE_KEYS_V18.analytes; if(V16 && key===V16.runs) key = QC_STORE_KEYS_V18.runs; if(V16 && key===V16.overrides) key = QC_STORE_KEYS_V18.overrides; return _origQcSave ? _origQcSave(key, value) : localStorage.setItem(key, JSON.stringify(value)); }; // --- helper function todayISO(){ return new Date().toISOString().slice(0,10); } function isExpired(iso){ if(!iso) return false; try{ return (new Date(iso).getTime() < new Date(todayISO()).getTime()); }catch(e){return false;} } function uid(prefix){ return (prefix||'ID')+'-'+Math.random().toString(16).slice(2,8).toUpperCase(); } // ---- Lots CRUD window.qcLots = function(){ return qcLoad(QC_STORE_KEYS_V18.lots, []); }; window.qcSaveLots = function(lots){ qcSave(QC_STORE_KEYS_V18.lots, lots); }; window.qcActiveLotsFor = function(machineId, analyteId, level){ const lots = qcLots().filter(l=>l.active!==false && l.machineId===machineId && l.analyteId===analyteId && l.level===level); // filter expired return lots.filter(l=>!isExpired(l.expiry)); }; // ---- Per analyte rules window.qcRulesAll = ['1-2s','1-3s','2-2s','R-4s','4-1s','10x','8x','7T']; function analyteRules(analyte){ const r = analyte?.rules; return (Array.isArray(r) && r.length) ? r : qcRulesAll; } // ---- Westgard evaluation (series-based) function z(value, mean, sd){ if(!sd) return 0; return (value-mean)/sd; } function evalSeries(runs, mean, sd, rules){ // returns per-index {status, violations[]} const out = runs.map(()=>({status:'PASS', violations:[]})); const zs = runs.map(r=>z(Number(r.value), mean, sd)); const absz = zs.map(v=>Math.abs(v)); // helper set const mark = (i, v, sev)=>{ if(i<0||i>=out.length) return; out[i].violations.push(v); if(sev==='FAIL') out[i].status='FAIL'; else if(sev==='WARN' && out[i].status!=='FAIL') out[i].status='WARN'; }; const has = (r)=>rules.includes(r); for(let i=0;i= 2 && absz[i] < 3) mark(i,'1-2s','WARN'); // 1-3s reject if(has('1-3s') && absz[i] >= 3) mark(i,'1-3s','FAIL'); // 2-2s: two consecutive >=2 on same side if(has('2-2s') && i>=1){ const sameSide = (zs[i] >= 2 && zs[i-1] >= 2) || (zs[i] <= -2 && zs[i-1] <= -2); if(sameSide){ mark(i-1,'2-2s','FAIL'); mark(i,'2-2s','FAIL'); } } // R-4s: consecutive differ by >=4 SD if(has('R-4s') && i>=1){ if(Math.abs(zs[i]-zs[i-1]) >= 4){ mark(i-1,'R-4s','FAIL'); mark(i,'R-4s','FAIL'); } } // 4-1s: four consecutive beyond 1 SD same side if(has('4-1s') && i>=3){ const seg = zs.slice(i-3,i+1); const pos = seg.every(v=>v>=1); const neg = seg.every(v=>v<=-1); if(pos||neg){ for(let k=i-3;k<=i;k++) mark(k,'4-1s','FAIL'); } } // 10x: ten consecutive on same side of mean if(has('10x') && i>=9){ const seg = zs.slice(i-9,i+1); const pos = seg.every(v=>v>0); const neg = seg.every(v=>v<0); if(pos||neg){ for(let k=i-9;k<=i;k++) mark(k,'10x','FAIL'); } } // 8x: eight consecutive on same side if(has('8x') && i>=7){ const seg = zs.slice(i-7,i+1); const pos = seg.every(v=>v>0); const neg = seg.every(v=>v<0); if(pos||neg){ for(let k=i-7;k<=i;k++) mark(k,'8x','FAIL'); } } // 7T: seven-point trend (monotonic) if(has('7T') && i>=6){ const seg = runs.slice(i-6,i+1).map(r=>Number(r.value)); const inc = seg.every((v,idx)=> idx===0?true : v>seg[idx-1]); const dec = seg.every((v,idx)=> idx===0?true : v{ b.onclick = ()=>{ if(lvlSel){ lvlSel.value=b.getAttribute('data-qc-lvl'); } window.qcRenderLJ(); }; }); if(lvlSel){ const active = lvlSel.value || 'L1'; btns.forEach(b=> b.classList.toggle('bg-gray-900', b.getAttribute('data-qc-lvl')===active)); btns.forEach(b=> b.classList.toggle('text-white', b.getAttribute('data-qc-lvl')===active)); } }catch(e){} // call original render, then enhance chart point styles if possible if(_origQcRenderLJ) _origQcRenderLJ(); try{ const aSel = document.getElementById('qcAnalyteSel'); const level = document.getElementById('qcLevelSel')?.value || 'L1'; const mSel = document.getElementById('qcMachineSel'); if(!aSel?.value) return; const analyte = (window.qcActiveAnalytes? window.qcActiveAnalytes(): []).find(a=>a.id===aSel.value); if(!analyte) return; const mean = Number(analyte.mean?.[level]); const sd = Number(analyte.sd?.[level]); if(!mean || !sd) return; const runs = (window.qcRunsFor? window.qcRunsFor(analyte.id, level): []); // filter by machine if selector exists const machineId = mSel?.value || ''; const fruns = machineId ? runs.filter(r=>(r.machineId||'')===machineId) : runs; const evals = evalSeries(fruns, mean, sd, analyteRules(analyte)); // attach to global for tooltips/monthly window.qcLastEval = { analyteId: analyte.id, level, machineId, evals, runs: fruns }; if(window.qcLJChart){ const ds = window.qcLJChart.data.datasets?.[0]; if(ds){ const pointStyle = evals.map(e=> e.status==='FAIL' ? 'triangle' : (e.status==='WARN'?'rectRot':'circle')); const pointRadius = evals.map(e=> e.status==='FAIL' ? 6 : (e.status==='WARN'?5:4)); ds.pointStyle = pointStyle; ds.pointRadius = pointRadius; ds.pointHoverRadius = pointRadius.map(v=>v+2); // add tooltip callback with violations window.qcLJChart.options.plugins = window.qcLJChart.options.plugins || {}; window.qcLJChart.options.plugins.tooltip = window.qcLJChart.options.plugins.tooltip || {}; window.qcLJChart.options.plugins.tooltip.callbacks = window.qcLJChart.options.plugins.tooltip.callbacks || {}; window.qcLJChart.options.plugins.tooltip.callbacks.afterLabel = (ctx)=>{ const i = ctx.dataIndex; const v = evals[i]?.violations || []; return v.length ? ('Rules: '+v.join(', ')) : 'Rules: none'; }; window.qcLJChart.update(); } } }catch(e){} };