Auth Terminal

Quick Role Login

Admin

Full Access

Reception

Front Desk

Technician

Lab Bench

Doctor

Sign & Review

// وظيفة إظهار/إخفاء جهة التعاقد 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 = ''; facilities.forEach(f => options += ``); $('#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} في المختبر`); // هنا يتم تحديث حالة العينة في الجدول } /* ===================== 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){} };