04. Lab Module - Page-by-Page Features Documentation
เอกสารรายละเอียดคุณสมบัติของแต่ละหน้า
HIS Prototype - Laboratory Management System
📋 Table of Contents
- index.html - หน้าแรก/เมนูหลัก Lab Module
- doctor-order.html - สั่งตรวจแล็บ (Doctor Order Entry)
- order-queue.html - จัดการคิว Order แล็บ
- specimen-collection.html - เก็บสิ่งส่งตรวจ
- result-entry.html - ลงผลแล็บ (Result Entry)
- result-approval.html - อนุมัติผลแล็บ (Result Approval)
- Utility Pages - หน้าเสริม (Debug/Clear Storage)
1. index.html - หน้าแรก/เมนูหลัก
📌 ภาพรวม
หน้า Landing Page ของระบบงานชันสูตร ทำหน้าที่เป็น Navigation Hub เชื่อมโยงไปยังหน้าต่างๆ ในระบบ Lab
🎯 วัตถุประสงค์
- ให้ผู้ใช้เข้าถึงหน้าต่างๆ ในระบบ Lab ได้สะดวก
- แสดงภาพรวมความสามารถของระบบ Lab Module
- จัดกลุ่มเมนูตามบทบาทผู้ใช้งาน (Doctor, Phlebotomist, Technician, Supervisor)
🧩 UI Components
1. Hero Section
┌─────────────────────────────────────────────────────────────┐
│ 🔬 ระบบงานชันสูตร - Laboratory Management System │
│ จัดการคำสั่งตรวจแล็บ ตัวอย่างสิ่งส่งตรวจ และผลแล็บ │
└─────────────────────────────────────────────────────────────┘
คุณสมบัติ:
- Background Gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%)
- สีตัวอักษร: สีขาว (White)
- แสดงชื่อระบบและคำอธิบายสั้นๆ
2. Doctor Workflow Section
หัวข้อ: 👨⚕️ ระบบสำหรับแพทย์ (Doctor Workflow)
เมนู:
1. สั่งตรวจแล็บ (Lab Order)
- ไอคอน: fas fa-file-medical
- สี Badge: Success Green
- ลิงก์: doctor-order.html
- คำอธิบาย: สั่งตรวจแล็บสำหรับผู้ป่วย เลือกรายการตรวจ และพิมพ์ใบนำส่ง
- ดูผลแล็บ (View Results)
- ไอคอน:
fas fa-flask - สี Badge: Info Blue
- ลิงก์:
#(Coming Soon) - คำอธิบาย: ตรวจสอบผลแล็บ แสดง Critical Results และประวัติการตรวจ
3. Lab Staff Section
หัวข้อ: 🔬 ระบบสำหรับเจ้าหน้าที่แล็บ (Lab Staff Workflow)
เมนู:
3.1 จัดการคิว Order (Order Queue)
- ไอคอน: fas fa-list-check
- สี Badge: Warning Orange
- ลิงก์: order-queue.html
- คำอธิบาย: ตรวจสอบคิว Order แล็บ ยืนยันการรับ Order และจัดการสถานะ
3.2 เก็บสิ่งส่งตรวจ (Specimen Collection)
- ไอคอน: fas fa-vial
- สี Badge: Primary Blue
- ลิงก์: specimen-collection.html
- คำอธิบาย: บันทึกการเก็บตัวอย่าง ตรวจสอบคุณภาพ และพิมพ์ Label
3.3 ลงผลแล็บ (Result Entry)
- ไอคอน: fas fa-keyboard
- สี Badge: Info Cyan
- ลิงก์: result-entry.html
- คำอธิบาย: บันทึกผลแล็บ Delta Check และ Critical Alert
3.4 อนุมัติผลแล็บ (Result Approval)
- ไอคอน: fas fa-check-double
- สี Badge: Success Green
- ลิงก์: result-approval.html
- คำอธิบาย: ตรวจสอบและอนุมัติผลแล็บก่อนรายงาน
4. Utility Tools Section
หัวข้อ: 🛠️ เครื่องมือเสริม (Utility Tools)
เมนู:
4.1 ตรวจสอบข้อมูล (Debug Lab Data)
- ไอคอน: fas fa-bug
- สี Badge: Secondary Gray
- ลิงก์: debug-lab-data.html
- คำอธิบาย: ตรวจสอบข้อมูลใน LocalStorage
4.2 ล้างข้อมูล (Clear Storage)
- ไอคอน: fas fa-trash-can
- สี Badge: Danger Red
- ลิงก์: clear-storage.html
- คำอธิบาย: ล้างข้อมูล LocalStorage และ Reset ระบบ
🎨 Design System
Color Scheme
--primary-color: #667eea (Purple-Blue)
--success-color: #28a745 (Green)
--warning-color: #ffc107 (Orange)
--danger-color: #dc3545 (Red)
--info-color: #17a2b8 (Cyan)
--secondary-color: #6c757d (Gray)
Card Layout
┌─────────────────────────────────────────┐
│ 🔬 [Icon] [Badge] │
│ **Card Title** │
│ Description text here... │
│ │
│ [View Details →] │
└─────────────────────────────────────────┘
Card Properties:
- Border: 1px solid #ddd
- Border Radius: 8px
- Padding: 1.5rem
- Shadow: 0 2px 4px rgba(0,0,0,0.1)
- Hover Effect: Lift + Shadow increase
- Transition: all 0.3s ease
Grid System
- Layout: CSS Grid
- Columns:
repeat(auto-fill, minmax(280px, 1fr)) - Gap:
1.5rem - Responsive: Auto-adjust columns based on screen width
🔧 Technical Specifications
Dependencies
<!-- CSS -->
<link rel="stylesheet" href="../../assets/css/main.css">
<link rel="stylesheet" href="../../assets/css/components.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
File Size
- Total Lines: ~656 lines
- Primarily HTML + CSS (No JavaScript logic)
- Static navigation page
Browser Compatibility
- Modern browsers (Chrome, Firefox, Edge, Safari)
- Responsive design (Mobile, Tablet, Desktop)
- No polyfills required
📱 Responsive Behavior
Desktop (> 1200px)
- 3-4 cards per row
- Full-width hero section
- Spacious padding
Tablet (768px - 1200px)
- 2-3 cards per row
- Medium padding
- Readable font sizes
Mobile (< 768px)
- 1 card per row (stacked)
- Compact hero section
- Touch-friendly button sizes (min 44x44px)
🔗 Navigation Flow
graph LR
A[index.html] --> B[doctor-order.html]
A --> C[order-queue.html]
A --> D[specimen-collection.html]
A --> E[result-entry.html]
A --> F[result-approval.html]
A --> G[debug-lab-data.html]
A --> H[clear-storage.html]
style A fill:#667eea,color:#fff
style B fill:#28a745,color:#fff
style C fill:#ffc107,color:#000
style D fill:#007bff,color:#fff
style E fill:#17a2b8,color:#fff
style F fill:#28a745,color:#fff
style G fill:#6c757d,color:#fff
style H fill:#dc3545,color:#fff
💡 Best Practices
For UX/UI Team:
- Visual Hierarchy: ใช้ Section Headers แยกกลุ่มเมนูชัดเจน
- Color Coding: สีของ Badge บ่งบอกประเภทการใช้งาน (Success = Create, Info = View, Warning = Manage)
- Icon Consistency: ใช้ FontAwesome เพื่อความสม่ำเสมอ
- White Space: เว้นระยะระหว่าง Section เพื่อลดความรู้สึกแออัด
For Developers:
- No JavaScript Required: Static HTML page ไม่ต้อง Load Services
- Fast Load Time: Minimal dependencies (CSS + FontAwesome เท่านั้น)
- Easy Maintenance: แก้ไขเมนูได้ง่ายโดยเพิ่ม/ลบ Card HTML
🐛 Known Issues
- Coming Soon Links: เมนู "ดูผลแล็บ" ยังไม่มีหน้าจริง (ใช้
href="#") - No Active State: ไม่มีการไฮไลต์ Current Page (เพราะเป็นหน้าเมนูหลัก)
📊 Usage Statistics (Mock Data)
- Most Clicked: doctor-order.html (Doctor workflow entry point)
- Second: specimen-collection.html (Daily task for phlebotomists)
- Least Used: Utility tools (Debug/Clear - Admin only)
2. doctor-order.html - สั่งตรวจแล็บ
📌 ภาพรวม
หน้าสำหรับแพทย์สั่งตรวจแล็บให้ผู้ป่วย เป็นจุดเริ่มต้นของ Lab Workflow ทั้งหมด มีฟีเจอร์ครบถ้วนตั้งแต่ค้นหาผู้ป่วย เลือกรายการตรวจ คำนวณค่าใช้จ่าย จนถึงพิมพ์ใบนำส่ง
🎯 วัตถุประสงค์
- ให้แพทย์สั่งตรวจแล็บได้สะดวกและรวดเร็ว
- แสดงข้อมูลค่าใช้จ่ายโปร่งใส (ราคาเต็ม, สิทธิ์เบิก, ผู้ป่วยจ่าย)
- รองรับการสั่ง STAT Order (ด่วนพิเศษ)
- สร้าง Order Number อัตโนมัติ และพิมพ์ใบนำส่งแล็บ
📊 File Statistics
- Total Lines: 1,445 lines
- Components: 4 major sections
- Dependencies: Patient.js, LabItem.js, LabOrder.js, PatientService, LabOrderService
- Data Files: lab-items.json, lab-panels.json, lab-pricing.json, patients.json
🧩 UI Components & Workflow
Section 1: ค้นหาผู้ป่วย (Patient Search)
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ 🔍 ค้นหาผู้ป่วย │
├────────────────────────────────────────────────────────────┤
│ เลข HN: [_________________________] [🔍 ค้นหา] │
│ │
│ 💡 ข้อมูลทดสอบ - HN ที่มีในระบบ: │
│ [HN000001] [HN000002] [HN000123] [HN000456] ... │
└────────────────────────────────────────────────────────────┘
คุณสมบัติ: 1. HN Input Field - Placeholder: "ระบุเลข HN ผู้ป่วย (เช่น HN000001)" - Validation: Required field - Auto-uppercase: แปลง "hn000001" → "HN000001" อัตโนมัติ
- Quick Select Buttons
- แสดง HN 6 คนแรกจาก Mock Data
- คลิก → Auto-fill HN input → Trigger search
- สี: Gray (#6c757d) เพื่อให้ต่างจาก Primary Action
-
Format: "HN000001 - ชื่อ" (แสดง First Name only)
-
Search Logic (
searchPatient())// 1. Validate HN input // 2. Call PatientService.getPatientByHN(hn) // 3. If found → Display Patient Info + Show Lab Selection // 4. If not found → Alert "ไม่พบผู้ป่วย HN: XXX"
Error Handling: - Empty HN → "กรุณาระบุเลข HN" - Invalid HN → "ไม่พบผู้ป่วย HN: XXX" - Network Error → "เกิดข้อผิดพลาดในการค้นหา"
Section 2: ข้อมูลผู้ป่วย (Patient Information)
Visibility: display: none initially, แสดงหลัง Search สำเร็จ
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ 👤 ข้อมูลผู้ป่วย │
├────────────────────────────────────────────────────────────┤
│ HN: HN000123 ชื่อ-สกุล: นายสมชาย ใจดี │
│ อายุ: 30 ปี เพศ: ชาย │
│ วันเกิด: 15/05/2538 กรุ๊ปเลือด: O+ │
│ สิทธิการรักษา: บัตรทอง (UC) │
│ แพ้ยา: Penicillin, Aspirin │
│ โรคประจำตัว: Diabetes Type 2 │
└────────────────────────────────────────────────────────────┘
Data Display:
- Title: แปลงจาก 'mr' → 'นาย', 'mrs' → 'นาง' (via Patient.getTitleName())
- Age: คำนวณจาก Date of Birth (via Patient.getAge())
- Insurance Type: แสดงเป็นภาษาไทย (uc → 'บัตรทอง', social → 'ประกันสังคม')
- Allergies: แสดงเป็น Comma-separated list, สีแดงถ้ามี
- Chronic Diseases: แสดงเป็น List, สีส้มถ้ามี
Styling:
- Background: Info Light Blue (#cfe2ff)
- Border: Info Border (#0dcaf0)
- Grid: 2 columns on Desktop, 1 column on Mobile
Section 3: เลือกรายการตรวจ (Lab Item Selection)
Visibility: แสดงหลัง Patient Info loaded
UI Structure:
┌────────────────────────────────────────────────────────────┐
│ 🧪 เลือกรายการตรวจแล็บ │
├────────────────────────────────────────────────────────────┤
│ [🎯 Panel Tests] [📋 Individual Tests] │
│ │
│ ┌─── Panel View (Tab 1) ───────────────────────────────┐ │
│ │ ┌────────────────┐ ┌────────────────┐ │ │
│ │ │ CBC (PANEL) │ │ LFT (PANEL) │ │ │
│ │ │ นับเม็ดเลือด │ │ ตับอักเสบ │ │ │
│ │ │ 🔴 EDTA │ │ 🟢 Plain │ │ │
│ │ │ ⏱ 2 ชม. │ │ ⏱ 4 ชม. │ │ │
│ │ │ 📋 8 รายการ │ │ 📋 5 รายการ │ │ │
│ │ │ [Regular] [⚡STAT] │ │ │
│ │ └────────────────┘ └────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Individual View (Tab 2) ──────────────────────────┐ │
│ │ Category Tabs: │ │
│ │ [ทั้งหมด] [Hematology] [Chemistry] [Immunology]... │ │
│ │ │ │
│ │ 🔍 Search: [_________________________________] │ │
│ │ │ │
│ │ ┌────────────────┐ ┌────────────────┐ │ │
│ │ │ CBC │ │ FBS │ │ │
│ │ │ นับเม็ดเลือด │ │ น้ำตาลในเลือด │ │ │
│ │ │ 🔴 EDTA │ │ 🟢 Fluoride │ │ │
│ │ │ ⏱ 120 นาที │ │ ⏱ 60 นาที │ │ │
│ │ │ [Regular] [⚡STAT] │ [Regular] │ │ │
│ │ └────────────────┘ └────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
3.1 Panel Tests (Tab 1)
Panel Card Components:
- Panel Name: ชื่อ Panel (เช่น "CBC", "LFT") + Badge "PANEL"
- Thai Name: ชื่อภาษาไทย (เช่น "นับเม็ดเลือด", "ตับอักเสบ")
- Code: รหัส Panel (เช่น "PANEL001")
- Specimen Badge: สีหลอด + ชนิดตัวอย่าง
- 🔴 Red (EDTA) - CBC, Hematology
- 🟢 Green (Plain/Gel) - Chemistry
- 🟡 Yellow (SST) - Immunology
- 🔵 Blue (Sodium Citrate) - Coagulation
- 🟣 Purple (Fluoride) - Glucose
- Turnaround Time: แสดงเป็นชั่วโมงหรือวัน
- Item Count: จำนวนรายการในแต่ละ Panel (เช่น "8 รายการ")
- Priority Buttons:
- Regular (เขียว) - ตรวจปกติ
- ⚡ STAT (แดง) - ด่วนพิเศษ (ถ้า isStatAvailable: true)
Interaction: - Click Card Area → Show Panel Details (รายการภายใน Panel) - Click [Regular] → Add ทั้ง Panel เป็น Regular priority - Click [⚡ STAT] → Add ทั้ง Panel เป็น STAT priority (พร้อม Alert "STAT Order ต้องระบุเหตุผล")
3.2 Individual Tests (Tab 2)
Category Filter: - ทั้งหมด (ALL): แสดงรายการทั้งหมด - Hematology: เลือด (CBC, ESR, Blood Film) - Chemistry: เคมี (FBS, LFT, RFT, Lipid Profile) - Immunology: ภูมิคุ้มกัน (Hepatitis Markers, Thyroid Function) - Microbiology: จุลชีววิทยา (Culture & Sensitivity) - Coagulation: การแข็งตัวของเลือด (PT, APTT, INR) - Urinalysis: ตรวจปัสสาวะ (UA, Urine C&S) - Serology: เซรุ่มวิทยา (VDRL, HIV, HBsAg)
Search Bar:
- Real-time search โดย filterLabItems(query)
- ค้นจาก: Item Name (EN), Item Name (TH), Item Code
- Case-insensitive
- แสดง "ไม่พบรายการตรวจที่ตรงกับเงื่อนไข" ถ้าค้นไม่เจอ
Lab Item Card Components:
- Item Name (EN): ชื่อรายการภาษาอังกฤษ
- Item Name (TH): ชื่อภาษาไทย
- Item Code: รหัสรายการ (เช่น "CBC001")
- Specimen Badge: สีหลอด + ชนิดตัวอย่าง
- STAT Badge: แสดง "⚡ STAT" ถ้ารองรับ
- Outsource Badge: แสดง "🔗 ส่งตรวจนอก" + ชื่อแล็บ (ถ้า isOutsourced: true)
- Turnaround Time: แสดงเป็นชั่วโมง/วัน
- Fasting Warning: "⚠️ ต้องงดอาหาร" (เช่น FBS, Lipid Profile)
- Priority Buttons: Regular / STAT
Selection State:
- Unselected: White background, Gray border
- Selected Regular: Light Green background (#d4edda)
- Selected STAT: Light Red background (#f8d7da)
- Active Button: Bold + Primary Color
Interaction:
- Click [Regular] → toggleLabItem(itemId, 'regular')
- If already selected → Remove from cart
- If not selected → Add to cart + Fetch pricing
- Click [⚡ STAT] → toggleLabItem(itemId, 'stat')
- Same logic as Regular, but set priority: 'stat'
Section 4: สรุปรายการสั่งตรวจ (Order Summary)
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ 📋 สรุปรายการสั่งตรวจ │
├────────────────────────────────────────────────────────────┤
│ Diagnosis (ICD-10): [____________________________] │
│ Clinical Note: [____________________________] │
│ (จำเป็นสำหรับ STAT Order) │
│ │
│ ┌─── Order Table ────────────────────────────────────┐ │
│ │ ลำดับ │ รายการ │ Priority │ ราคา │ เบิก │ จ่าย│ │
│ ├───────┼────────────┼──────────┼──────┼──────┼─────┤ │
│ │ 1 │ CBC │ STAT 🔴 │ 120 │ 100 │ 20 │ │
│ │ 2 │ FBS │ Regular │ 80 │ 80 │ 0 │ │
│ │ 3 │ LFT Panel │ Regular │ 450 │ 400 │ 50 │ │
│ └───────┴────────────┴──────────┴──────┴──────┴─────┘ │
│ │
│ ค่าใช้จ่ายรวม: 650.00 ฿ │
│ สิทธิ์เบิกได้: 580.00 ฿ │
│ ผู้ป่วยจ่าย: 70.00 ฿ 🔴 │
│ ──────────────────────────────────────────────────── │
│ รวมทั้งสิ้น: 650.00 ฿ │
│ │
│ [🗑️ ล้างรายการ] [💾 บันทึกคำสั่งตรวจ] │
└────────────────────────────────────────────────────────────┘
Components:
4.1 Diagnosis & Clinical Note - Diagnosis: ฟรีเทกซ์ หรือ ICD-10 Code (Future: Auto-complete) - Clinical Note: Required for STAT Order (Validation ตอนกด Save) - Placeholder: "ระบุเหตุผลการสั่ง STAT, อาการผู้ป่วย, หรือข้อมูลเพิ่มเติม" - Multi-line textarea (4 rows)
4.2 Order Items Table - Columns: 1. ลำดับ (Auto-number) 2. รายการตรวจ (Item Name TH + EN) 3. Priority (STAT Badge แดง, Regular Badge เขียว) 4. ราคาเต็ม (Unit Price) 5. สิทธิ์เบิก (Reimbursement Price) 6. ผู้ป่วยจ่าย (Patient Pay = Unit - Reimb) 7. ลบ (🗑️ Delete Button)
- Row Styling:
- STAT Order: Light Red background (
#ffe6e6) - Regular Order: White background
- Hover: Light Gray background
4.3 Price Breakdown
// Calculation Logic
totalPrice = sum(item.unitPrice)
insuranceCoverage = sum(item.reimbPrice)
patientPay = sum(item.patientPay)
grandTotal = totalPrice
- ค่าใช้จ่ายรวม: Total Unit Price (สีดำ)
- สิทธิ์เบิกได้: Total Reimbursement (สีเขียว)
- ผู้ป่วยจ่าย: Total Patient Pay (สีแดง, Bold ถ้า > 0)
- รวมทั้งสิ้น: Grand Total (สีน้ำเงิน, Bold, Large font)
4.4 Action Buttons
🗑️ ล้างรายการ (clearOrder())
- Confirmation: "ต้องการยกเลิกรายการสั่งตรวจทั้งหมด?"
- Action: Clear selectedItems[], Reset form, Reload Lab Items
💾 บันทึกคำสั่งตรวจ (saveOrder())
- Validations:
1. Patient selected? → "กรุณาเลือกผู้ป่วยก่อน"
2. Items > 0? → "กรุณาเลือกรายการตรวจอย่างน้อย 1 รายการ"
3. STAT Order + No Clinical Note? → "STAT Order ต้องระบุเหตุผล"
- Process:
- Determine overall priority: STAT > Urgent > Routine
- Create
orderDataobject:{ hn, patientName, vn, orderType, priority, doctorId, doctorName, department, diagnosis, clinicalNote, items[], insuranceRightId, insuranceType, createdBy, createdAt } - Call
labOrderService.createOrder(orderData) - Generate Order Number:
LAB-YYYYMMDD-XXXX(e.g., LAB-20240126-0001) - Save to LocalStorage (
his_lab_orders) - Show Success Alert: "บันทึกคำสั่งตรวจเรียบร้อยแล้ว เลขที่: LAB-20240126-0001"
- Prompt: "ต้องการพิมพ์ใบนำส่งแล็บหรือไม่?"
- Reset form → Reload page
🖨️ Print Lab Request (ใบนำส่งแล็บ)
Trigger: หลังจาก Save Order สำเร็จ (optional) หรือ Click "พิมพ์ใบนำส่ง" Button
Print Layout:
┌─────────────────────────────────────────────────────────────┐
│ โรงพยาบาลต้นแบบ HIS │
│ ใบนำส่งตรวจทางห้องปฏิบัติการ │
├─────────────────────────────────────────────────────────────┤
│ เลขที่: LAB-20240126-0001 วันที่: 26/12/2567 │
│ HN: HN000123 VN: VN20240126001 │
│ ชื่อ-สกุล: นายสมชาย ใจดี อายุ: 30 ปี │
│ สิทธิ์: บัตรทอง (UC) แพทย์: นพ.สมชาย แพทย์ดี │
├─────────────────────────────────────────────────────────────┤
│ Diagnosis: E11.9 - Diabetes Mellitus Type 2 │
│ Clinical Note: Follow up blood sugar level │
├─────────────────────────────────────────────────────────────┤
│ รายการตรวจ: │
│ ┌────┬──────────────────────┬────────────┬──────────────┐ │
│ │ ลำดับ │ รายการตรวจ │ Priority │ หลอด/ตัวอย่าง│ │
│ ├────┼──────────────────────┼────────────┼──────────────┤ │
│ │ 1 │ CBC (นับเม็ดเลือด) │ STAT 🔴 │ EDTA (สีม่วง)│ │
│ │ 2 │ FBS (น้ำตาล) │ Regular │ Fluoride │ │
│ │ 3 │ LFT Panel (ตรวจตับ) │ Regular │ Plain/Gel │ │
│ └────┴──────────────────────┴────────────┴──────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ หมายเหตุ: กรุณาเก็บตัวอย่างตามหลอดที่ระบุ │
│ STAT Order: ด่วน - ส่งผลภายใน 2 ชั่วโมง │
│ │
│ ลงชื่อแพทย์: _____________ วันที่: __/__/____ │
│ ลงชื่อผู้เก็บ: ____________ วันที่: __/__/____ │
└─────────────────────────────────────────────────────────────┘
Print Features:
- Window.open(): เปิด Print Preview ใน New Tab
- Print Styling: ใช้ @media print ซ่อนปุ่มที่ไม่ต้องการ
- Barcode (Future): QR Code สำหรับ Order Number
- Copy Count: พิมพ์ 2 ชุด (1 ชุดติดไปกับผู้ป่วย, 1 ชุดเก็บไว้ที่แล็บ)
⚙️ Technical Features
1. Data Loading (loadLabData())
// Fetch 3 JSON files in parallel
Promise.all([
fetch('/data/lab-items.json'), // ~200 items
fetch('/data/lab-panels.json'), // ~20 panels
fetch('/data/lab-pricing.json') // ~300 pricing rules
]);
2. Pricing Calculation (getItemPricing())
// Logic:
// 1. Get insuranceRightId from patient.insuranceType
// 2. Find pricing rule: itemCode + insuranceRightId + priority
// 3. Calculate:
// - unitPrice (ราคาเต็ม)
// - reimbPrice (สิทธิ์เบิก)
// - patientPay = unitPrice - reimbPrice
// Examples:
// UC (บัตรทอง): reimbPrice = 100%, patientPay = 0
// Social Security: reimbPrice = 80%, patientPay = 20%
// Cash: reimbPrice = 0%, patientPay = 100%
3. Panel Item Expansion (selectPanel())
// When click Panel Card:
// 1. Find panel by ID
// 2. Get all items in panel.items[]
// 3. Add each item to selectedItems[] with same priority
// 4. Fetch pricing for each item
// 5. Update Order Summary
4. LocalStorage Keys
'his_lab_orders' // Array of Lab Orders
'his_lab_order_counter' // Counter for Order Number
'his_patients' // Patient data (shared)
🎨 Design Patterns
Color Coding by Priority
- STAT: Red (#dc3545) - ด่วนพิเศษ
- Urgent: Orange (#ffc107) - ด่วน
- Routine: Green (#28a745) - ปกติ
Specimen Container Colors
- 🔴 Purple/Red (EDTA) - Hematology
- 🟢 Green (Heparin/Plain) - Chemistry
- 🟡 Yellow (SST) - Immunology
- 🔵 Blue (Citrate) - Coagulation
- 🟣 Gray (Fluoride) - Glucose
Badge System
- Panel Badge: Purple background, "PANEL" text
- STAT Badge: Red background, "⚡ STAT" text
- Outsource Badge: Blue background, "🔗 ส่งตรวจนอก"
- Fasting Badge: Orange background, "⚠️ ต้องงดอาหาร"
📱 Responsive Behavior
Desktop (> 1200px)
- Lab Items Grid: 3-4 columns
- Patient Info: 2 columns
- Full-width Order Summary Table
Tablet (768px - 1200px)
- Lab Items Grid: 2 columns
- Patient Info: 2 columns
- Scrollable Order Table (horizontal)
Mobile (< 768px)
- Lab Items Grid: 1 column (stacked)
- Patient Info: 1 column
- Order Table: Simplified cards instead of table
- Priority Buttons: Stack vertically
💡 Best Practices (UX/UI)
- Visual Feedback:
- Selected items มี Border Green + Background Light Green
- STAT items มี Border Red + Red Badge
-
Price แสดงเป็น Bold ถ้า Patient Pay > 0
-
Error Prevention:
- Disable Save Button ถ้า No Items Selected
- Show Warning Alert ก่อน Clear Order
-
Validate STAT Order must have Clinical Note
-
Performance:
- Load Lab Items แบบ Lazy (แยก Tab)
- ใช้ Grid Layout แทน Flexbox (faster rendering)
-
Debounce Search Input (300ms delay)
-
Accessibility:
- Label ชัดเจนทุก Input
- Tab Order ถูกต้อง (HN → Search → Items → Save)
- Color Contrast ratio > 4.5:1
🐛 Known Issues & Limitations
- No VN (Visit Number): ใช้
vn: nullชั่วคราว (Future: Get from OPD Visit) - Mock Doctor: ใช้
DOC001hardcode (Future: Get from Session/Login) - No ICD-10 Auto-complete: ต้องพิมพ์ Diagnosis เอง
- No Lab Item Image: ไม่มีรูปภาพ Specimen Container
- Print Format Fixed: ไม่สามารถแก้ไข Layout ใบนำส่งได้
📊 Business Rules
- STAT Order Requirements:
- Must have Clinical Note (Reason for urgency)
- STAT Price = Regular Price × 1.5 (or custom pricing)
-
STAT Turnaround Time = Regular TAT / 2
-
Pricing Rules:
- UC (Universal Coverage): 100% covered (Patient Pay = 0)
- Social Security: 80% covered (Patient Pay = 20%)
- Government: 100% covered
- Private Insurance: Variable coverage
-
Cash: 0% covered (Patient Pay = 100%)
-
Panel vs Individual:
- Panel = Grouped tests (cheaper bundle price)
- Individual = À la carte (full price per item)
- Panels cannot be partially selected (All or None)
3. order-queue.html - จัดการคิว Order แล็บ
📌 ภาพรวม
หน้าสำหรับเจ้าหน้าที่แล็บจัดการคิว Order ที่แพทย์สั่งเข้ามา ทำหน้าที่เป็น Gateway ก่อนเข้าสู่กระบวนการเก็บสิ่งส่งตรวจ ตรวจสอบความถูกต้อง ยืนยัน หรือปฏิเสธ Order
🎯 วัตถุประสงค์
- ตรวจสอบ Order: ตรวจสอบความถูกต้องของ Order (ข้อมูลครบถ้วน, สิทธิ์ครอบคลุม)
- จัดการสถานะ: ยืนยันหรือปฏิเสธ Order
- ส่ง LIS: ส่ง Order เข้าระบบ LIS (Laboratory Information System) เพื่อเริ่มกระบวนการตรวจ
- ติดตาม: ติดตามสถานะ Order แบบ Real-time
📊 File Statistics
- Total Lines: 1,313 lines
- Components: Statistics Cards, Filter Section, Order Table, 3 Modals
- Dependencies: LabOrder.js, Patient.js, LabOrderService, PatientService, SpecimenService
- Data Files: lab-items.json (ตรวจสอบ outsourced items)
🧩 UI Components & Workflow
Section 1: Statistics Dashboard
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ [รอยืนยัน: 5] [ยืนยันแล้ว: 12] [STAT: 3] [กำลังเก็บ: 8] │
│ [กำลังตรวจ: 15] [ปฏิเสธ: 2] [เสร็จสิ้น: 45] │
└────────────────────────────────────────────────────────────┘
Statistics Cards:
1. รอยืนยัน (Pending) - สีส้ม
- ไอคอน: fas fa-clock
- Count: Orders with status = 'pending'
- Click → Auto-filter status = pending
- ยืนยันแล้ว (Confirmed) - สีฟ้า
- ไอคอน:
fas fa-check -
Count: Orders with status = 'confirmed'
-
STAT Orders - สีแดง
- ไอคอน:
fas fa-bolt - Count: Orders with priority = 'stat'
-
Special Alert: แสดงเลขโดดเด่น
-
กำลังเก็บสิ่งส่งตรวจ (Collecting) - สีม่วง
- ไอคอน:
fas fa-vial -
Count: Orders with status = 'collecting'
-
กำลังตรวจ (In Progress) - สีน้ำเงิน
- ไอคอน:
fas fa-flask -
Count: Orders with status = 'in_progress'
-
ปฏิเสธ (Rejected) - สีเทา
- ไอคอน:
fas fa-times -
Count: Orders with status = 'rejected'
-
เสร็จสิ้น (Completed) - สีเขียว
- ไอคอน:
fas fa-check-double - Count: Orders with status = 'completed'
Calculation Logic:
// labOrderService.getStatistics()
{
pending: orders.filter(o => o.status === 'pending').length,
confirmed: orders.filter(o => o.status === 'confirmed').length,
stat: orders.filter(o => o.priority === 'stat').length,
collecting: orders.filter(o => o.status === 'collecting').length,
in_progress: orders.filter(o => o.status === 'in_progress').length,
rejected: orders.filter(o => o.status === 'rejected').length,
completed: orders.filter(o => o.status === 'completed').length
}
Styling:
- Card Border Left: 4px solid (สีตามประเภท)
- Card Shadow: 0 2px 4px rgba(0,0,0,0.1)
- Hover Effect: Lift + Cursor pointer
- Number Font: 2rem, Bold, Color coded
Section 2: Filter & Search
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ Status: [ทั้งหมด ▼] Priority: [ทั้งหมด ▼] │
│ Category: [ทั้งหมด ▼] Search: [___________] [🔍 ค้นหา] │
│ [🔄 รีเซ็ต] │
└────────────────────────────────────────────────────────────┘
Filter Options:
2.1 Status Filter - ทั้งหมด (Default - แสดงทุกสถานะ) - รอยืนยัน (pending) - ยืนยันแล้ว (confirmed) - กำลังเก็บสิ่งส่งตรวจ (collecting) - เก็บสิ่งส่งตรวจแล้ว (specimen_collected) - ส่ง LIS แล้ว (sent_to_lis) - กำลังตรวจ (in_progress) - ผลบางส่วน (partial_result) - เสร็จสิ้น (completed) - ยกเลิก (cancelled) - ปฏิเสธ (rejected)
2.2 Priority Filter - ทั้งหมด - STAT (stat) - ด่วนพิเศษ - Urgent (urgent) - ด่วน - Routine (routine) - ปกติ
2.3 Category Filter - ทั้งหมด - Hematology (เลือด) - Chemistry (เคมี) - Immunology (ภูมิคุ้มกัน) - Microbiology (จุลชีววิทยา) - Coagulation (การแข็งตัวของเลือด) - Urinalysis (ปัสสาวะ) - Serology (เซรุ่มวิทยา)
2.4 Search Box - ค้นหาจาก: Order Number, HN - Real-time search (กด Enter หรือ Click ปุ่มค้นหา) - Case-insensitive
Filter Logic (applyFilters()):
// 1. Start with all orders
let filtered = labOrderService.getAllOrders();
// 2. Apply status filter
if (status) filtered = filtered.filter(o => o.status === status);
// 3. Apply priority filter
if (priority) filtered = filtered.filter(o => o.priority === priority);
// 4. Apply category filter (check items)
if (category) {
filtered = filtered.filter(o =>
o.items.some(item => item.category === category)
);
}
// 5. Apply search
if (search) {
filtered = filtered.filter(o =>
o.orderNumber.includes(search) || o.hn.includes(search)
);
}
[🔄 รีเซ็ต] Button: - Clear all filters - Reset dropdown to default - Reload full order list
Section 3: Order List Table
UI Layout:
┌────────────────────────────────────────────────────────────────────────────────────┐
│ แสดงผล: 15 รายการ │
├──────┬────────┬─────────────────┬──────────┬────────┬─────────┬─────────┬─────────┤
│ Order│ HN │ ผู้ป่วย │ Priority │ จำนวน │ วันที่ │ แพทย์ │ สถานะ │Actions│
├──────┼────────┼─────────────────┼──────────┼────────┼─────────┼─────────┼─────────┤
│ LAB- │HN00001 │ นายสมชาย ใจดี │ ⚡STAT │ 3 รายการ│ 26/12/67│ นพ.สมชาย│ รอยืนยัน│[ยืนยัน]│
│ 2024 │ │ │ │ 🔗 มีส่ง│ 14:30 │ │ │[ปฏิเสธ]│
│ 1226-│ │ │ │ ตรวจนอก│ │ │ │ │
│ 0001 │ │ │ │ │ │ │ │ │
├──────┼────────┼─────────────────┼──────────┼────────┼─────────┼─────────┼─────────┤
│ LAB- │HN00002 │ นางสมศรี ดีมาก │ Regular │ 5 รายการ│ 26/12/67│ นพ.วิชัย│ ยืนยัน │[ส่ง LIS]│
│ 2024 │ │ │ │ │ 13:15 │ │ แล้ว │[ดู] │
│ 1226-│ │ │ │ │ │ │ │ │
│ 0002 │ │ │ │ │ │ │ │ │
└──────┴────────┴─────────────────┴──────────┴────────┴─────────┴─────────┴─────────┘
Table Columns:
- Order No (Order Number)
- Format:
LAB-YYYYMMDD-XXXX - Bold text, Primary color
-
Click → Open Detail Modal
-
HN (Hospital Number)
- Format:
HN000001 -
Link to Patient Record (Future)
-
ผู้ป่วย (Patient Name)
- Format: "title firstName lastName"
-
Get from
PatientService.getPatientByHN(hn) -
Priority
-
Badge Display:
- ⚡ STAT (สีแดง) - ด่วนพิเศษ
- 🔥 Urgent (สีส้ม) - ด่วน
- ✅ Routine (สีเขียว) - ปกติ
-
จำนวนรายการ (Item Count)
- Format: "X รายการ"
- Show Badge "🔗 มีส่งตรวจนอก" ถ้ามี outsourced items
-
Check Logic:
order.items.some(item => { const labItem = labItems.find(li => li.id === item.itemId); return labItem?.isOutsourced === true; }) -
วันที่สั่ง (Order Date)
- Format:
DD/MM/YY HH:MM(Thai Buddhist Era) -
Color: Gray (muted)
-
แพทย์ผู้สั่ง (Doctor Name)
- Format: "นพ./พญ. ชื่อ นามสกุล"
-
Default: "ไม่ระบุ"
-
สถานะ (Status Badge)
-
Color-coded badges (11 statuses):
- 🟠 รอยืนยัน (pending) - Orange
- 🔵 ยืนยันแล้ว (confirmed) - Blue
- 🟣 กำลังเก็บ (collecting) - Purple
- 🟢 เก็บแล้ว (specimen_collected) - Green
- 🔷 ส่ง LIS (sent_to_lis) - Cyan
- 🔵 กำลังตรวจ (in_progress) - Light Blue
- 🟡 ผลบางส่วน (partial_result) - Yellow
- ✅ เสร็จสิ้น (completed) - Dark Green
- 🔴 ปฏิเสธ (rejected) - Red
- ⚫ ยกเลิก (cancelled) - Gray
-
Actions (Action Buttons)
- Dynamic Buttons ตามสถานะ:
Status = 'pending': - [✅ ยืนยัน] (btn-confirm, Green) → Show Confirm Modal - [❌ ปฏิเสธ] (btn-reject, Red) → Show Reject Modal
Status = 'confirmed' or 'specimen_collected': - [📤 ส่ง LIS] (btn-send-lis, Blue) → Send to LIS function
Status = 'sent_to_lis' or 'completed': - [🧪 ดูผล] (btn-view, Green) → View Results (Future)
All Statuses: - [👁️ ดู] (btn-view, Gray) → Open Detail Modal
Row Styling:
- Hover: Light Gray background (#f8f9fa)
- STAT Order: Light Red background (#ffebee)
- Rejected Order: Light Gray background + Red border left
- Click → viewOrderDetail(orderNumber)
Empty State:
┌────────────────────────────────────┐
│ 📥 │
│ ไม่พบรายการ Order │
│ ไม่มีรายการที่ตรงกับเงื่อนไข │
└────────────────────────────────────┘
Section 4: Detail Modal (รายละเอียด Order)
Trigger: Click Order Row หรือ Click [👁️ ดู] Button
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 📋 รายละเอียด Order [✕] │
├─────────────────────────────────────────────────────────────┤
│ ข้อมูล Order │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Order Number: LAB-20241226-0001 │ │
│ │ สถานะ: [รอยืนยัน 🟠] Priority: [⚡STAT 🔴] │ │
│ │ วันที่สั่ง: 26/12/2567 14:30:15 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ข้อมูลผู้ป่วย │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ HN: HN000123 ชื่อ: นายสมชาย ใจดี │ │
│ │ อายุ: 30 ปี เพศ: ชาย │ │
│ │ สิทธิ์: บัตรทอง (UC) แพ้ยา: Penicillin │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ข้อมูลแพทย์ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ แพทย์: นพ.สมชาย แพทย์ดี แผนก: OPD │ │
│ │ Diagnosis: E11.9 - Diabetes Mellitus Type 2 │ │
│ │ Clinical Note: Follow up blood sugar level │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ รายการแล็บ (3 รายการ) │
│ ┌────┬─────────────────┬────────┬────────────┬─────────┐ │
│ │ลำดับ│ รายการ │หมวดหมู่│ สิ่งส่งตรวจ│ ราคา │ │
│ ├────┼─────────────────┼────────┼────────────┼─────────┤ │
│ │ 1 │ CBC (นับเม็ดเลือด)│Hemato │ EDTA │ 120.00 ฿│ │
│ │ 2 │ FBS (น้ำตาล) │Chemis │ Fluoride │ 80.00 ฿│ │
│ │ 3 │ HbA1c │Chemis │ EDTA │ 250.00 ฿│ │
│ │ │ 🔗 ส่งตรวจนอก │ │ │ │ │
│ │ │ (Bangkok Lab) │ │ │ │ │
│ └────┴─────────────────┴────────┴────────────┴─────────┘ │
│ │
│ ราคารวม: 450.00 บาท │
│ │
│ [ปิด] │
└─────────────────────────────────────────────────────────────┘
Modal Sections:
4.1 Order Information - Order Number (Unique ID) - Status Badge (Color-coded) - Priority Badge - Order Date & Time (Thai format)
4.2 Patient Information - HN, Full Name - Age, Gender - Insurance Type - Allergies (Alert ถ้ามี)
4.3 Doctor Information - Doctor Name, Department - Diagnosis (ICD-10 code + description) - Clinical Note (Reason for order)
4.4 Lab Items Table - Columns: ลำดับ, รายการ, หมวดหมู่, สิ่งส่งตรวจ, ราคา - Show Badge "🔗 ส่งตรวจนอก (Lab Name)" for outsourced items - Total Price (Bold, Large font)
Modal Actions: - [✕] Close Button (Top-right corner) - [ปิด] Button (Footer) - Click outside → Close modal
Technical:
// viewOrderDetail(orderNumber)
// 1. Get order from service
const order = labOrderService.getOrderByNumber(orderNumber);
// 2. Get patient data
const patient = patientService.getPatientByHN(order.hn);
// 3. Enrich items with lab item details
order.items.forEach(item => {
const labItem = labItems.find(li => li.id === item.itemId);
item.isOutsourced = labItem?.isOutsourced;
item.outsourceLabName = labItem?.outsourceLabName;
});
// 4. Render modal HTML
// 5. Show modal: document.getElementById('detail-modal').classList.add('active');
Section 5: Confirm Modal (ยืนยัน Order)
Trigger: Click [✅ ยืนยัน] Button (เมื่อ status = 'pending')
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ✅ ยืนยัน Order [✕] │
├─────────────────────────────────────────────────────────────┤
│ คุณต้องการยืนยัน Order นี้หรือไม่? │
│ │
│ Order: LAB-20241226-0001 │
│ ผู้ป่วย: นายสมชาย ใจดี │
│ รายการ: 3 รายการ │
│ │
│ หมายเหตุ (ถ้ามี): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [ยกเลิก] [✅ ยืนยัน] │
└─────────────────────────────────────────────────────────────┘
Form Fields: - Order Number (Display only) - Patient Name (Display only) - Items Count (Display only) - หมายเหตุ (Optional textarea) - Placeholder: "ระบุหมายเหตุเพิ่มเติม..." - Multi-line (3 rows)
Actions:
- [ยกเลิก] → Close modal, No changes
- [✅ ยืนยัน] → Call confirmOrder():
// 1. Get note from textarea
const note = document.getElementById('confirm-note').value;
// 2. Update order status
labOrderService.updateOrderStatus(
orderNumber,
'confirmed',
note || 'ยืนยัน Order โดยเจ้าหน้าที่แล็บ',
currentUser.id,
currentUser.name
);
// 3. Show success message
showSuccess('ยืนยัน Order สำเร็จ');
// 4. Close modal
// 5. Reload order list
Status Transition:
- Before: status: 'pending'
- After: status: 'confirmed'
- History Log: บันทึก Action, User, Timestamp ใน statusHistory[]
Section 6: Reject Modal (ปฏิเสธ Order)
Trigger: Click [❌ ปฏิเสธ] Button (เมื่อ status = 'pending')
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ❌ ปฏิเสธ Order [✕] │
├─────────────────────────────────────────────────────────────┤
│ คุณต้องการปฏิเสธ Order นี้หรือไม่? │
│ │
│ Order: LAB-20241226-0001 │
│ ผู้ป่วย: นายสมชาย ใจดี │
│ │
│ เหตุผล: * (Required) │
│ [-- เลือกเหตุผล -- ▼] │
│ - ข้อมูลไม่ครบถ้วน │
│ - รายการซ้ำ │
│ - รายการไม่ครอบคลุมในสิทธิ์ │
│ - ผู้ป่วยไม่พร้อม │
│ - อื่นๆ │
│ │
│ หมายเหตุเพิ่มเติม: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [ยกเลิก] [❌ ปฏิเสธ Order] │
└─────────────────────────────────────────────────────────────┘
Form Fields:
6.1 เหตุผล (Reason) - Required Dropdown - ข้อมูลไม่ครบถ้วน: Patient info incomplete, Missing diagnosis - รายการซ้ำ: Duplicate order already exists - รายการไม่ครอบคลุมในสิทธิ์: Insurance doesn't cover items - ผู้ป่วยไม่พร้อม: Patient not available, Not fasting - อื่นๆ: Other reasons (must specify in Note)
6.2 หมายเหตุเพิ่มเติม (Additional Note) - Optional - Textarea (4 rows) - Placeholder: "ระบุรายละเอียดเพิ่มเติม..." - Required ถ้าเลือก "อื่นๆ"
Validation:
// rejectOrder()
// 1. Check reason selected
if (!reason) {
showError('กรุณาเลือกเหตุผลในการปฏิเสธ');
return;
}
// 2. Combine reason + note
const fullNote = `${reason}${note ? ': ' + note : ''}`;
// 3. Update status
labOrderService.updateOrderStatus(
orderNumber,
'rejected',
fullNote,
currentUser.id,
currentUser.name
);
Status Transition:
- Before: status: 'pending'
- After: status: 'rejected'
- Note: บันทึกเหตุผลใน statusHistory[]
Actions:
- [ยกเลิก] → Close modal, No changes
- [❌ ปฏิเสธ Order] → Call rejectOrder():
- Validate reason selected
- Update status to 'rejected'
- Show success message
- Close modal
- Reload list (Rejected orders move to separate tab)
⚙️ Technical Features
1. Auto-Create Mock Orders
// On page load (DOMContentLoaded)
// 1. Check if orders exist in LocalStorage
const existingOrders = labOrderService.getAllOrders();
// 2. If empty, create mock orders
if (existingOrders.length === 0) {
await labOrderService.createMockOrders();
}
// Mock orders include:
// - Various statuses (pending, confirmed, collecting, etc.)
// - Various priorities (stat, urgent, routine)
// - Multiple patients (HN000001, HN000002, etc.)
// - Different lab categories
// - Some with outsourced items
2. Real-time Statistics Update
// updateStatistics()
// Called after every status change
// Recalculate counts from current orders
const stats = labOrderService.getStatistics();
// Update DOM
document.getElementById('stat-pending').textContent = stats.pending;
document.getElementById('stat-confirmed').textContent = stats.confirmed;
// ... etc
3. Send to LIS (Simulation)
// sendToLIS(orderNumber)
// In real implementation:
// - Call external LIS API
// - Send order data + patient info
// - Receive LIS order number
// - Update order with LIS ID
// Current simulation:
// - Just change status to 'sent_to_lis'
// - Log action in statusHistory
// - Show success alert
4. LocalStorage Keys
'his_lab_orders' // Array of Lab Orders
'his_patients' // Patient data (shared)
'his_lab_order_counter' // Auto-increment counter
🎨 Design System
Status Color Mapping
/* Pending Phase */
.pending { background: #fff3e0; color: #ef6c00; } /* Orange */
.confirmed { background: #e3f2fd; color: #1976d2; } /* Blue */
.rejected { background: #ffebee; color: #c62828; } /* Red */
/* Collection Phase */
.collecting { background: #f3e5f5; color: #6a1b9a; } /* Purple */
.specimen_collected { background: #e8f5e9; color: #2e7d32; } /* Green */
/* Processing Phase */
.sent_to_lis { background: #e0f7fa; color: #00838f; } /* Cyan */
.in_progress { background: #e1f5fe; color: #0277bd; } /* Light Blue */
.partial_result { background: #fff3cd; color: #856404; } /* Yellow */
/* Final Phase */
.completed { background: #c8e6c9; color: #1b5e20; } /* Dark Green */
.cancelled { background: #e0e0e0; color: #424242; } /* Gray */
Action Button Colors
.btn-confirm { background: #4caf50; } /* Green */
.btn-reject { background: #f44336; } /* Red */
.btn-send-lis { background: #2196f3; } /* Blue */
.btn-view { background: #9e9e9e; } /* Gray */
📱 Responsive Behavior
Desktop (> 1200px)
- Statistics: 7 cards in 1 row
- Filter: 4 filters in 1 row
- Table: Full width, All columns visible
Tablet (768px - 1200px)
- Statistics: 3-4 cards per row
- Filter: 2 filters per row
- Table: Horizontal scroll for overflow
Mobile (< 768px)
- Statistics: 2 cards per row (stacked)
- Filter: 1 filter per row (stacked)
- Table: Convert to Card List view
┌──────────────────────────────┐ │ LAB-20241226-0001 [⚡STAT] │ │ นายสมชาย ใจดี (HN000123) │ │ 3 รายการ | 26/12/67 14:30 │ │ สถานะ: [รอยืนยัน] │ │ [ยืนยัน] [ปฏิเสธ] [ดู] │ └──────────────────────────────┘
💡 Best Practices (UX/UI)
- Visual Priority:
- STAT Orders → Red background + Bold
- Rejected Orders → Gray background + Red border left
-
Confirmed Orders → Light Blue background
-
Clear Action Hierarchy:
- Primary Action (ยืนยัน): Green, Large
- Dangerous Action (ปฏิเสธ): Red, Same size as Primary
-
Secondary Action (ดู): Gray, Smaller
-
Error Prevention:
- Confirm Modal: Extra confirmation step
- Reject Modal: Required reason dropdown
-
STAT Alert: Visual indicator ⚡ สีแดง
-
Feedback:
- Success Alert: Green toast (Top-right, 3s auto-dismiss)
- Error Alert: Red toast (Top-right, 3s auto-dismiss)
- Loading State: Spinner while processing
🐛 Known Issues & Limitations
- No Real LIS Integration: Send to LIS is simulated (just changes status)
- Mock User:
currentUseris hardcoded (Future: Get from session) - No Notification: Rejected orders don't notify doctor (Future: Add notification system)
- No Undo: Cannot undo Confirm/Reject action (Future: Add undo within 30 seconds)
- No Batch Operations: Cannot confirm/reject multiple orders at once
📊 Business Rules
- Status Transition Rules:
- Pending → Can go to: Confirmed, Rejected
- Confirmed → Can go to: Collecting, Sent to LIS
-
Rejected → Cannot change (Final status)
-
Permission Rules (Future):
- Lab Staff: Can confirm/reject pending orders
- Lab Supervisor: Can override rejected orders
-
Doctor: Cannot modify after submission (must contact lab)
-
STAT Order Priority:
- STAT orders แสดงที่ด้านบนสุดเสมอ (Auto-sort by priority)
-
STAT Alert: แสดงเลขโดดเด่นใน Statistics Card
-
Outsourced Items:
- Show Badge "🔗 มีส่งตรวจนอก"
- Extended TAT (Turnaround Time)
- Cannot send to internal LIS (must handle separately)
4. specimen-collection.html - เก็บสิ่งส่งตรวจ
📌 ภาพรวม
หน้าสำหรับเจ้าหน้าที่เจาะเลือด (Phlebotomist) เก็บสิ่งส่งตรวจจากผู้ป่วย พร้อมระบบ Quality Control (QC) ตรวจสอบคุณภาพตัวอย่าง และสร้าง Specimen Number อัตโนมัติพร้อม Print Label
🎯 วัตถุประสงค์
- เก็บสิ่งส่งตรวจ: บันทึกการเก็บตัวอย่าง (Blood, Urine, etc.)
- Quality Assessment: ประเมินคุณภาพตัวอย่าง (Good, Acceptable, Poor, Rejected)
- Generate Specimen Number: สร้างเลขสิ่งส่งตรวจอัตโนมัติ (Format:
SPEC-YYYYMMDD-XXXX) - Print Label: พิมพ์ฉลากติดหลอดตัวอย่าง (Barcode + Patient Info)
- Recollection: เก็บใหม่สำหรับตัวอย่างที่ถูกปฏิเสธ
📊 File Statistics
- Total Lines: 2,417 lines
- Components: Search Section, Order List, Collection Modal, Success Modal, Toast Notifications
- Dependencies: LabOrder.js, Patient.js, Specimen.js, LabOrderService, PatientService, SpecimenService
- Data Files: lab-orders-specimen.json, patients.json
🧩 UI Components & Workflow
Section 1: Search Section
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ 🔍 ค้นหา Order / ผู้ป่วย │
├────────────────────────────────────────────────────────────┤
│ [____________________________________________] [🔍 ค้นหา] │
│ Scan Barcode หรือพิมพ์ HN / Order Number / ชื่อผู้ป่วย │
│ │
│ 💡 สามารถ Scan Barcode จากใบนำส่งแล็บได้โดยตรง │
└────────────────────────────────────────────────────────────┘
คุณสมบัติ: 1. Multi-Search Support - Barcode Scan: รับค่าจาก Barcode Scanner (Order Number) - HN: ค้นหาจากเลข HN (HN000001) - Order Number: ค้นหาจาก Order Number (LAB-20241226-0001) - Patient Name: ค้นหาจากชื่อผู้ป่วย (Thai/English)
-
Search Logic (
searchOrder())// Filter orders by: // - order.orderNumber.includes(searchText) // - order.hn.includes(searchText) // - order.vn?.includes(searchText) // - order.patientName?.includes(searchText) -
Auto-Focus: Cursor ใน Search Box เพื่อรอ Scan Barcode
Section 2: Order List (รายการ Order ทั้งหมด)
Filter Logic: - แสดงเฉพาะ Orders ที่ status = 'confirmed', 'collecting', 'specimen_collected' - ไม่แสดง 'pending', 'rejected', 'completed' - เรียงตาม Priority (STAT → Urgent → Routine) → วันที่ (ใหม่ล่าสุด)
UI Layout:
┌────────────────────────────────────────────────────────────┐
│ 📋 รายการ Order ทั้งหมด (15 รายการ) │
├────────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────┐ │
│ │ Order: LAB-20241226-0001 [⚡STAT] │ │
│ │ HN: HN000123 | ผู้ป่วย: นายสมชาย ใจดี │ │
│ │ VN: VN20241226001 | แผนก: OPD │ │
│ │ วันที่: 26/12/2567 14:30 | แพทย์: นพ.สมชาย แพทย์ดี│ │
│ │ │ │
│ │ สถานะ: [ยืนยันแล้ว - พร้อมเก็บสิ่งส่งตรวจ] │ │
│ │ │ │
│ │ 🧪 รายการตรวจ (เลือกรายการที่ต้องการเก็บ): │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ [✓] # │ รายการ │ Specimen │ Container │สถานะ││ │
│ │ ├───────────────────────────────────────────────┤ │ │
│ │ │ [✓] 1 │ CBC │ Blood │ EDTA │รอเก็บ││ │
│ │ │ [✓] 2 │ FBS │ Blood │ Fluoride │รอเก็บ││ │
│ │ │ [✓] 3 │ HbA1c │ Blood │ EDTA │รอเก็บ││ │
│ │ └───────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ รายการที่เลือก: 3/3 รายการ │ │
│ │ [เก็บสิ่งส่งตรวจที่เลือก] [ดูรายละเอียด] [พิมพ์ Label] │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Order Card Components:
2.1 Order Header
- Order Number: LAB-YYYYMMDD-XXXX (Bold, Primary Color)
- Priority Badge:
- ⚡ STAT (Red) - ด่วนพิเศษ
- 🔥 Urgent (Orange) - ด่วน
- ✅ Routine (Green) - ปกติ
2.2 Patient Information - HN: Hospital Number - Patient Name: Title + First Name + Last Name - VN: Visit Number - Department: OPD/IPD
2.3 Order Details - Order Date: วันที่แพทย์สั่ง Order - Doctor Name: แพทย์ผู้สั่ง Order
2.4 Status Display - ยืนยันแล้ว - พร้อมเก็บสิ่งส่งตรวจ (confirmed) - สีฟ้า - กำลังเก็บสิ่งส่งตรวจ (บางส่วน) (collecting) - สีม่วง - เก็บสิ่งส่งตรวจแล้ว (specimen_collected) - สีเขียว
2.5 Item Selection Table
Columns: 1. Checkbox: เลือกรายการที่ต้องการเก็บ - Select All Checkbox (Header) - Individual Checkbox (แต่ละ item) 2. #: ลำดับรายการ 3. รายการ: Item Name (Thai/English) 4. Specimen Type: Blood, Urine, Stool, etc. 5. Container: EDTA (Purple), Plain (Red), Fluoride (Gray), etc. 6. สถานะ: - 🟡 รอเก็บ (pending_collection) - Yellow - 🟢 เก็บแล้ว (collected) - Green - 🔴 ปฏิเสธ (rejected) - Red + [🔄 เก็บใหม่] Button
Container Color Display:
// getContainerDisplay(containerType)
{
'edta': 'EDTA (Purple)', // 🟣 CBC, Hematology
'plain': 'Plain (Red)', // 🔴 Chemistry
'heparin': 'Heparin (Green)', // 🟢 Blood Gas
'citrate': 'Citrate (Blue)', // 🔵 Coagulation
'fluoride': 'Fluoride (Gray)', // ⚪ Glucose
'urine_container': 'Urine Container' // 🟡 Urinalysis
}
Selection Logic: - Cannot Select: Items ที่ status = 'collected' (เก็บแล้ว) - Can Select: Items ที่ status = 'pending_collection' (รอเก็บ) - Special: Items ที่ status = 'rejected' → แสดง [🔄 เก็บใหม่] Button แทน Checkbox
Item Count Display:
รายการที่เลือก: 3/5 รายการ
2.6 Action Buttons
[เก็บสิ่งส่งตรวจที่เลือก] (Primary Button - Green) - Enabled: เมื่อมี Checkbox ถูกเลือก (>= 1 item) - Disabled: เมื่อไม่มี Checkbox ถูกเลือก - Action: เปิด Collection Modal
[👁️ ดูรายละเอียด] (Secondary Button - Blue) - แสดง Order Detail Modal (Patient Info, Doctor, All Items, Clinical Note)
[🖨️ พิมพ์ Label] (Tertiary Button - Purple) - พิมพ์ฉลากติดหลอดตัวอย่าง (ล่วงหน้า หรือ พิมพ์ซ้ำ) - Support: Print multiple labels (1 label per item)
Section 3: Collection Modal (บันทึกการเก็บสิ่งส่งตรวจ)
Trigger: Click [เก็บสิ่งส่งตรวจที่เลือก] Button
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 🩸 บันทึกการเก็บสิ่งส่งตรวจ [✕] │
├─────────────────────────────────────────────────────────────┤
│ ℹ️ Order Number: LAB-20241226-0001 │
│ HN: HN000123 | ผู้ป่วย: นายสมชาย ใจดี │
│ จำนวนรายการที่เลือก: 3 รายการ │
├─────────────────────────────────────────────────────────────┤
│ 🧪 รายการที่เลือกเก็บ: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. CBC (นับเม็ดเลือด) (Blood - EDTA (Purple)) │ │
│ │ 2. FBS (น้ำตาลในเลือด) (Blood - Fluoride (Gray)) │ │
│ │ 3. HbA1c (Hemoglobin A1c) (Blood - EDTA (Purple)) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 🔬 ปริมาตร (ml): * [_______] 📅 เวลาที่เก็บ: * [______] │
│ │
│ ✅ คุณภาพสิ่งส่งตรวจ: * │
│ [Good (ดี) ▼] │
│ - Good (ดี) │
│ - Acceptable (พอใช้ได้) │
│ - Poor (ไม่ดี) │
│ - Rejected (ปฏิเสธ) │
│ │
│ ⚠️ เหตุผล/ปัญหาที่พบ: * (แสดงเมื่อเลือก Poor/Rejected) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Hemolysis รุนแรง, เลือดแข็งตัว, ปริมาตรไม่พอ... │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [ยกเลิก] [💾 บันทึกการเก็บสิ่งส่งตรวจ] │
└─────────────────────────────────────────────────────────────┘
Form Fields:
3.1 Order Information (Display Only) - Order Number - HN + Patient Name - Selected Item Count
3.2 Selected Items List - แสดงรายการที่เลือก (Read-only) - Format: "#{index}. {ItemName} ({SpecimenType} - {Container})"
3.3 ปริมาตร (Volume) - Required - Type: Number input (step: 0.1) - Unit: ml (milliliter) - Example: 3.0, 5.5, 10.0 - Validation: Min = 0, Required
3.4 เวลาที่เก็บ (Collected At) - Required
- Type: datetime-local input
- Default: Current date & time
- Format: YYYY-MM-DD HH:MM
- Auto-fill: ตั้งค่าเวลาปัจจุบันเมื่อเปิด Modal
3.5 คุณภาพสิ่งส่งตรวจ (Quality Assessment) - Required
Quality Options: 1. Good (ดี) - สีเขียว - ตัวอย่างมีคุณภาพดี ไม่มีปัญหา - ไม่ต้องระบุเหตุผล
- Acceptable (พอใช้ได้) - สีเหลือง
- ตัวอย่างมีปัญหาเล็กน้อย แต่ยังตรวจได้
-
ต้องระบุเหตุผล (เช่น Slight Hemolysis, Lipemia เล็กน้อย)
-
Poor (ไม่ดี) - สีส้ม
- ตัวอย่างมีปัญหามาก อาจส่งผลต่อความแม่นยำ
-
ต้องระบุเหตุผล (เช่น Moderate Hemolysis, Clotted)
-
Rejected (ปฏิเสธ) - สีแดง
- ตัวอย่างไม่ผ่านเกณฑ์ QC ต้องเก็บใหม่
- ต้องระบุเหตุผล (เช่น Severe Hemolysis, Insufficient Volume, Wrong Label)
3.6 เหตุผล/ปัญหาที่พบ (Quality Notes) - Conditional Required
Display Logic:
// toggleQualityIssues()
if (quality === 'acceptable' || quality === 'poor' || quality === 'rejected') {
document.getElementById('qualityNotesGroup').style.display = 'block';
document.getElementById('qualityNotes').required = true;
} else {
document.getElementById('qualityNotesGroup').style.display = 'none';
document.getElementById('qualityNotes').required = false;
}
Textarea Placeholder:
ระบุเหตุผล/ปัญหาที่พบ เช่น:
- Hemolysis รุนแรง
- เลือดแข็งตัว (Clotted)
- ปริมาตรไม่พอ (Insufficient Volume)
- ฉลากผิด (Wrong Label)
- ปนเปื้อน (Contamination)
- Lipemia (Lipemic specimen)
- Icteric (สีเหลืองจากบิลิรูบิน)
Validation: - Required ถ้าเลือก Acceptable/Poor/Rejected - Minimum length: 5 characters - Recommend: ระบุรายละเอียดชัดเจนเพื่อ Technician เข้าใจปัญหา
Section 4: Save Specimen (บันทึกการเก็บสิ่งส่งตรวจ)
Process Flow:
// Form Submit Handler
collectionForm.addEventListener('submit', async (e) => {
e.preventDefault();
// 1. Get form data
const orderNumber = document.getElementById('collectionOrderNumber').value;
const selectedItems = JSON.parse(document.getElementById('collectionSelectedItems').value);
const volume = parseFloat(document.getElementById('volume').value);
const collectedAt = new Date(document.getElementById('collectedAt').value);
const quality = document.getElementById('quality').value;
const qualityNotes = document.getElementById('qualityNotes').value;
// 2. Validate
if (!orderNumber || !selectedItems.length || !volume || !collectedAt || !quality) {
alert('กรุณากรอกข้อมูลให้ครบถ้วน');
return;
}
if ((quality === 'acceptable' || quality === 'poor' || quality === 'rejected')
&& !qualityNotes.trim()) {
alert('กรุณาระบุเหตุผล/ปัญหาที่พบ');
return;
}
// 3. Create specimen for each selected item
const createdSpecimens = [];
for (const item of selectedItems) {
// Generate Specimen Number
const specimenNumber = specimenService.generateSpecimenNumber();
// Format: SPEC-YYYYMMDD-XXXX (e.g., SPEC-20241226-0001)
// Create specimen data
const specimenData = {
specimenNumber,
orderNumber,
hn: order.hn,
labItems: [item], // Single item per specimen
specimenType: item.specimenType,
containerType: item.containerType,
volume,
collectedAt: collectedAt.toISOString(),
collectedBy: currentUser.id,
collectedByName: currentUser.name,
quality,
qualityNotes: qualityNotes || null,
status: quality === 'rejected' ? 'rejected' : 'collected'
};
// Save to service
const result = await specimenService.createSpecimen(specimenData);
if (result.success) {
createdSpecimens.push(result.specimen);
}
}
// 4. Update order status
if (quality === 'rejected') {
// Keep status as 'collecting' (ยังเก็บไม่ครบ)
labOrderService.updateOrderStatus(orderNumber, 'collecting',
`⚠️ Specimen ถูกปฏิเสธ: ${qualityNotes}`);
} else {
// Check if all items collected
const allSpecimens = specimenService.getSpecimensByOrderNumber(orderNumber);
const allItemsCodes = order.items.map(i => i.itemCode);
const collectedItemCodes = allSpecimens
.filter(s => s.status === 'collected')
.flatMap(s => s.labItems.map(i => i.itemCode));
const allCollected = allItemsCodes.every(code => collectedItemCodes.includes(code));
if (allCollected) {
labOrderService.updateOrderStatus(orderNumber, 'specimen_collected',
'เก็บสิ่งส่งตรวจครบทุกรายการแล้ว');
} else {
labOrderService.updateOrderStatus(orderNumber, 'collecting',
`เก็บแล้ว ${collectedItemCodes.length}/${allItemsCodes.length} รายการ`);
}
}
// 5. Show success modal
showSuccessModal(createdSpecimens);
// 6. Close collection modal
closeCollectionModal();
// 7. Reload orders
loadOrders();
});
Specimen Number Generation:
// specimenService.generateSpecimenNumber()
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
// Get counter from LocalStorage
const key = `his_specimen_counter_${year}${month}${day}`;
let counter = parseInt(localStorage.getItem(key) || '0') + 1;
localStorage.setItem(key, counter);
// Format: SPEC-YYYYMMDD-XXXX
return `SPEC-${year}${month}${day}-${String(counter).padStart(4, '0')}`;
// Examples:
// SPEC-20241226-0001
// SPEC-20241226-0002
// SPEC-20241227-0001 (reset เมื่อเปลี่ยนวัน)
Section 5: Success Modal (บันทึกสำเร็จ)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ✅ บันทึกการเก็บสิ่งส่งตรวจสำเร็จ! │
│ เก็บสิ่งส่งตรวจเรียบร้อยแล้ว │
├─────────────────────────────────────────────────────────────┤
│ 📋 รายการสิ่งส่งตรวจที่สร้าง: │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SPEC-20241226-0001 │ │
│ │ CBC (Blood - EDTA) | Volume: 3.0 ml | Quality: Good │ │
│ │ [🖨️ พิมพ์ Label] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SPEC-20241226-0002 │ │
│ │ FBS (Blood - Fluoride) | Volume: 3.0 ml | Quality: Good│
│ │ [🖨️ พิมพ์ Label] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [🖨️ พิมพ์ Label ทั้งหมด] [ปิด] │
└─────────────────────────────────────────────────────────────┘
Specimen Item Display: - Specimen Number: Bold, Monospace font (เลขสิ่งส่งตรวจ) - Details: Item Name, Specimen Type, Container, Volume, Quality - Individual Print Button: พิมพ์ Label แต่ละชิ้น - Print All Button: พิมพ์ Label ทั้งหมดพร้อมกัน
Animation: - Fade In from bottom (slideUp animation) - Checkmark icon: Scale animation (0 → 1.2 → 1) - Green gradient background (Header)
Section 6: Print Label (พิมพ์ฉลากสิ่งส่งตรวจ)
Trigger: - Click [🖨️ พิมพ์ Label] ใน Success Modal (หลัง Save) - Click [🖨️ พิมพ์ Label] ใน Order Card (Print ล่วงหน้า)
Label Layout (100mm × 50mm):
┌────────────────────────────────────────────┐
│ SPECIMEN LABEL │
├────────────────────────────────────────────┤
│ SPEC-20241226-0001 │
│ (Barcode/QR Code) │
├────────────────────────────────────────────┤
│ Order: LAB-20241226-0001 │
│ HN: HN000123 │
│ Tests: CBC (นับเม็ดเลือด) │
│ Specimen: Blood │
│ Container: EDTA (Purple) │
│ Volume: 3.0 ml │
│ Collected: 26/12/2567 14:30 │
│ By: นางสาววรรณา เจ้าหน้าที่ │
└────────────────────────────────────────────┘
Print Features: - Page Size: 100mm × 50mm (Label size) - Barcode: Specimen Number (Courier New, Bold, Large font) - Patient Info: HN (ไม่แสดงชื่อเต็มเพื่อความเป็นส่วนตัว) - Test Info: รายการตรวจ (ชื่อ Item) - Specimen Info: ประเภท + หลอด + ปริมาตร - Collected Info: เวลาที่เก็บ + ผู้เก็บ
Print Layout (Grid for Multiple Labels):
- 2 columns × N rows สำหรับ A4 paper
- Page Break: ไม่ตัดกลาง Label (break-inside: avoid)
Section 7: Recollection (เก็บใหม่)
Trigger: Click [🔄 เก็บใหม่] Button (เมื่อ Specimen status = 'rejected')
UI Flow:
1. User คลิก [🔄 เก็บใหม่] บน Item ที่ถูกปฏิเสธ
↓
2. แสดง Confirmation Dialog:
"ยืนยันการเก็บสิ่งส่งตรวจใหม่?
เหตุผลที่ปฏิเสธ: Hemolysis รุนแรง"
↓
3. User คลิก [OK]
↓
4. เปิด Collection Modal พร้อมข้อมูล:
- Pre-fill Order Number
- Pre-select ร ายการที่ถูกปฏิเสธ (Cannot uncheck)
- แสดง Warning Box:
"⚠️ การเก็บใหม่: สิ่งส่งตรวจก่อนหน้าถูกปฏิเสธ
กรุณาเก็บใหม่"
↓
5. User กรอกข้อมูล + Submit
↓
6. สร้าง Specimen Number ใหม่:
- Old: SPEC-20241226-0001 (rejected)
- New: SPEC-20241226-0001R1 (recollection #1)
- Next: SPEC-20241226-0001R2 (recollection #2)
↓
7. บันทึก Specimen ใหม่ + Update Order Status
Recollection Specimen Number Format:
// Original: SPEC-20241226-0001
// 1st Recollection: SPEC-20241226-0001R1
// 2nd Recollection: SPEC-20241226-0001R2
// 3rd Recollection: SPEC-20241226-0001R3
// Logic:
const baseNumber = rejectedSpecimen.specimenNumber; // SPEC-20241226-0001
const recollectionCount = specimenService.getRecollectionCount(baseNumber);
const newSpecimenNumber = `${baseNumber}R${recollectionCount + 1}`;
Recollection Rules:
1. Cannot Edit: Rejected specimen cannot be edited (immutable)
2. Create New: Always create new specimen with R suffix
3. Link: Store original specimen number in originalSpecimenNumber field
4. History: Keep rejection history for audit trail
5. Status: New specimen status = 'collected' (if QC passed)
⚙️ Technical Features
1. Specimen Model
class Specimen {
specimenNumber: string; // SPEC-20241226-0001
orderNumber: string; // LAB-20241226-0001
hn: string; // HN000123
labItems: Array<{ // รายการตรวจที่ใช้ specimen นี้
itemCode: string;
itemName: string;
}>;
specimenType: string; // blood, urine, stool
containerType: string; // edta, plain, fluoride
volume: number; // ml
collectedAt: Date;
collectedBy: string; // User ID
collectedByName: string; // User Name
quality: string; // good, acceptable, poor, rejected
qualityNotes: string; // Reason/Issues
qualityIssues: Array<string>; // [hemolysis, clotted, lipemic]
status: string; // collected, rejected, testing, completed
originalSpecimenNumber: string; // For recollection (R1, R2)
createdAt: Date;
updatedAt: Date;
}
2. Quality Issue Types
const QUALITY_ISSUES = {
// Blood Issues
'hemolysis': 'Hemolysis (เม็ดเลือดแดงแตก)',
'clotted': 'Clotted (เลือดแข็งตัว)',
'lipemic': 'Lipemia (ไขมันในเลือดสูง)',
'icteric': 'Icteric (บิลิรูบินสูง สีเหลือง)',
// Volume Issues
'insufficient_volume': 'Insufficient Volume (ปริมาตรไม่พอ)',
'excessive_volume': 'Excessive Volume (ปริมาตรมากเกินไป)',
// Labeling Issues
'wrong_label': 'Wrong Label (ฉลากผิด)',
'missing_label': 'Missing Label (ไม่มีฉลาก)',
'unreadable_label': 'Unreadable Label (ฉลากอ่านไม่ได้)',
// Contamination
'contamination': 'Contamination (ปนเปื้อน)',
'bacterial_contamination': 'Bacterial Contamination (ปนเปื้อนเชื้อแบคทีเรีย)',
// Other
'wrong_container': 'Wrong Container (หลอดผิดประเภท)',
'expired_container': 'Expired Container (หลอดหมดอายุ)',
'damaged_container': 'Damaged Container (หลอดชำรุด)'
};
3. LocalStorage Keys
'his_specimens' // Array of Specimens
'his_specimen_counter_YYYYMMDD' // Daily counter (reset each day)
'his_lab_orders' // Array of Lab Orders (shared)
4. Status Transitions
// Specimen Status Flow
'pending_collection' // → Initial (ยังไม่เก็บ)
↓
'collected' // → เก็บแล้ว (QC: Good/Acceptable/Poor)
↓
'received' // → รับตัวอย่างแล้วที่แล็บ
↓
'testing' // → กำลังตรวจ
↓
'completed' // → ตรวจเสร็จ (มีผลแล้ว)
// Rejected Flow (Separate branch)
'pending_collection'
↓
'rejected' // → ปฏิเสธ (QC: Rejected)
↓
[Recollection] // → เก็บใหม่ (Create new specimen with R suffix)
↓
'collected' (New)
🎨 Design System
Quality Color Coding
.quality-good { background: #d4edda; color: #155724; } /* Green */
.quality-acceptable { background: #fff3cd; color: #856404; } /* Yellow */
.quality-poor { background: #ffe5d0; color: #d63384; } /* Orange */
.quality-rejected { background: #f8d7da; color: #721c24; } /* Red */
Specimen Status Colors
.specimen-status-pending_collection { background: #fff3e0; color: #ef6c00; } /* Orange */
.specimen-status-collected { background: #e8f5e9; color: #2e7d32; } /* Green */
.specimen-status-received { background: #e3f2fd; color: #1976d2; } /* Blue */
.specimen-status-testing { background: #e1f5fe; color: #0277bd; } /* Cyan */
.specimen-status-completed { background: #c8e6c9; color: #1b5e20; } /* Dark Green */
.specimen-status-rejected { background: #ffebee; color: #c62828; } /* Red */
📱 Responsive Behavior
Desktop (> 1200px)
- Order Cards: Full width
- Item Table: All columns visible
- Modal: 800px width (centered)
Tablet (768px - 1200px)
- Order Cards: Full width
- Item Table: Horizontal scroll
- Modal: 90% width
Mobile (< 768px)
- Order Cards: Stacked (1 per row)
- Item Table: Simplified (hide some columns)
- Modal: 95% width, Vertical scroll
💡 Best Practices (UX/UI)
- QC Workflow:
- เริ่มต้นที่ "Good" (Default)
- Force input เหตุผลถ้าเลือก Poor/Rejected
-
แสดง Warning ชัดเจนเมื่อปฏิเสธ
-
Barcode Integration:
- Auto-focus Search Box
- Support Barcode Scanner input
-
No need to press Enter (auto-submit)
-
Print Label Timing:
- Option 1: Print ล่วงหน้า (ก่อนเก็บ) - สำหรับ Workflow ที่ติด Label ก่อน
-
Option 2: Print หลังเก็บ (Success Modal) - สำหรับ Workflow ที่เก็บก่อน
-
Error Prevention:
- Disable Save ถ้า Form ไม่ครบ
- Validate Volume > 0
- Validate DateTime ไม่เกินปัจจุบัน
- Confirm ก่อนปฏิเสธ (Rejected)
🐛 Known Issues & Limitations
- No Barcode Generation: แสดงเป็น Text only (Future: Generate QR Code/Barcode image)
- No Photo Upload: ไม่สามารถถ่ายรูปตัวอย่าง (Future: Add camera feature)
- No Multi-Specimen: 1 Item = 1 Specimen (ไม่รองรับการเก็บ Pooled Specimen)
- Mock User:
collectedByhardcoded (Future: Get from session) - No Temperature Log: ไม่บันทึกอุณหภูมิการเก็บ/เก็บรักษา
📊 Business Rules
- Specimen Creation Rules:
- 1 Item = 1 Specimen (แยก Specimen ต่อรายการ)
- Same Container Type → Can group (Future enhancement)
-
Different Container → Must separate
-
QC Threshold:
- Good: ผ่าน QC ทุกเกณฑ์
- Acceptable: ผ่านบางเกณฑ์ แต่ยังตรวจได้ (Note เหตุผล)
- Poor: ไม่ผ่านหลายเกณฑ์ แต่ยังตรวจได้ (ผลอาจไม่แม่นยำ)
-
Rejected: ไม่ผ่าน QC ต้องเก็บใหม่
-
Recollection Rules:
- Maximum 3 recollections (R1, R2, R3)
- After R3 → Escalate to Supervisor
-
Original rejected specimen → Keep for audit (ไม่ลบ)
-
Label Printing:
- Must print within 24 hours of collection
- Cannot print label for rejected specimens
- Re-print allowed (for damaged labels)
5. result-entry.html - ลงผลแล็บ
📌 ภาพรวม
หน้าสำหรับ Lab Technician ลงผลการตรวจแล็บ พร้อมระบบ Delta Check (เปรียบเทียบผลเก่า-ใหม่), Critical Alert (แจ้งเตือนผลวิกฤต), และ Outlab File Upload (อัพโหลดผลจากแล็บนอก)
🎯 วัตถุประสงค์
- ลงผลแล็บ: บันทึกค่าผลการตรวจ (Numeric/Text)
- Delta Check: เปรียบเทียบผลปัจจุบันกับผลก่อนหน้า (%Change)
- Critical Alert: แจ้งเตือนผลวิกฤต + Countdown Timer (30 นาที)
- Flag Calculation: คำนวณ Flag อัตโนมัติ (Normal, High, Low, Critical)
- Outlab Upload: อัพโหลดผลจากแล็บนอก (PDF/Image)
- Partial Result: รองรับบันทึกผลบางส่วน (ค่อยๆ ลงผล)
- Lab Note: เพิ่มหมายเหตุแจ้งแพทย์
📊 File Statistics
- Total Lines: 2,462 lines
- Components: Search, Order List, Result Entry Panel, Critical Modal, Outlab Upload Modal
- Dependencies: LabResult.js, LabOrder.js, Patient.js, LabResultService, FileAttachmentService
- Data Files: lab-previous-results.json, lab-instruments.json, lab-items.json
🧩 UI Components & Workflow
Section 1: Search & Order List
Filter Logic:
- แสดงเฉพาะ Orders ที่ status = sent_to_lis, in_progress, partial_result
- เรียงตาม Priority (STAT → Urgent → Routine)
Order Card Display:
┌────────────────────────────────────────────────────────────┐
│ LAB-20241226-0001 [⚡STAT] │
│ HN: HN000123 | ผู้ป่วย: นายสมชาย ใจดี │
│ รายการ: 5 รายการ | ลงผลแล้ว: 2/5 รายการ (40%) │
│ วันที่: 26/12/2567 | แพทย์: นพ.สมชาย │
│ [เลือก Order นี้] │
└────────────────────────────────────────────────────────────┘
Progress Indicator: - แสดง ลงผลแล้ว: X/Y รายการ (Z%) สำหรับ Orders ที่มีผลบางส่วน - สี Progress Bar: - 0-39%: Red (ยังค้างเยอะ) - 40-79%: Orange (กำลังดำเนินการ) - 80-99%: Yellow (เกือบครบ) - 100%: Green (ครบแล้ว)
Section 2: Patient Info Bar
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 👤 HN: HN000123 | ชื่อ: นายสมชาย ใจดี | อายุ: 30 ปี │
│ 📋 Order: LAB-20241226-0001 | Priority: ⚡STAT │
│ 🏥 VN: VN20241226001 | แผนก: OPD | กรุ๊ปเลือด: O+ │
│ ⚠️ แพ้ยา: Penicillin, Aspirin │
└─────────────────────────────────────────────────────────────┘
Background: Gradient Purple-Blue (linear-gradient(135deg, #667eea 0%, #764ba2 100%))
Critical Info Highlight: - แพ้ยา: แสดงเป็นสีแดงถ้ามี (⚠️ Icon) - โรคประจำตัว: แสดงถ้ามี (💊 Icon) - STAT Order: แสดง ⚡ Icon สีแดงโดดเด่น
Section 3: Results Table (ตารางลงผล)
UI Layout:
┌──────────────────────────────────────────────────────────────────────────────┐
│ # │ รายการตรวจ │ ผล │ หน่วย │ Flag │ Reference Range │ หมายเหตุ│
├───┼──────────────┼──────────────┼───────┼────────┼─────────────────┼─────────┤
│ 1 │ WBC │ [_____] ⬅️ │ x10³/µL│ Normal│ 4.5-11.0 │ [____] │
│ │ │ ⚠️ Delta: +15%│ │ │ │ │
│ │ │ (prev: 7.0) │ │ │ │ │
├───┼──────────────┼──────────────┼───────┼────────┼─────────────────┼─────────┤
│ 2 │ RBC │ [4.8___] │ x10⁶/µL│ Normal│ 4.5-5.5 │ [____] │
├───┼──────────────┼──────────────┼───────┼────────┼─────────────────┼─────────┤
│ 3 │ Hemoglobin │ [14.2__] │ g/dL │ Normal│ 13.5-17.5 │ [____] │
├───┼──────────────┼──────────────┼───────┼────────┼─────────────────┼─────────┤
│ 4 │ Platelet │ [___50_] 🚨 │ x10³/µL│🔴Critical│ 150-400 │ [____] │
│ │ │ │ │ Low │ │ │
├───┼──────────────┼──────────────┼───────┼────────┼─────────────────┼─────────┤
│ 5 │ **Outlab** │ [📎 Upload] │ - │ - │ - │ [____] │
│ │ HIV Viral Load│ │ │ │ │ │
└──────────────────────────────────────────────────────────────────────────────┘
Columns:
3.1 # (ลำดับ) - Auto-number (1, 2, 3, ...)
3.2 รายการตรวจ (Test Name)
- Item Name: ชื่อรายการภาษาไทย/อังกฤษ
- Item Code: แสดงด้านล่าง (เทาๆ, เล็ก)
- Outlab Indicator: แสดง Badge "🔗 ส่งตรวจนอก" ถ้า isOutsourced: true
3.3 ผล (Result Value) - Input Field
Input Types: - Numeric: Number input (step: 0.01)
<input type="number" step="0.01" min="0" placeholder="0.0">
<input type="text" placeholder="Negative / Positive / Normal">
Real-time Validation: - On Input → Calculate Flag → Update Flag Badge - Check Critical → Change border color (Red) - Check Abnormal → Change border color (Yellow)
Delta Check Display (แสดงใต้ Input ถ้ามีผลเก่า):
<div class="delta-check delta-warning">
⚠️ Delta: +15.3% (ก่อนหน้า: 7.0 x10³/µL วันที่ 20/12/67)
</div>
Delta Calculation:
// Calculate percentage change
const percentChange = ((newValue - oldValue) / oldValue) * 100;
// Delta thresholds (from lab-items.json)
if (Math.abs(percentChange) >= criticalDeltaThreshold) {
// Critical Delta (e.g., >50% for WBC)
return 'delta-critical'; // Red background
} else if (Math.abs(percentChange) >= warningDeltaThreshold) {
// Warning Delta (e.g., >20% for WBC)
return 'delta-warning'; // Yellow background
} else {
// Normal Delta
return 'delta-check'; // Blue background (informational)
}
3.4 หน่วย (Unit)
- Display Only
- Examples: g/dL, x10³/µL, mg/dL, mEq/L, %
3.5 Flag (Interpretation)
Flag Badge Colors:
.flag-normal { background: #d4edda; color: #155724; } /* Green */
.flag-high { background: #fff3cd; color: #856404; } /* Yellow */
.flag-low { background: #fff3cd; color: #856404; } /* Yellow */
.flag-critical_high { background: #f8d7da; color: #721c24; } /* Red */
.flag-critical_low { background: #f8d7da; color: #721c24; } /* Red */
Flag Calculation Logic:
// calculateFlag()
if (result >= criticalHigh || result <= criticalLow) {
return 'critical_high' or 'critical_low'; // 🔴 Critical
} else if (result > referenceHigh) {
return 'high'; // 🟡 Abnormal High
} else if (result < referenceLow) {
return 'low'; // 🟡 Abnormal Low
} else {
return 'normal'; // 🟢 Normal
}
Reference Ranges (จาก lab-items.json):
{
"referenceLow": 4.5,
"referenceHigh": 11.0,
"criticalLow": 2.0,
"criticalHigh": 30.0,
"resultUnit": "x10³/µL"
}
3.6 Reference Range - Display: "4.5 - 11.0" (Text only) - Color: Gray (#666)
3.7 หมายเหตุ (Note) - Optional textarea - Examples: "ตรวจซ้ำ", "Hemolysis เล็กน้อย", "ผลยืนยัน"
Section 4: Outlab File Upload (อัพโหลดผลแล็บนอก)
Trigger: Click [📎 Upload] Button บน Outlab Item Row
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 📤 แนบผลแล็บนอก (Outsourced Lab Result) [✕] │
├─────────────────────────────────────────────────────────────┤
│ 🔼 อัพโหลดไฟล์ผลการตรวจ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ☁️ │ │
│ │ คลิกหรือลากไฟล์มาวางที่นี่ │ │
│ │ รองรับไฟล์ PDF, JPG, PNG (สูงสุด 5 MB/ไฟล์) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 📎 ไฟล์ที่แนบ (2 ไฟล์): │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 📄 HIV_Result_26122567.pdf (1.2 MB) [❌ ลบ] │ │
│ │ 🖼️ lab_report_scan.jpg (850 KB) [❌ ลบ] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 📝 หมายเหตุผลการตรวจ: │
│ ┌────────────────────────────────────────────────────┐ │
│ │ HIV Viral Load: < 50 copies/mL (Undetectable) │ │
│ │ ตรวจที่ห้องแล็บศิริราช วันที่ 15/01/2568 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 💡 หมายเหตุนี้จะแสดงร่วมกับไฟล์แนบในระบบ │
│ │
│ [ยกเลิก] [✅ บันทึกผลแล็บ] │
└─────────────────────────────────────────────────────────────┘
File Upload Features:
4.1 Drag & Drop Support
// setupDragDrop()
fileUploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
fileUploadArea.classList.add('drag-over');
});
fileUploadArea.addEventListener('drop', (e) => {
e.preventDefault();
fileUploadArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
handleFiles(files);
});
4.2 File Validation
// Allowed file types
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
// Max file size: 5 MB
const maxSize = 5 * 1024 * 1024;
// Validation
for (const file of files) {
if (!allowedTypes.includes(file.type)) {
alert(`ไฟล์ ${file.name} ไม่รองรับ (รองรับเฉพาะ PDF, JPG, PNG)`);
continue;
}
if (file.size > maxSize) {
alert(`ไฟล์ ${file.name} ขนาดเกิน 5 MB (${(file.size / 1024 / 1024).toFixed(2)} MB)`);
continue;
}
// File is valid → Add to attachment list
}
4.3 Progress Bar (แสดงเมื่ออัพโหลด)
<div class="upload-progress show">
<div class="progress-bar-container">
<div class="progress-bar" style="width: 45%">45%</div>
</div>
<div>กำลังอัพโหลด...</div>
</div>
Simulation: - Progress: 0% → 100% (animate in 2 seconds) - Actual storage: Base64 encode + LocalStorage (FileAttachmentService)
4.4 Attachment List Display
<div class="attachment-item">
<div class="attachment-icon">📄</div>
<div class="attachment-info">
<div class="attachment-name">HIV_Result_26122567.pdf</div>
<div class="attachment-size">1.2 MB</div>
</div>
<button class="btn-remove" onclick="removeAttachment(index)">
<i class="fas fa-trash"></i> ลบ
</button>
</div>
File Type Icons: - PDF: 📄 - JPG/PNG: 🖼️ - Other: 📎
4.5 Result Note (หมายเหตุผลการตรวจ) - Textarea input (Multi-line) - Purpose: ระบุค่าผลการตรวจที่สำคัญจากรายงาน (ไม่สามารถ OCR ได้) - Example:
HIV Viral Load: < 50 copies/mL (Undetectable)
CD4 Count: 580 cells/µL
ตรวจที่ห้องแล็บศิริราช วันที่ 15/01/2568
Save Logic:
// saveOutlabResult()
const attachmentData = {
orderNumber: currentOrder.orderNumber,
itemCode: currentItem.itemCode,
files: uploadedFiles, // Array of { fileName, fileType, fileSize, fileData (Base64) }
note: attachmentNote,
uploadedBy: currentUser.id,
uploadedByName: currentUser.name,
uploadedAt: new Date().toISOString()
};
// Save to FileAttachmentService
const result = await fileAttachmentService.uploadAttachment(attachmentData);
// Update result status to 'preliminary'
labResultService.enterResult(
resultId,
'See Attachment', // Placeholder text
currentUser.id,
currentUser.name,
attachmentNote
);
Section 5: Lab Note (หมายเหตุแจ้งแพทย์)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 📝 [✓] เพิ่ม Lab Note (หมายเหตุแจ้งแพทย์) │
├─────────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ค่า WBC ต่ำกว่าปกติ แนะนำให้ตรวจซ้ำภายใน 1 สัปดาห์ │ │
│ │ เพื่อดู Trend │ │
│ │ │ │
│ │ หรือ: สิ่งส่งตรวจมี Mild Hemolysis อาจส่งผลต่อค่า │ │
│ │ Potassium │ │
│ └────────────────────────────────────────────────────────┘ │
│ ผู้เขียน: เทคนิคแล็บสมหญิง | เวลา: 26/12/67 14:30 │
└─────────────────────────────────────────────────────────────┘
Purpose: - เพิ่มหมายเหตุจากเทคนิคแล็บแจ้งแพทย์เกี่ยวกับผลการตรวจ - Use Cases: - ค่าผิดปกติที่ต้องติดตาม - ปัญหาสิ่งส่งตรวจ (Hemolysis, Lipemia) - แนะนำการตรวจเพิ่มเติม - Panic Value (Critical result)
Toggle Behavior:
// toggleLabNote()
if (checkbox.checked) {
labNoteContent.style.display = 'block';
document.getElementById('labNoteAuthor').textContent = currentUser.name;
document.getElementById('labNoteTime').textContent = new Date().toLocaleString('th-TH');
} else {
labNoteContent.style.display = 'none';
document.getElementById('labNoteText').value = '';
}
Save Behavior: - บันทึกพร้อมกับ Results - แสดงใน Result Report (สีเหลือง Highlight) - แสดงใน Result Approval Page (สำหรับ Supervisor)
Section 6: Instrument Section (ข้อมูลการตรวจ)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 🔬 ข้อมูลการตรวจ │
├─────────────────────────────────────────────────────────────┤
│ เครื่องมือที่ใช้: [Sysmex XN-1000 ▼] │
│ ผู้ลงผล: เทคนิคแล็บสมหญิง (Read-only) │
└─────────────────────────────────────────────────────────────┘
Instrument Dropdown:
- Load from lab-instruments.json
- Grouped by Category:
- Hematology: Sysmex XN-1000, Mindray BC-6800
- Chemistry: Cobas C311, Architect ci4100, Vitros 350
- Immunology: Cobas e411, Architect i2000SR
- Microbiology: BacT/ALERT 3D, VITEK 2
- Manual: Manual Microscope, Manual Counting
Instrument Data Model:
{
"id": "INS001",
"name": "Sysmex XN-1000",
"category": "Hematology",
"manufacturer": "Sysmex Corporation",
"model": "XN-1000",
"status": "active",
"maintenanceDate": "2024-01-15"
}
Section 7: Partial Result Summary
Display Condition: แสดงเมื่อมีผลบางส่วน (completed > 0 && pending > 0)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ℹ️ สถานะการลงผล (Partial Result Status) │
├─────────────────────────────────────────────────────────────┤
│ [✅ ลงผลแล้ว: 3] [⏳ รอลงผล: 2] [📊 ทั้งหมด: 5] │
└─────────────────────────────────────────────────────────────┘
Real-time Update:
// updatePartialResultSummary()
const completed = currentResults.filter(r => r.result && r.result !== '').length;
const total = currentResults.length;
const pending = total - completed;
document.getElementById('completedCount').textContent = completed;
document.getElementById('pendingCount').textContent = pending;
document.getElementById('totalCount').textContent = total;
Section 8: Action Buttons
Three Save Options:
8.1 [💾 บันทึกทั้งหมด] (btn-success, Green)
// saveAllResults()
// - Save all results (skip empty inputs)
// - Calculate flags
// - Check for critical results
// - If critical → Show Critical Modal
// - If no critical → Success message
// - Update order status to 'in_progress'
8.2 [💾 บันทึกผลบางส่วน] (btn-partial, Orange)
// savePartialResults()
// - Save only filled inputs (skip empty)
// - Count: savedCount vs skippedCount
// - Update order status to 'partial_result'
// - Show summary: "✅ ลงผลแล้ว: 3 รายการ, ⏳ ยังค้างอยู่: 2 รายการ"
// - Keep panel open (ไม่ปิดหน้า)
8.3 [✅ บันทึกและยืนยัน] (btn-info, Cyan)
// saveAndVerify()
// - saveAllResults() first
// - Then verify all results (status: preliminary → final)
// - Update order status to 'completed'
// - Success message + Close panel
Section 9: Critical Alert Modal
Trigger: เมื่อมี Critical Result (Flag = critical_high or critical_low)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 🚨 │
│ พบผลวิกฤต (Critical Result) │
├─────────────────────────────────────────────────────────────┤
│ ⚠️ คำเตือน: พบผลการตรวจที่อยู่ในระดับวิกฤต │
│ กรุณาตรวจสอบความถูกต้องและแจ้งแพทย์ทันที │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ WBC (White Blood Cell) │ │
│ │ ค่าที่วัดได้: 1.5 x10³/µL 🔴 │ │
│ │ ค่าปกติ: 4.5 - 11.0 x10³/µL │ │
│ │ Critical Low Threshold: < 2.0 x10³/µL │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Platelet │ │
│ │ ค่าที่วัดได้: 48 x10³/µL 🔴 │ │
│ │ ค่าปกติ: 150 - 400 x10³/µL │ │
│ │ Critical Low Threshold: < 50 x10³/µL │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ⏱️ ต้องแจ้งแพทย์ภายใน: **29:45** นาที │
│ │
│ [✓] ยืนยันว่าได้ตรวจสอบผลซ้ำแล้ว และผลนี้ถูกต้อง │
│ │
│ [🔄 ตรวจซ้ำ] [✅ ยืนยันและบันทึก] │
└─────────────────────────────────────────────────────────────┘
Critical Result Item Display:
<div class="critical-result-item">
<div>
<strong>WBC (White Blood Cell)</strong><br>
<span style="font-size: 0.9rem; color: #666;">CBC001</span>
</div>
<div style="text-align: right;">
<div style="font-size: 1.3rem; font-weight: 600; color: #dc3545;">
1.5 x10³/µL
</div>
<div style="font-size: 0.85rem; color: #666;">
ค่าปกติ: 4.5 - 11.0
</div>
</div>
</div>
Countdown Timer:
// startCriticalCountdown(30 * 60) // 30 minutes
let remaining = 1800; // seconds
setInterval(() => {
const minutes = Math.floor(remaining / 60);
const secs = remaining % 60;
countdownDisplay.textContent = `${minutes}:${secs.toString().padStart(2, '0')}`;
// Warning when < 5 minutes
if (remaining < 300) {
countdownDisplay.style.color = '#dc3545';
countdownDisplay.style.animation = 'pulse 1s infinite';
}
remaining--;
if (remaining < 0) {
clearInterval(countdownInterval);
showAlert('⚠️ เวลาในการแจ้งแพทย์หมดแล้ว! กรุณาติดต่อแพทย์ทันที', 'danger');
}
}, 1000);
Double-Check Confirmation:
<label class="double-check-label">
<input type="checkbox" id="doubleCheckConfirm">
<span>✓ ยืนยันว่าได้ตรวจสอบผลซ้ำแล้ว และผลนี้ถูกต้อง</span>
</label>
Actions:
[🔄 ตรวจซ้ำ] (Gray) - Close modal - Stop countdown - Alert: "กรุณาตรวจสอบผลอีกครั้ง และแก้ไขหากจำเป็น" - ไม่บันทึก → User แก้ไขค่าได้
[✅ ยืนยันและบันทึก] (Green, Disabled ถ้าไม่ Checkbox) - Require: Double-check checkbox checked - Stop countdown - Save all results - Alert: "บันทึกผลวิกฤตสำเร็จ X รายการ ระบบได้แจ้งเตือนแพทย์แล้ว" - Update order status
⚙️ Technical Features
1. Delta Check System
// Load previous results from JSON
const previousResults = {
"HN000123": {
"WBC": {
"value": 7.0,
"unit": "x10³/µL",
"date": "2024-12-20T10:30:00"
},
"Hemoglobin": {
"value": 14.5,
"unit": "g/dL",
"date": "2024-12-20T10:30:00"
}
}
};
// Calculate delta
function calculateDelta(hn, itemCode, newValue) {
const prevResult = previousResults[hn]?.[itemCode];
if (!prevResult) return null;
const oldValue = prevResult.value;
const percentChange = ((newValue - oldValue) / oldValue) * 100;
return {
oldValue,
percentChange: percentChange.toFixed(1),
date: prevResult.date,
isDeltaWarning: Math.abs(percentChange) >= 20,
isDeltaCritical: Math.abs(percentChange) >= 50
};
}
Delta Display Logic:
// Show delta if change > 10%
if (Math.abs(percentChange) >= 10) {
const deltaClass = isDeltaCritical ? 'delta-critical' :
isDeltaWarning ? 'delta-warning' : 'delta-check';
const deltaHTML = `
<div class="${deltaClass}">
${isDeltaCritical ? '🚨' : '⚠️'} Delta: ${percentChange > 0 ? '+' : ''}${percentChange}%
(ก่อนหน้า: ${oldValue} ${unit} วันที่ ${formatDate(date)})
</div>
`;
}
2. Flag Calculation Engine
// Auto-calculate flag on input
resultInput.addEventListener('input', () => {
const value = parseFloat(resultInput.value);
if (isNaN(value)) return;
// Get reference ranges from LabItem
const { referenceLow, referenceHigh, criticalLow, criticalHigh } = labItem;
let flag = 'normal';
if (value <= criticalLow) {
flag = 'critical_low';
} else if (value >= criticalHigh) {
flag = 'critical_high';
} else if (value < referenceLow) {
flag = 'low';
} else if (value > referenceHigh) {
flag = 'high';
}
// Update UI
updateFlagBadge(flag);
updateInputBorder(flag);
// Store for save
result.flag = flag;
});
3. LocalStorage Keys
'his_lab_results' // Array of Lab Results
'his_lab_result_counter' // Auto-increment ID
'his_file_attachments' // Outlab file attachments (Base64)
'his_lab_orders' // Orders (shared, status updates)
4. Status Transitions
// Order Status Flow (Result Entry perspective)
'sent_to_lis' // → Ready for result entry
↓
'in_progress' // → Started result entry (some results entered)
↓
'partial_result' // → Partial results saved (not all items)
↓
'pending_approval' // → All results entered, waiting approval
↓
'approved' // → Approved by supervisor
↓
'completed' // → Final status
// Result Status Flow
'pending' // → Initial (no result yet)
↓
'preliminary' // → Result entered (not verified)
↓
'final' // → Verified and approved
↓
'rejected' // → Rejected (need re-entry or recollection)
🎨 Design System
Input Border Colors
.result-input { border-color: #ddd; } /* Normal */
.result-input:focus { border-color: #2196f3; } /* Focus */
.result-input.abnormal { border-color: #ffc107; } /* Abnormal */
.result-input.critical { border-color: #dc3545; } /* Critical */
Delta Check Colors
.delta-check { background: #e3f2fd; border-left: 4px solid #2196f3; } /* Info */
.delta-warning { background: #fff3cd; border-left: 4px solid #ffc107; } /* Warning */
.delta-critical { background: #f8d7da; border-left: 4px solid #dc3545; } /* Critical */
Critical Modal Animation
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Apply to countdown when < 5 min */
.critical-countdown span {
animation: pulse 1s infinite;
}
📱 Responsive Behavior
Desktop (> 1200px)
- Table: Full width, All columns visible
- Modal: 800px width (centered)
Tablet (768px - 1200px)
- Table: Horizontal scroll
- Modal: 90% width
Mobile (< 768px)
- Table: Simplified (hide Note column)
- Modal: 95% width
- Stack Input fields vertically
💡 Best Practices (UX/UI)
- Real-time Feedback:
- Flag updates immediately on input
- Delta check shows as you type (if > 10% change)
-
Input border changes color based on flag
-
Critical Result Safety:
- Cannot save critical results without double-check
- 30-minute countdown to ensure prompt notification
-
Visual alerts (Red, Pulsing, Icon)
-
Partial Result Support:
- Separate "บันทึกผลบางส่วน" button
- Progress indicator (X/Y items completed)
-
Can save and come back later
-
Error Prevention:
- Numeric input validation (min: 0, step: 0.01)
- File size/type validation for uploads
- Require note for outlab results
🐛 Known Issues & Limitations
- No Auto-Import from LIS: Manual entry only (Future: LIS integration)
- Delta Check Limited: Only checks last result (Future: Check trends)
- Critical Notification Mock: No real SMS/Email (Future: Real notification)
- Outlab Files in LocalStorage: Limited size (Future: Cloud storage)
- No Batch Entry: One order at a time (Future: Batch mode)
📊 Business Rules
- Critical Result Rules:
- Must double-check before saving
- Must notify doctor within 30 minutes
-
Cannot bypass confirmation (forced workflow)
-
Partial Result Rules:
- Can save partial results (at least 1 item filled)
- Order status = 'partial_result'
-
Cannot approve until all results entered
-
Outlab Result Rules:
- Must upload file attachment (PDF/Image)
- Must provide result note (text description)
-
Counts as "result entered" for approval
-
Delta Check Thresholds:
- Info: 10-19% change (Blue)
- Warning: 20-49% change (Yellow)
- Critical: ≥50% change (Red)
6. result-approval.html - อนุมัติผลแล็บ
📌 ภาพรวม
หน้าสำหรับ Lab Supervisor/Lab Chief ตรวจสอบและอนุมัติผลแล็บที่ลงผลเสร็จแล้ว พร้อมระบบ Partial Approval (อนุมัติเลือกทีละรายการ) และ Rejection Workflow (ปฏิเสธพร้อมเหตุผล)
🎯 วัตถุประสงค์
- ตรวจสอบผล: Review ผลที่ลงเสร็จแล้ว (status: preliminary)
- Partial Approval: อนุมัติเฉพาะรายการที่เลือก (ไม่จำเป็นต้องทั้งหมด)
- Quick Approve: อนุมัติทีละรายการแบบเร็ว
- Reject with Reason: ปฏิเสธผลพร้อมระบุเหตุผล
- Critical Alert: แจ้งเตือนผลวิกฤต (ต้องแจ้งแพทย์)
- Outlab Support: แสดงไฟล์ PDF/Image จาก Lab นอก
- Statistics Dashboard: แสดงสรุปรออนุมัติ, ผลวิกฤต, อนุมัติแล้ว
📊 File Statistics
- Total Lines: 1,162 lines
- Components: Statistics Dashboard, Order List, Approval Modal, Reject Modal
- Dependencies: LabResult.js, LabOrder.js, Patient.js, LabResultService, PatientService
- Data Files: lab-items.json (for outlab check)
🧩 UI Components & Workflow
Section 1: Statistics Dashboard (สรุปสถิติ)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 15 │ │ 3 │ │ 28 │ │
│ │ รออนุมัติ │ │ ผลวิกฤต │ │ อนุมัติแล้ว │ │
│ │ │ │ │ │ (วันนี้) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ 🟡 Yellow 🔴 Red 🟢 Green │
└─────────────────────────────────────────────────────────────┘
Card Borders:
- Pending: Yellow (#ffc107)
- Critical: Red (#dc3545)
- Approved: Green (#28a745)
Data Source:
// labResultService.getStatistics()
{
preliminary: 15, // Count of results with status 'preliminary'
critical: 3, // Count of results with isCritical() = true
final: 28 // Count of results approved today
}
Section 2: Order List (รายการรออนุมัติ)
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ 📋 รายการรออนุมัติ (12 รายการ) │
├─────────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 🚨 พบผลวิกฤต 2 รายการ - กรุณาตรวจสอบและอนุมัติโดยเร็ว│ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ LAB-20241226-0001 [🌐 OUTLAB (3/5)] │
│ HN: HN000123 | ชื่อ: นายสมชาย ใจดี | แพทย์: นพ.สมชาย │
│ วันที่สั่ง: 26/12/67 10:30 │
│ │
│ 📊 สรุปผลการตรวจ (5 รายการ): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ WBC 8.5 x10³/µL 🟢 Normal │ │
│ │ Hemoglobin 14.2 g/dL 🟢 Normal │ │
│ │ Platelet 50 x10³/µL 🔴 Critical Low│ │
│ │ HIV Viral Load 🌐 Lab นอก 📄 PDF Report │ │
│ │ CD4 Count 🌐 Lab นอก 📄 PDF Report │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 🔴 ผลวิกฤต: 1 | 🟡 ผิดปกติ: 0 │
│ ลงผลโดย: เทคนิคแล็บสมหญิง [ตรวจสอบและอนุมัติ]│
└─────────────────────────────────────────────────────────────┘
Order Card Features:
2.1 Critical Alert Banner (แสดงถ้ามี Critical)
<div class="critical-alert">
🚨 พบผลวิกฤต 2 รายการ - กรุณาตรวจสอบและอนุมัติโดยเร็ว
</div>
2.2 Outlab Badge (แสดงถ้ามี Outlab Items)
<!-- Mixed Order (มีทั้ง In-house และ Outlab) -->
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
🌐 MIXED (3/5)
</span>
<!-- All Outlab (ทุกรายการส่ง Lab นอก) -->
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
🌐 OUTLAB (3/3)
</span>
Check Logic:
// Check if lab item is outsourced
const hasOutlabItems = order.items.some(item => {
const labItem = window.labItems?.find(li => li.id === item.itemId);
return labItem?.isOutsourced === true;
});
// Count outlab items
const outlabItemCount = order.items.filter(item => {
const labItem = window.labItems?.find(li => li.id === item.itemId);
return labItem?.isOutsourced === true;
}).length;
// Determine badge type
const isAllOutlab = outlabItemCount === order.items.length;
const badgeText = isAllOutlab ? 'OUTLAB' : 'MIXED';
2.3 Results Summary (สรุปผล 5 รายการแรก) - แสดง Result Value + Flag - Outlab Items: แสดง "📄 PDF Report" แทนค่าตัวเลข - แสดง "... และอีก X รายการ" ถ้ามีเกิน 5 รายการ
2.4 Empty State (ไม่มีรายการ)
┌─────────────────────────────────────────────────────────────┐
│ ✓ │
│ ไม่มีรายการรออนุมัติ │
│ ไม่มีผลแล็บที่รออนุมัติในขณะนี้ │
└─────────────────────────────────────────────────────────────┘
Section 3: Approval Modal (Detail View)
Trigger: Click [ตรวจสอบและอนุมัติ] Button
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ✓ อนุมัติผลแล็บ [✕] │
├─────────────────────────────────────────────────────────────┤
│ 🚨 พบผลวิกฤต 2 รายการ │
│ กรุณาตรวจสอบอย่างละเอียดก่อนอนุมัติและแจ้งแพทย์ทันที │
│ │
│ 👤 ข้อมูลผู้ป่วย │
│ ───────────────────────────────────────────────────────── │
│ HN: HN000123 | ชื่อ-สกุล: นายสมชาย ใจดี │
│ อายุ/เพศ: 30 ปี / ชาย | แพทย์ผู้สั่ง: นพ.สมชาย │
│ │
│ 🧪 ผลการตรวจ │
│ ───────────────────────────────────────────────────────── │
│ [☐ เลือกทั้งหมด (5)] เลือกแล้ว: 0 รายการ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │☐│ รายการตรวจ │ ผลการตรวจ │ ค่าปกติ │ Flag │ หมายเหตุ│ │ │
│ ├─┼────────────┼───────────┼─────────┼──────┼─────────┤ │ │
│ │☐│ WBC │ 8.5 x10³/µL│ 4.5-11.0│Normal│ - │[✓][✗]│
│ │☐│ Hemoglobin │ 14.2 g/dL │ 13.5-17.5│Normal│ - │[✓][✗]│
│ │☐│ Platelet │ 50 x10³/µL│ 150-400 │🔴Critical│ - │[✓][✗]│
│ │☐│ HIV Load │ 📄 PDF │ - │ - │🌐 Outlab│[✓][✗]│
│ │ │ (Outlab) │ Report │ │ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ลงผลโดย: เทคนิคแล็บสมหญิง | วันที่: 26/12/67 14:30 │
│ │
│ [ยกเลิก] [อนุมัติที่เลือก (0)] [ปฏิเสธที่เลือก] [อนุมัติทั้งหมด]│
└─────────────────────────────────────────────────────────────┘
Features:
3.1 Critical Alert Banner (แสดงถ้ามี Critical)
<div class="critical-alert">
🚨 พบผลวิกฤต 2 รายการ<br>
กรุณาตรวจสอบอย่างละเอียดก่อนอนุมัติและแจ้งแพทย์ทันที
</div>
3.2 Patient Info Section - HN, ชื่อ-สกุล (จาก currentOrder) - อายุ/เพศ (จาก PatientService, ถ้ามี) - แพทย์ผู้สั่ง - วันที่สั่ง Order
3.3 Selection Toolbar
<div class="selection-toolbar">
<div>
<input type="checkbox" id="selectAllResults" onchange="toggleSelectAll()">
<label>เลือกทั้งหมด (<span id="totalResultsCount">5</span>)</label>
</div>
<div>
เลือกแล้ว: <strong id="selectedCountInModal">0</strong> รายการ
</div>
</div>
Selection Logic:
// Track selected result IDs
let selectedResultIds = [];
// Update selection
function updateSelection() {
const checkboxes = document.querySelectorAll('.result-select-cb');
selectedResultIds = [];
checkboxes.forEach(cb => {
if (cb.checked) {
selectedResultIds.push(parseInt(cb.dataset.resultId));
// Highlight row
document.getElementById(`result-row-${cb.dataset.resultId}`).classList.add('selected');
} else {
document.getElementById(`result-row-${cb.dataset.resultId}`).classList.remove('selected');
}
});
// Update count displays
document.getElementById('selectedCount').textContent = selectedResultIds.length;
document.getElementById('selectedCountInModal').textContent = selectedResultIds.length;
// Enable/disable buttons
document.getElementById('approveSelectedBtn').disabled = (selectedResultIds.length === 0);
document.getElementById('rejectSelectedBtn').disabled = (selectedResultIds.length === 0);
}
// Toggle select all
function toggleSelectAll() {
const selectAll = document.getElementById('selectAllResults');
const checkboxes = document.querySelectorAll('.result-select-cb');
checkboxes.forEach(cb => {
cb.checked = selectAll.checked;
});
updateSelection();
}
3.4 Results Table
Columns: 1. ☐ Checkbox: เลือกรายการ (data-result-id) 2. รายการตรวจ: Item Name + Code + Outlab Badge (ถ้ามี) 3. ผลการตรวจ: - In-house: Result Value + Unit - Outlab: PDF Icon + "PDF Report พร้อมแล้ว" + Attachment Details 4. ค่าปกติ: Reference Range Text 5. Flag: Badge (Normal/High/Low/Critical) 6. หมายเหตุ: Lab Note (ถ้ามี) 7. การดำเนินการ: [✓] Quick Approve | [✗] Quick Reject
Outlab Result Display:
<!-- Outlab with Attachment -->
<div style="background: white; border: 2px solid #667eea; border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-file-pdf" style="color: #dc3545; font-size: 1.5rem;"></i>
<div>
<strong>HIV_Result_26122567.pdf</strong><br>
<small>1.2 MB | ห้องแล็บศิริราช</small>
</div>
</div>
<small>📥 Upload: 26/12/67 14:30</small><br>
<small style="color: #667eea; font-style: italic;">
HIV Viral Load: < 50 copies/mL (Undetectable)
</small>
</div>
Attachment Model:
{
"fileName": "HIV_Result_26122567.pdf",
"fileSize": 1228800,
"labName": "ห้องแล็บศิริราช",
"uploadedAt": "2024-12-26T14:30:00",
"note": "HIV Viral Load: < 50 copies/mL (Undetectable)"
}
3.5 Quick Action Buttons (ในแต่ละ Row)
[✓ Quick Approve] (Green Button)
// quickApprove(resultId)
// - Select only this result
// - Call approveSelectedResults([resultId])
// - Close modal
// - Reload data
[✗ Quick Reject] (Red Button)
// quickReject(resultId)
// - Select only this result checkbox
// - Call showRejectModal()
Section 4: Action Buttons (Modal Footer)
Four Action Options:
4.1 [ยกเลิก] (Gray) - Close modal - No changes
4.2 [อนุมัติที่เลือก (X)] (Green, Disabled if none selected)
// approveSelectedResults()
// - Validate: selectedResultIds.length > 0
// - Confirm dialog
// - Call labResultService.approveSelectedResults(selectedResultIds, ...)
// - Show success message
// - Close modal, reload data
4.3 [ปฏิเสธที่เลือก] (Orange, Disabled if none selected) - Show Reject Modal (see Section 5)
4.4 [อนุมัติทั้งหมด] (Green, Always enabled)
// approveAllResults()
// - Confirm dialog
// - Call labResultService.approveAllResults(orderNumber, ...)
// - Show success message + Critical alert (if any)
// - Close modal, reload data
Response Handling:
// Success response
{
success: true,
approvedCount: 5,
skippedCount: 0,
criticalResults: [
{ labItemName: 'Platelet', result: '50 x10³/µL' }
]
}
// Display message
let message = `อนุมัติสำเร็จ ${approvedCount} รายการ`;
if (skippedCount > 0) {
message += `<br>ข้าม ${skippedCount} รายการ`;
}
if (criticalResults.length > 0) {
message += `<br><strong style="color: #dc3545;">พบผลวิกฤต ${criticalResults.length} รายการ - กรุณาแจ้งแพทย์ทันที</strong>`;
}
showAlert(message, 'success');
Section 5: Reject Modal (ปฏิเสธผลการตรวจ)
Trigger: Click [ปฏิเสธที่เลือก] or [Quick Reject]
UI Layout:
┌─────────────────────────────────────────────────────────────┐
│ ⚠️ ปฏิเสธผลการตรวจ [✕] │
├─────────────────────────────────────────────────────────────┤
│ รายการที่เลือก: │
│ • WBC (8.5 x10³/µL) │
│ • Platelet (50 x10³/µL) │
│ │
│ 📝 เหตุผลในการปฏิเสธ: * │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ผลผิดปกติผิดธรรมชาติ ไม่สอดคล้องกับอาการผู้ป่วย │ │
│ │ ต้องการตรวจซ้ำเพื่อยืนยัน │ │
│ └────────────────────────────────────────────────────────┘ │
│ 💡 ตัวอย่างเหตุผล: ผลผิดปกติผิดธรรมชาติ, │
│ Specimen Hemolysis/Clotted, ผู้ป่วย Fasting ไม่ครบ, │
│ ต้องการตรวจซ้ำเพื่อยืนยัน, QC ไม่ผ่าน, เครื่องขัดข้อง│
│ │
│ ☐ ต้องการเก็บ Specimen ใหม่ │
│ (เลือกเมื่อต้องการให้กลับไปขั้นตอน Specimen Collection)│
│ │
│ ⚠️ คำเตือน: รายการที่ถูกปฏิเสธจะส่งกลับไปให้ผู้ลงผล │
│ แก้ไขและตรวจซ้ำ │
│ │
│ [ยกเลิก] [✓ ยืนยันปฏิเสธ] │
└─────────────────────────────────────────────────────────────┘
Form Fields:
5.1 Selected Items List
// Build list from selectedResultIds
const selectedResults = currentResults.filter(r => selectedResultIds.includes(r.id));
const itemsList = selectedResults.map(r =>
`<li><strong>${r.labItemName}</strong> (${r.getResultWithUnit()})</li>`
).join('');
5.2 Rejection Reason (Required)
<textarea id="rejectionReason"
placeholder="กรุณาระบุเหตุผลในการปฏิเสธ..."
required></textarea>
Common Reasons: - ผลผิดปกติผิดธรรมชาติ - ไม่สอดคล้องกับอาการผู้ป่วย - Specimen Hemolysis/Clotted - ผู้ป่วย Fasting ไม่ครบ - ต้องการตรวจซ้ำเพื่อยืนยัน - QC ไม่ผ่าน - เครื่องขัดข้อง
5.3 Require New Specimen Checkbox
<input type="checkbox" id="requireNewSpecimen">
<label>
ต้องการเก็บ Specimen ใหม่<br>
<small>เลือกเมื่อต้องการให้กลับไปขั้นตอน Specimen Collection</small>
</label>
Behavior:
- Checked: Order/Specimen status → rejected_specimen
- Unchecked: Result status → rejected (กลับไป Result Entry)
5.4 Confirm Reject Logic
async function confirmReject() {
// Validate
const reason = document.getElementById('rejectionReason').value.trim();
if (!reason) {
showAlert('กรุณาระบุเหตุผลในการปฏิเสธ', 'warning');
return;
}
const requireNewSpecimen = document.getElementById('requireNewSpecimen').checked;
// Confirm
if (!confirm(`ยืนยันการปฏิเสธ ${selectedResultIds.length} รายการหรือไม่?\n\nเหตุผล: ${reason}\n${requireNewSpecimen ? '\n⚠️ ต้องเก็บ Specimen ใหม่' : ''}`)) {
return;
}
// Call service
const result = await labResultService.rejectSelectedResults(
selectedResultIds,
currentUser.id,
currentUser.name,
'FREE_TEXT', // reasonCode
reason, // reason text
'', // note (merged with reason)
requireNewSpecimen
);
if (result.success) {
let message = `ปฏิเสธสำเร็จ ${result.rejectedCount} รายการ`;
if (requireNewSpecimen) {
message += '<br>📋 รายการถูกส่งกลับไปขั้นตอน Specimen Collection';
} else {
message += '<br>📋 รายการถูกส่งกลับให้ผู้ลงผลแก้ไข';
}
showAlert(message, 'success');
closeRejectModal();
closeApprovalModal();
// Reload
setTimeout(() => {
loadPendingApprovals();
updateStatistics();
}, 1000);
}
}
⚙️ Technical Features
1. Data Loading & Grouping
// Load pending approvals
function loadPendingApprovals() {
// 1. Get preliminary results
const preliminaryResults = labResultService.getPreliminaryResults();
// 2. Group by order number
const orderGroups = {};
for (const result of preliminaryResults) {
if (!orderGroups[result.orderNumber]) {
orderGroups[result.orderNumber] = [];
}
orderGroups[result.orderNumber].push(result);
}
// 3. Get orders
const orders = [];
for (const orderNumber in orderGroups) {
const order = labOrderService.getOrderByNumber(orderNumber);
if (order) {
orders.push({
order: order,
results: orderGroups[orderNumber]
});
}
}
// 4. Display
displayOrders(orders);
}
2. Outlab Detection
// Check if order has outlab items
const hasOutlabItems = order.items && order.items.some(item => {
const labItem = window.labItems?.find(li => li.id === item.itemId);
return labItem?.isOutsourced === true;
});
// Count outlab items
const outlabItemCount = order.items.filter(item => {
const labItem = window.labItems?.find(li => li.id === item.itemId);
return labItem?.isOutsourced === true;
}).length;
// Determine badge
const isAllOutlab = outlabItemCount === order.items.length;
const badgeText = isAllOutlab ? 'OUTLAB' : `MIXED (${outlabItemCount}/${order.items.length})`;
3. Partial Approval Flow
// Approve selected results
approveSelectedResults: async function(resultIds, approvedBy, approvedByName) {
let approvedCount = 0;
let skippedCount = 0;
const criticalResults = [];
for (const id of resultIds) {
const result = this.results.find(r => r.id === id);
if (!result || result.status !== 'preliminary') {
skippedCount++;
continue;
}
// Update status
result.status = 'final';
result.approvedBy = approvedBy;
result.approvedByName = approvedByName;
result.approvedAt = new Date().toISOString();
// Track critical
if (result.isCritical()) {
criticalResults.push({
labItemName: result.labItemName,
result: result.getResultWithUnit()
});
}
approvedCount++;
}
// Save
this.saveToLocalStorage();
// Check if all results in order are approved
const orderNumber = this.results.find(r => resultIds.includes(r.id))?.orderNumber;
if (orderNumber) {
this.checkOrderCompletion(orderNumber);
}
return {
success: true,
approvedCount,
skippedCount,
criticalResults
};
}
4. Rejection Flow
// Reject selected results
rejectSelectedResults: async function(
resultIds,
rejectedBy,
rejectedByName,
reasonCode,
reason,
note,
requireNewSpecimen
) {
let rejectedCount = 0;
let skippedCount = 0;
for (const id of resultIds) {
const result = this.results.find(r => r.id === id);
if (!result || result.status !== 'preliminary') {
skippedCount++;
continue;
}
// Update result status
result.status = 'rejected';
result.rejectedBy = rejectedBy;
result.rejectedByName = rejectedByName;
result.rejectedAt = new Date().toISOString();
result.rejectionReason = reason;
result.rejectionReasonCode = reasonCode;
// Update order/specimen status if require new specimen
if (requireNewSpecimen) {
const order = labOrderService.getOrderByNumber(result.orderNumber);
if (order) {
order.status = 'rejected_specimen';
labOrderService.updateOrder(order);
}
}
rejectedCount++;
}
// Save
this.saveToLocalStorage();
return {
success: true,
rejectedCount,
skippedCount
};
}
5. Statistics Calculation
// Get statistics
getStatistics: function() {
const today = new Date().toDateString();
return {
preliminary: this.results.filter(r => r.status === 'preliminary').length,
critical: this.results.filter(r => r.isCritical() && r.status === 'preliminary').length,
final: this.results.filter(r => {
return r.status === 'final' &&
r.approvedAt &&
new Date(r.approvedAt).toDateString() === today;
}).length
};
}
🎨 Design System
Critical Alert Styling
.critical-alert {
background: #f8d7da;
border: 2px solid #dc3545;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
color: #721c24;
}
.critical-alert i {
color: #dc3545;
font-size: 1.2rem;
margin-right: 0.5rem;
}
Selection Highlighting
.result-select-cb:checked ~ * {
background: #e3f2fd; /* Light blue highlight */
}
tr.selected {
background: #e3f2fd !important;
border-left: 4px solid #2196f3;
}
Outlab Badge Gradient
.outlab-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-weight: 600;
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
}
📱 Responsive Behavior
Desktop (> 1200px)
- Statistics: 3 columns (auto-fit)
- Results Table: Full width, all columns visible
- Modal: 900px width
Tablet (768px - 1200px)
- Statistics: 2-3 columns (wrap)
- Results Table: Horizontal scroll
- Modal: 90% width
Mobile (< 768px)
- Statistics: 1 column (stack)
- Results Table: Hide checkbox/action columns
- Modal: 95% width, simplified layout
💡 Best Practices (UX/UI)
- Critical Visibility:
- Red alert banner at order and modal level
- Pulse animation on critical badges
-
Force review before approval
-
Partial Approval Flexibility:
- Can approve/reject individual items
- Quick actions ([✓]/[✗]) for single-item workflows
-
Bulk selection for batch workflows
-
Clear Rejection Workflow:
- Required reason text (cannot bypass)
- Option to send back to Specimen Collection vs Result Entry
-
Clear warning messages
-
Outlab Integration:
- Visual distinction (gradient badge, icon)
- Show PDF/Image attachment details
- Support mixed orders (some in-house, some outlab)
🐛 Known Issues & Limitations
- No PDF Preview: Cannot preview PDF in browser (Future: Inline PDF viewer)
- No Batch Edit: Cannot edit rejection reason for multiple items at once
- No Approval History: No audit log of who approved what when (Future: Audit table)
- No Re-approval: Once rejected, cannot un-reject (must re-enter result)
- No Notification: No real-time notification to doctor (Future: SMS/Email)
📊 Business Rules
- Approval Requirements:
- Only
preliminaryresults can be approved - Must be Lab Supervisor/Lab Chief role
-
Critical results must be notified to doctor immediately
-
Partial Approval Rules:
- Can approve subset of results in an order
- Order status remains
pending_approvaluntil all results approved -
Once all results approved → Order status =
approved -
Rejection Rules:
- Must provide rejection reason (free text)
- If "Require New Specimen" → Order/Specimen status =
rejected_specimen - If not → Result status =
rejected(back to Result Entry) -
Rejected results cannot be approved (must re-enter)
-
Critical Result Rules:
- Show red alert banner
- Display in statistics dashboard
- Success message includes critical count + reminder to notify doctor
-
No auto-notification (manual responsibility)
-
Outlab Result Rules:
- Must have file attachment (PDF/Image)
- Can approve outlab results same as in-house
- Show attachment details in approval modal
7. Utility Pages - เครื่องมือช่วยเหลือ (Debugging & Admin)
📌 ภาพรวม
หน้าสำหรับ Developer/Admin ใช้ในการตรวจสอบ, จัดการ, และดีบักข้อมูล Mock Data ในระบบแล็บ
7.1 debug-lab-data.html - ตรวจสอบข้อมูลแล็บ
🎯 วัตถุประสงค์
- Debug Tool: ตรวจสอบสถานะข้อมูลใน LocalStorage
- Statistics Dashboard: แสดงสรุปจำนวน Orders/Results แบบละเอียด
- Data Management: สร้าง/รีเซ็ต/ลบข้อมูล Mock Data
- Data Inspection: ตรวจสอบโครงสร้างข้อมูล JSON
- Export Data: ดาวน์โหลดข้อมูลเป็นไฟล์ JSON
📊 File Statistics
- Total Lines: 409 lines
- Components: Statistics Dashboard, Action Buttons, Data Viewers, LocalStorage Inspector
- Dependencies: LabOrder.js, LabResult.js, LabOrderService, LabResultService
🧩 UI Components
Section 1: Statistics Dashboard (6 Cards)
┌─────────────────────────────────────────────────────────────┐
│ 📊 สถิติข้อมูล │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 25 │ │ 12 │ │ 45 │ │
│ │ Total │ │ ส่ง LIS │ │ Total │ │
│ │ Orders │ │ แล้ว │ │ Results │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 🟢 Green 🔵 Blue 🔵 Blue │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 8 │ │ 3 │ │ 28 │ │
│ │ รอ │ │ Critical │ │ Approved │ │
│ │ Approve │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 🟡 Yellow 🔴 Red 🟢 Green │
└─────────────────────────────────────────────────────────────┘
Statistics Calculation:
function refreshStats() {
const orders = labOrderService.getAllOrders();
const results = labResultService.getAllResults();
const ordersByStatus = {
pending: orders.filter(o => o.status === 'pending').length,
confirmed: orders.filter(o => o.status === 'confirmed').length,
sent_to_lis: orders.filter(o => o.status === 'sent_to_lis').length,
in_progress: orders.filter(o => o.status === 'in_progress').length,
completed: orders.filter(o => o.status === 'completed').length
};
const resultsByStatus = {
pending: results.filter(r => r.status === 'pending').length,
preliminary: results.filter(r => r.status === 'preliminary').length,
final: results.filter(r => r.status === 'final').length,
critical: results.filter(r => r.isCritical && r.isCritical()).length
};
// Display 6 stat boxes...
}
Section 2: Data Management Actions (4 Buttons)
┌─────────────────────────────────────────────────────────────┐
│ ⚙️ การจัดการข้อมูล │
├─────────────────────────────────────────────────────────────┤
│ [➕ สร้าง Mock Data] [🔄 รีเซ็ตข้อมูลทั้งหมด] │
│ [🗑️ ลบข้อมูลทั้งหมด] [↻ รีเฟรชสถิติ] │
│ │
│ ✅ สร้าง Mock Data สำเร็จ! │
└─────────────────────────────────────────────────────────────┘
Button Functions:
2.1 [➕ สร้าง Mock Data] (Green)
async function createMockData() {
showMessage('กำลังสร้าง Mock Data...', 'info');
try {
// Create orders first
const ordersResult = await labOrderService.createMockOrders();
console.log('Orders:', ordersResult);
// Create preliminary results
const resultsResult = await labResultService.createMockPreliminaryResults();
console.log('Results:', resultsResult);
refreshStats();
showMessage('✅ สร้าง Mock Data สำเร็จ!', 'success');
} catch (error) {
console.error('Error:', error);
showMessage('❌ เกิดข้อผิดพลาด: ' + error.message, 'danger');
}
}
2.2 [🔄 รีเซ็ตข้อมูลทั้งหมด] (Orange)
async function resetAllData() {
if (!confirm('ต้องการรีเซ็ตข้อมูลทั้งหมดหรือไม่?')) return;
showMessage('กำลังรีเซ็ตข้อมูล...', 'info');
try {
// Clear localStorage
localStorage.removeItem('his_lab_orders');
localStorage.removeItem('his_lab_metadata');
localStorage.removeItem('his_lab_results');
localStorage.removeItem('his_lab_results_metadata');
// Reinitialize services
await labOrderService.initializeData();
await labResultService.initializeData();
// Create fresh mock data
await labOrderService.createMockOrders();
await labResultService.createMockPreliminaryResults();
refreshStats();
showMessage('✅ รีเซ็ตข้อมูลสำเร็จ! กรุณารีเฟรชหน้าอื่นๆ', 'success');
} catch (error) {
console.error('Error:', error);
showMessage('❌ เกิดข้อผิดพลาด: ' + error.message, 'danger');
}
}
2.3 [🗑️ ลบข้อมูลทั้งหมด] (Red)
function clearAllData() {
if (!confirm('ต้องการลบข้อมูลทั้งหมดหรือไม่? (จะไม่สร้างข้อมูลใหม่)')) return;
// Remove all lab-related keys
localStorage.removeItem('his_lab_orders');
localStorage.removeItem('his_lab_metadata');
localStorage.removeItem('his_lab_results');
localStorage.removeItem('his_lab_results_metadata');
refreshStats();
showMessage('✅ ลบข้อมูลทั้งหมดแล้ว', 'warning');
}
2.4 [↻ รีเฟรชสถิติ] (Blue)
- Call refreshStats() function
- Update all 6 stat boxes
Section 3: Orders List Viewer
┌─────────────────────────────────────────────────────────────┐
│ 📋 รายการ Orders │
│ [โหลดรายการ] │
├─────────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────┐ │
│ │ LAB-20241226-0001 - นายสมชาย ใจดี │ │
│ │ Status: sent_to_lis | Priority: STAT | Items: 5 │ │
│ └────────────────────────────────────────────────────────┘ │
│ (Border: Green = confirmed, Yellow = pending, Gray = completed)│
└─────────────────────────────────────────────────────────────┘
Load Function:
function loadOrders() {
const orders = labOrderService.getAllOrders();
const html = orders.map(order => `
<div class="order-item ${order.status}">
<strong>${order.orderNumber}</strong> - ${order.patientName || '-'}<br>
<small>Status: ${order.status} | Priority: ${order.priority} | Items: ${order.items.length}</small>
</div>
`).join('');
document.getElementById('ordersList').innerHTML = html || '<em>ไม่มีข้อมูล</em>';
}
Section 4: Results List Viewer
┌─────────────────────────────────────────────────────────────┐
│ 🧪 รายการ Results │
│ [โหลดรายการ] │
├─────────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────┐ │
│ │ LAB-20241226-0001 - WBC │ │
│ │ Value: 8.5 | Status: preliminary │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ LAB-20241226-0001 - Platelet ⚠️ CRITICAL │ │
│ │ Value: 50 | Status: preliminary │ │
│ └────────────────────────────────────────────────────────┘ │
│ (Border: Yellow = preliminary, Green = final, Red = critical)│
└─────────────────────────────────────────────────────────────┘
Load Function:
function loadResults() {
const results = labResultService.getAllResults();
const html = results.map(result => {
const critical = result.isCritical && result.isCritical();
return `
<div class="result-item ${result.status} ${critical ? 'critical' : ''}">
<strong>${result.orderNumber}</strong> - ${result.labItemName}<br>
<small>Value: ${result.resultValue || '-'} | Status: ${result.status} ${critical ? '⚠️ CRITICAL' : ''}</small>
</div>
`;
}).join('');
document.getElementById('resultsList').innerHTML = html || '<em>ไม่มีข้อมูล</em>';
}
Section 5: LocalStorage Inspector
┌─────────────────────────────────────────────────────────────┐
│ 💾 LocalStorage Inspector │
│ [ตรวจสอบ LocalStorage] [ดาวน์โหลดข้อมูล (JSON)] │
├─────────────────────────────────────────────────────────────┤
│ orders: │
│ { │
│ "orders": [ │
│ { │
│ "id": 1, │
│ "orderNumber": "LAB-20241226-0001", │
│ "hn": "HN000123", │
│ ... │
│ } │
│ ] │
│ } │
│ │
│ ordersMeta: │
│ { "nextId": 26, "lastUpdated": "2024-12-26T..." } │
└─────────────────────────────────────────────────────────────┘
Inspection Function:
function inspectLocalStorage() {
const data = {
orders: localStorage.getItem('his_lab_orders'),
ordersMeta: localStorage.getItem('his_lab_metadata'),
results: localStorage.getItem('his_lab_results'),
resultsMeta: localStorage.getItem('his_lab_results_metadata')
};
const html = Object.entries(data).map(([key, value]) => {
if (!value) return `<div><strong>${key}:</strong> <em>null</em></div>`;
const parsed = JSON.parse(value);
return `
<div style="margin-bottom: 1rem;">
<strong>${key}:</strong>
<pre style="background: white; padding: 0.5rem; border-radius: 4px; margin-top: 0.25rem; overflow-x: auto;">
${JSON.stringify(parsed, null, 2)}
</pre>
</div>
`;
}).join('');
document.getElementById('localStorageData').innerHTML = html;
}
Download Function:
function downloadData() {
const data = {
orders: labOrderService.getAllOrders().map(o => o.toJSON ? o.toJSON() : o),
results: labResultService.getAllResults().map(r => r.toJSON ? r.toJSON() : r),
metadata: {
orders: labOrderService.getMetadata(),
results: labResultService.getMetadata()
},
exportedAt: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `lab-data-${new Date().getTime()}.json`;
a.click();
URL.revokeObjectURL(url);
showMessage('✅ ดาวน์โหลดข้อมูลแล้ว', 'success');
}
Output File Example:
{
"orders": [...],
"results": [...],
"metadata": {
"orders": { "nextId": 26, "lastUpdated": "2024-12-26T..." },
"results": { "nextId": 101, "lastUpdated": "2024-12-26T..." }
},
"exportedAt": "2024-12-26T14:30:00.000Z"
}
💡 Use Cases
- Development Testing: สร้าง Mock Data เพื่อทดสอบหน้า UI
- Bug Investigation: ตรวจสอบข้อมูลใน LocalStorage เมื่อเกิดข้อผิดพลาด
- Data Reset: รีเซ็ตข้อมูลเมื่อต้องการเริ่มต้นใหม่
- Data Export: ดาวน์โหลดข้อมูลเพื่อวิเคราะห์หรือสำรองข้อมูล
- Statistics Monitoring: ตรวจสอบจำนวนข้อมูลในแต่ละ Status
7.2 clear-storage.html - ล้างข้อมูล LocalStorage (Quick Reset)
🎯 วัตถุประสงค์
- Quick Clear: ล้างข้อมูล LAB LocalStorage อย่างรวดเร็ว
- Simple UI: หน้าเดียวจบ ไม่ซับซ้อน
- Redirect: เปลี่ยนหน้าไปยัง Order Queue เพื่อสร้างข้อมูลใหม่
📊 File Statistics
- Total Lines: ~100 lines (สั้น, minimal)
- Components: Clear Button, Redirect Button, Success Message
- Dependencies: None (Pure HTML + JavaScript)
🧩 UI Layout
┌─────────────────────────────────────────────────────────────┐
│ │
│ 🧹 ล้างข้อมูล LocalStorage │
│ │
│ ล้างข้อมูล LAB Orders ทั้งหมดใน LocalStorage │
│ และสร้างข้อมูลทดสอบใหม่อีกครั้ง │
│ │
│ [🗑️ ล้างข้อมูล LocalStorage] │
│ │
│ [→ ไปหน้า Order Queue] │
│ │
│ ✅ ล้างข้อมูลสำเร็จ! │
│ กรุณาไปหน้า Order Queue เพื่อสร้างข้อมูลทดสอบใหม่ │
│ │
└─────────────────────────────────────────────────────────────┘
Background: Gradient Purple-Blue
⚙️ Functions
Clear Storage Function:
function clearStorage() {
// Clear LAB-related LocalStorage keys
localStorage.removeItem('his_lab_orders');
localStorage.removeItem('his_lab_metadata');
localStorage.removeItem('his_specimens');
localStorage.removeItem('his_specimen_metadata');
// Show success message
const result = document.getElementById('result');
result.className = 'success';
result.style.display = 'block';
result.innerHTML = `
<strong>✅ ล้างข้อมูลสำเร็จ!</strong><br>
<small>กรุณาไปหน้า Order Queue เพื่อสร้างข้อมูลทดสอบใหม่</small>
`;
}
Redirect Function:
function goToQueue() {
window.location.href = 'order-queue.html';
}
🎨 Design System
Centered Card Layout:
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 3rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
}
Button Styling:
.btn-danger {
background: #e74c3c;
color: white;
padding: 1rem 2rem;
font-size: 1.1rem;
border-radius: 8px;
}
.btn-danger:hover {
background: #c0392b;
transform: translateY(-2px);
}
.btn-primary {
background: #667eea;
color: white;
}
💡 Use Cases
- Quick Reset During Development: ลบข้อมูลเก่าและเริ่มต้นใหม่อย่างรวดเร็ว
- Demo Preparation: เคลียร์ข้อมูลก่อน Demo
- Testing Fresh State: ทดสอบระบบในสถานะเริ่มต้น (ไม่มีข้อมูลเก่า)
📊 Comparison: debug-lab-data.html vs clear-storage.html
| Feature | debug-lab-data.html | clear-storage.html |
|---|---|---|
| Purpose | Comprehensive debugging | Quick reset |
| UI Complexity | Complex (6 sections) | Minimal (1 card) |
| Statistics | ✅ Yes (6 cards) | ❌ No |
| Data Viewers | ✅ Yes (Orders, Results) | ❌ No |
| LocalStorage Inspector | ✅ Yes | ❌ No |
| Export JSON | ✅ Yes | ❌ No |
| Create Mock Data | ✅ Yes | ❌ No |
| Clear Data | ✅ Yes (+ Reset options) | ✅ Yes (Simple clear) |
| Target User | Developer | Anyone (Simple UI) |
| Dependencies | Lab Services | None |
🔧 Technical Notes
LocalStorage Keys (Lab Module)
// Orders
'his_lab_orders' // Array of LabOrder objects
'his_lab_metadata' // { nextId, lastUpdated }
// Results
'his_lab_results' // Array of LabResult objects
'his_lab_results_metadata' // { nextId, lastUpdated }
// Specimens
'his_specimens' // Array of Specimen objects
'his_specimen_metadata' // { nextId, nextSpecimenNumber }
When to Use Each Tool
Use debug-lab-data.html when: - Investigating data corruption issues - Need to see statistics breakdown - Want to export data for analysis - Need to inspect JSON structure - Testing service methods
Use clear-storage.html when: - Quick reset needed - Starting fresh demo - Simple clear operation without inspection - No need for data export
🐛 Known Limitations
- No Undo: ลบแล้วไม่สามารถกู้คืนได้ (ต้องสร้าง Mock Data ใหม่)
- No Selective Delete: ไม่สามารถลบเฉพาะ Order/Result ที่เลือก (ลบทั้งหมด)
- No Import: ไม่สามารถนำเข้าข้อมูลจากไฟล์ JSON (Export only)
- No Backup: ไม่มีระบบสำรองข้อมูลอัตโนมัติ
- Browser-Specific: ข้อมูลอยู่ใน Browser localStorage (ไม่ sync ข้าม Browser)
💡 Future Enhancements (Suggestions)
- Import Data: นำเข้าข้อมูลจากไฟล์ JSON
- Selective Delete: ลบเฉพาะ Order/Result ที่เลือก
- Auto Backup: สำรองข้อมูลอัตโนมัติทุก X นาที
- Data Validation: ตรวจสอบความถูกต้องของข้อมูล
- Performance Metrics: แสดงขนาดข้อมูลใน LocalStorage (KB/MB)
- Visual Diff: เปรียบเทียบข้อมูลก่อน-หลัง Reset
สรุป 04-PAGES-FEATURES.md
เอกสารนี้ครอบคลุม 7 หน้าหลักในระบบ LAB:
- ✅ index.html: Landing Page + Navigation Hub (4 sections)
- ✅ doctor-order.html: Doctor Order Entry (Patient search, Lab selection, Print)
- ✅ order-queue.html: Order Queue Management (Statistics, Filters, Modals)
- ✅ specimen-collection.html: Specimen Collection (QC, Numbering, Print Labels)
- ✅ result-entry.html: Result Entry (Delta Check, Critical Alert, Outlab Upload)
- ✅ result-approval.html: Result Approval (Partial Approval, Rejection)
- ✅ debug-lab-data.html + clear-storage.html: Debugging Tools
จำนวนบรรทัดทั้งหมด: ~7,500+ บรรทัด
Target Audience: UX/UI Team, Product Owners, QA Testers, Developers
ถัดไป: สร้าง 05-DATA-STATUS-FLOW.md (Data Models + Status Transitions)