feat: Implement configurable printer system and enhance UAT workflow

Add comprehensive printer configuration support:
- New Printers.php config with role-based printer defaults (lab, phlebo, reception)
- Update LabelController for configurable printer routing with error handling
- Add ResponseTrait for proper JSON responses (success/error status)
- Update routes to accept optional printer parameter for label printing
- Add default printer configuration per role in shared config

Enhance report generation workflow:
- Support REPORT_LANG from CM_REQUESTS table for language preference
- Prioritize URL parameter, then database value, then default
- Add language info to PDF generation response (Indonesian/English)
- Update all report methods (view, eng, preview, generate) with unified logic

Improve UI and user experience:
- Add dialog_results_generate to all role dashboards (superuser, admin, lab, phlebo, cs)
- Update skeleton loading states widths in content requests
- Add printer selection capability in sample collection flow

Add comprehensive UAT documentation:
- New UAT_GDC_CMOD_Checklist.md with 150+ test cases
- Cover all roles: superuser, admin, lab, phlebo, cs, and cross-role scenarios
- Include acceptance criteria (functional, security, performance, usability, data integrity)
- Test categories: authentication, user management, validation, sample management, audit trail, reporting
- Detailed sign-off structure for stakeholders

Add barcode printing documentation:
- docs/barcode_print_all.php - all labels printing implementation
- docs/barcode_print_coll.php - collection label implementation
- docs/barcode_print_disp.php - dispatch label implementation

Update TODO tracking:
- Mark Reprint Label and PDF Generation as completed
- Update pending tasks for testing and audit trails
This commit is contained in:
mahdahar 2026-02-05 06:21:08 +07:00
parent 0b4fdcfe5f
commit cfb81201a2
22 changed files with 1448 additions and 106 deletions

12
TODO.md
View File

@ -3,9 +3,8 @@
**Last Updated:** 20260204
Pending:
- Auto generate PDF
- Print Eng Result
- Reprint Label (Add functionality to reprint labels)
- Test and fix Reprint label
- Test and fix PDF Generation
- Print Result Audit (Track when result reports are printed/exported, log user and timestamp)
Completed:
@ -31,4 +30,9 @@ Completed:
- Add Val1 Val2 on the result
- Show Print / PDF button when val1 val2 done
- Restrict Print/Save-to-PDF to CS Role only (Admin, Lab, CS can print/save)
- Add Receive to Audit
- Add Receive to Audit
- Put all action to dropdown on request / dashboard
- Auto generate PDF on second val
- Validate delay when loading result
- Reprint Label (Add functionality to reprint labels)
- Create Eng Result UI UX on request dashboard

606
UAT_GDC_CMOD_Checklist.md Normal file
View File

@ -0,0 +1,606 @@
# 📋 UAT (USER ACCEPTANCE TESTING) - CHECKLIST
## Sistem Manajemen Laboratorium - GDC CMOD
---
## 1. IDENTITAS DOKUMEN
| Item | Detail |
|------|--------|
| **Nama Proyek** | GDC CMOD - Laboratory Management System |
| **Versi Aplikasi** | 1.0 |
| **Tanggal UAT** | _______________ |
| **Durasi UAT** | _____ hari |
| **Dokumen Versi** | 1.0 |
| **Prepared By** | __________________ |
| **Reviewed By** | __________________ |
| **Approved By** | __________________ |
---
## 2. INSTRUKSI PENGISIAN
**PASS** = Test case berhasil sepenuhnya
**FAIL** = Test case gagal - ada defect
⏭️ **N/A** = Tidak dapat dilakukan
🚫 **BLOCKED** = Terhalang oleh issue lain
**Cara Pengisian:**
1. Centang checkbox [ ] jika test case LULUS (PASS)
2. Tulis "X" dalam kotak [X] jika test case GAGAL (FAIL)
3. Tambahkan catatan @ kolom "Issue/Jawaban" untuk kasus FAIL/N/A/BLOCKED
4. Beri nomor urut di "No." untuk tracking
---
## 3. TEST CASE - AUTENTIKASI & SESI
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-AUTH-001** | Login Berhasil | Login dengan username & password valid | Redirect ke dashboard sesuai role | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-002** | Login Gagal - Username Salah | Masukkan username tidak ada | Error "Invalid credentials" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-003** | Login Gagal - Password Salah | Password salah | Error invalid credentials | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-004** | Login Gagal - Akun Terkunci | Login ke akun locked dari legacy | Error "Account locked" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-005** | Logout Berhasil | Klik menu Logout | Session terhapus, redirect ke login | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-006** | Redirect ke Login Belum Login | Akses protected page tanpa login | Redirect otomatis ke halaman login | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-007** | Ganti Password Berhasil | Change password baru | Password berhasil diubah dan di-hash | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-008** | Ganti Password Gagal - Password Lama Salah | Masukkan password lama salah | Error "Invalid old password" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-AUTH-009** | Role-Based Redirect Login | Login dengan berbagai role | Redirect ke dashboard sesuai role | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Autentikasi: _ LULUS / _____ GAGAL
---
## 4. TEST CASE - SUPERUSER (ROLE 0)
### 4.1 DASHBOARD SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-001** | Tampilkan Semua Request | Login Superuser → `/superuser` | Tabel requests semua status ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-002** | Filter by Status | Pilih status di dashboard filter | Filter berfungsi sesuai status | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-003** | Filter by Date Range | Set date1 & date2 → filter | Filter tanggal berfungsi | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-004** | Search by Patient Name | Masukkan nama pasien | Tampilkan request dengan nama tsb | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-005** | Search by Lab Number | Masukkan Lab Number | Tampilkan request tsb | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-006** | Search by Register Number | Masukkan Register Number | Tampilkan request tsb | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-007** | Table Sorting | Klik header kolom | Kolom di-sort ASC/DESC | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-008** | Pagination | Klik halaman berbeda | Berpindah halaman dengan benar | [ ] PASS <br> [ ] FAIL | ___________ |
### 4.2 USER MANAGEMENT SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-009** | Tampilkan List User | Access `/superuser/users` | Tabel users ditampilkan lengkap | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-010** | Buat User Baru | Add User → isi form | User berhasil dibuat dan password di-hash | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-011** | Validasi Password Min 3 Karakter | Buat user dengan password < 3 char | Error "Password min 3 karakter" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-012** | Update User Role | Edit user → ubah role | Role berhasil diubah | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-013** | Update Password User | Edit user → ganti password | Password di-hash dengan benar | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-014** | Delete User | Delete user | User terhapus dari database | [ ] PASS <br> [ ] FAIL | ___________ |
### 4.3 VALIDASI SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-015** | Tampilkan Request Final | Access `/superuser/validate` | Hanya request "Fin" yang tampil | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-016** | Filter by Date Range Validasi | Set date1 & date2 → filter | Filter berfungsi di halaman validasi | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-017** | First Validation (VAL1) | Validasi request "Fin" | ISVAL1=1, VAL1USER & VAL1DATE diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-018** | Second Validation (VAL2) | Validasi dengan user berbeda | ISVAL2=1, request duavalidated | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-019** | Gagal Validasi Sendiri | Coba validasi ulang sama user | Error "Cannot validate own request" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-020** | Un-Validate Request | Un-validate dengan reason | ISVAL reset, PENDING diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-021** | Shortcut Keyboard | Enter=Validate, N=Skip, Esc=Close | Shortcut berfungsi dengan benar | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-022** | Progress Indicator | Buka validation modal | Progress muncul (posisi / total) | [ ] PASS <br> [ ] FAIL | ___________ |
### 4.4 SAMPLE MANAGEMENT SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-023** | Collect Sample | Buka dialog sample → Collect | STATUS=1, COLLECTIONDATE & USERID diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-024** | Un-Collect Sample | Buka dialog sample → Un-Collect | STATUS di-reset, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-025** | Un-Receive Sample | Un-receive sample yang received | Status berubah, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-026** | Print Label Individu | Print label per tube | Label tercetak dengan ZPL format | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-027** | Print Collection Label | Print collection label | Label collection tercetak | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-028** | Print All Labels | Print semua tubes | Semua labels tercetak | [ ] PASS <br> [ ] FAIL | ___________ |
### 4.5 AUDIT TRAIL SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-029** | View Request Audit | Buka audit dialog request | Audit trail timeline ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-030** | Filter by Category | Filter: All, Validation, Receive, Sample | Filter berfungsi sesuai kategori | [ ] PASS <br> [ ] FAIL | ___________ |
### 4.6 REPORT SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-031** | View Report Validated | View report request duavalidated | Report ditampilkan lengkap | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-032** | Cannot View Not Validated | View report belum duavalidated | Error: "Validated request required" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-033** | Print Report | Print report request validated | Print dialog buka, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-034** | Generate PDF | Generate PDF request validated | PDF berhasil di-generate | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-035** | Preview Report (No Audit) | Preview report tanpa log audit | Report tampil, tidak ada audit log | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-036** | English Version Report | Access `/report/{id}/eng` | Report dalam Bahasa Inggris | [ ] PASS <br> [ ] FAIL | ___________ |
### 4.7 FITUR TAMBAHAN SUPERUSER
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-SU-037** | Edit Comment | Edit comment di dashboard | Comment berubah tersimpan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-038** | Error Handling 404 | Akses URL tidak valid | Custom notfound page tampil | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-SU-039** | Error Handling Unauthorized | Coba akses role lain | Redirect ke `/unauthorized` | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Superuser: _ LULUS / _____ GAGAL
---
## 5. TEST CASE - ADMIN (ROLE 1)
### 5.1 DASHBOARD ADMIN
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-ADM-001** | Tampilkan Semua Request | Login Admin → `/admin` | Tabel requests semua status | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-002** | Filter & Search | Uji berbagai filter dan search | Semua berfungsi seperti Superuser | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-003** | Sample Collection Buttons Enabled | Buka dialog sample | Tombol Collect/Un-Coll/Un-Recv enabled | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-004** | Edit Comment Enabled | Edit comment dashboard | Comment dapat diubah | [ ] PASS <br> [ ] FAIL | ___________ |
### 5.2 VALIDASI ADMIN
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-ADM-005** | Tampilkan Request Final | Access `/admin/validate` | Hanya request "Fin" tampil | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-006** | First Validation (VAL1) | Validasi request "Fin" | ISVAL1=1, VAL1USER & VAL1DATE diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-007** | Second Validation (VAL2) | Validasi dengan user berbeda | ISVAL2=1, request duavalidated | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-008** | Gagal Validasi Sendiri | Coba validasi ulang sama user | Error "Cannot validate own request" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-009** | Un-Validate Request | Un-validate request validated | ISVAL reset, PENDING diset | [ ] PASS <br> [ ] FAIL | ___________ |
### 5.3 SAMPLE & FITUR LAIN ADMIN
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-ADM-010** | Collect Sample | Collect tube | STATUS=1, COLLECTIONDATE & USERID diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-011** | Un-Collect Sample | Un-collect tube | STATUS di-reset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-012** | Un-Receive Sample | Un-receive sample | Status berubah, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-013** | Print Labels | Print individual/collection/all | Semua labels tercetak | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-014** | View Audit Trail | Buka audit dialog | Timeline audit ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-015** | Tampilkan List User | Access `/admin/users` | Tabel users ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-016** | Buat User Baru | Add user lengkap | User berhasil dibuat dan di-hash | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-017** | Update User | Edit user → ubah data | User berhasil di-update | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-018** | Delete User | Delete user | User terhapus | [ ] PASS <br> [ ] FAIL | ___________ |
### 5.4 REPORT ADMIN
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-ADM-019** | View Report Validated | View report request validated | Report ditampilkan lengkap | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-020** | Print Report | Print report | Print dialog buka, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-ADM-021** | Generate PDF | Generate PDF report | PDF berhasil di-generate | [ ] PASS <br> [ ] FAIL | ___________ |
### 5.5 ROLE RESTRICTIONS ADMIN
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-ADM-PageNav** | Cannot Access Role Lain | Coba `/superuser`, `/lab`, `/phlebo`, `/cs` | Redirect ke unauthorized/error | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Admin: _ LULUS / _____ GAGAL
---
## 6. TEST CASE - LAB ANALYST (ROLE 2)
### 6.1 DASHBOARD LAB
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-LAB-001** | Filter Test Code Alphabetical | Login Lab → `/lab` | Hanya request TESTS A-Z yang tampil | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-002** | View Request Details | Buka request | Detail request ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-003** | Filter & Search dalam Batas Filter | Uji filter & search | Sesuai batas test code A-Z | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-004** | Sample Buttons Enabled | Buka dialog sample | Tombol Collect/Un-Coll/Un-Recv enabled | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-005** | Edit Comment Disabled | Cek comment field | Comment field non-editable | [ ] PASS <br> [ ] FAIL | ___________ |
### 6.2 VALIDASI LAB
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-LAB-006** | Tampilkan Request Final | Access `/lab/validate` | Hanya "Fin" dengan TESTS A-Z tampil | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-007** | First Validation (VAL1) | Validasi request "Fin" | ISVAL1=1, VAL1USER & VAL1DATE diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-008** | Second Validation (VAL2) | Validasi user berbeda | ISVAL2=1, request duavalidated | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-009** | Gagal Validasi Sendiri | Coba validasi sendiri | Error "Cannot validate own request" | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-010** | Un-Validate Disabled | Coba un-validate | Error/Unauthorized - cannot un-validate | [ ] PASS <br> [ ] FAIL | ___________ |
### 6.3 SAMPLE & FITUR LAIN LAB
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-LAB-011** | Collect Sample | Collect tube | STATUS=1, COLLECTIONDATE & USERID diset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-012** | Un-Collect Sample | Un-collect tube | STATUS di-reset | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-013** | Un-Receive Sample | Un-receive sample | Status berubah, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-014** | Print Labels | Print semua jenis label | Semua labels tercetak | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-015** | View Audit Trail | Buka audit dialog | Timeline audit ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
### 6.4 REPORT LAB
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-LAB-016** | View Report Validated | View report request validated | Report ditampilkan lengkap | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-017** | Print Report | Print report validated | Print dialog buka, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-LAB-018** | Generate PDF | Generate PDF | PDF berhasil di-generate | [ ] PASS <br> [ ] FAIL | ___________ |
### 6.5 ROLE RESTRICTIONS LAB
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-LAB-PageNav** | Cannot Access Role Lain | Coba `/superuser`, `/admin`, `/phlebo`, `/cs` | Redirect ke unauthorized/error | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Lab: _ LULUS / _____ GAGAL
---
## 7. TEST CASE - PHLEBOTOMIST (ROLE 3)
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-PHB-001** | Tampilkan Semua Request | Login Phlebo → `/phlebo` | Tabel requests semua status | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-002** | Filter & Search | Uji filter & search | Semua berfungsi | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-003** | Sample Collection Disabled | Buka dialog sample | Tombol Collect/Un-Coll/Un-Recv DISABLED | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-004** | Edit Comment Disabled | Cek comment field | Comment field non-editable | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-005** | Validation Page Denied | Coba `/phlebo/validate` | Halaman tidak tersedia/unauthorized | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-006** | View Sample Details (Read-Only) | Buka dialog sample | Sample details view-only | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-007** | Print Labels | Print individual/collection/all | Labels tercetak | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-008** | View Audit Trail | Buka audit dialog | Audit trail ditampilkan (read-only) | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-009** | Cannot View Report | Coba view report | Report tidak tersedia/error | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-010** | Cannot Print Report | Coba print report | Print tidak tersedia | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-011** | Cannot Generate PDF | Coba generate PDF | PDF tidak tersedia | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-PHB-PageNav** | Cannot Access Role Lain | Coba `/superuser`, `/admin`, `/lab`, `/cs` | Redirect ke unauthorized/error | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Phlebotomist: _ LULUS / _____ GAGAL
---
## 8. TEST CASE - CUSTOMER SERVICE (ROLE 4)
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-CS-001** | Tampilkan Semua Request | Login CS → `/cs` | Tabel requests semua status | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-002** | Filter & Search | Uji filter & search | Semua berfungsi | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-003** | Sample Collection Disabled | Buka dialog sample | Tombol Collect/Un-Coll/Un-Recv DISABLED | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-004** | Edit Comment Disabled | Cek comment field | Comment field non-editable | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-005** | Validation Page Denied | Coba `/cs/validate` | Halaman tidak tersailable | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-006** | View Sample Details (Read-Only) | Buka dialog sample | Sample details view-only | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-007** | Print Labels (Limited) | Print labels | Labels tercetak sesuai kapasitas | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-008** | View Audit Trail | Buka audit dialog | Audit trail ditampilkan | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-009** | View Report Validated | View report request validated | Report ditampilkan lengkap | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-010** | Print Report | Print report validated | Print dialog buka, audit log tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-011** | Generate PDF | Generate PDF | PDF berhasil di-generate | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-CS-PageNav** | Cannot Access Role Lain | Coba `/superuser`, `/admin`, `/lab`, `/phlebo` | Redirect ke unauthorized/error | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Customer Service: _ LULUS / _____ GAGAL
---
## 9. TEST CASE - FITUR CROSS-ROLE
| No. | ID | Judul Test Case | Langkah Utama | Expected Result | Hasil | Issue/Jawaban |
|-----|----|----------------|---------------|-----------------|-------|---------------|
| | **TC-X-001** | User Mgmt Role Restrictions | Lab/Phlebo/CS coba user management API | Error unauthorized | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-002** | Cross-Role Validation | Lab A validasi VAL1, Superuser B validasi VAL2 | Cross-role validation berhasil | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-003** | Un-Validate Role Restrictions | Lab/Phlebo/CS coba un-validate | Error unauthorized - only SU/Admin | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-004** | Un-Receive Role Restrictions | Lab/Phlebo/CS coba un-receive | Error unauthorized - only SU/Admin | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-005** | Report Access Restrictions | Coba view report semua role | SU/Admin/Lab/CS OK, Phlebo NOT | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-006** | Report Validated Requirement | View report hanya VAR1 vs duavalidated | Hanya duavalidated dapat dilihat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-007** | Comment Edit Restrictions | Lab/Phlebo/CS coba edit comment | Error - only SU/Admin editable | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-008** | Validasi tidak Pengaruhi Data Lain | Record data sebelum & sesudah validasi | Hanya status validasi berubah | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-009** | Un-Validate Mempertahankan Data | Record data sebelum & sesudah un-validate | Hanya status validasi berubah | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-010** | Audit Trail Logging | Lakukan berbagai aktivitas | Semua tercatat di audit log | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-011** | Concurrent Validation | 2 user buka validation dialog bersamaan | Validasi berhasil, tidak conflict | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-012** | Concurrent Sample Collection | 2 user collect tube berbeda bersamaan | Semua berhasil tercatat | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-013** | Session Timeout | Tunggu session timeout | Redirect ke login | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-014** | Password Hashing Security | Buat user → cek database | Password dalam HASH bukan plain | [ ] PASS <br> [ ] FAIL | ___________ |
| | **TC-X-015** | Legacy Read-Only | Cek koneksi & fungsi Firebird | Hanya READ dari Firebird, TIDAK WRITE | [ ] PASS <br> [ ] FAIL | ___________ |
### Ringkasan Cross-Role: _ LULUS / _____ GAGAL
---
## 10. KRITERIA PENERIMAAN (ACCEPTANCE CRITERIA)
### 10.1 Functional Criteria
| No. | Kriteria Functional | Hasil |
|-----|---------------------|-------|
| FC-01 | Autentikasi benar untuk semua role (0-4) | [ ] PASS <br> [ ] FAIL |
| FC-02 | Role-based access control (RBAC) berfungsi | [ ] PASS <br> [ ] FAIL |
| FC-03 | Dashboard menampilkan request sesuai filter & role | [ ] PASS <br> [ ] FAIL |
| FC-04 | User management CRUD berfungsi untuk Superuser & Admin | [ ] PASS <br> [ ] FAIL |
| FC-05 | Dual-level validation system (VAL1 & VAL2) berfungsi | [ ] PASS <br> [ ] FAIL |
| FC-06 | Validasi tidak bisa oleh user yang sama | [ ] PASS <br> [ ] FAIL |
| FC-07 | Un-validate berfungsi (Superuser & Admin only) | [ ] PASS <br> [ ] FAIL |
| FC-08 | Sample collection/un-collection berfungsi | [ ] PASS <br> [ ] FAIL |
| FC-09 | Un-receive sample berfungsi (Superuser & Admin only) | [ ] PASS <br> [ ] FAIL |
| FC-10 | Audit trail mencatat semua aktivitas krusial | [ ] PASS <br> [ ] FAIL |
| FC-11 | Report hanya untuk request duavalidated | [ ] PASS <br> [ ] FAIL |
| FC-12 | Report hanya accessible role (SU, Admin, Lab, CS) | [ ] PASS <br> [ ] FAIL |
| FC-13 | PDF generation berfungsi | [ ] PASS <br> [ ] FAIL |
| FC-14 | Label printing berfungsi (individual, collection, all) | [ ] PASS <br> [ ] FAIL |
| FC-15 | Comment editing berfungsi (Superuser & Admin only) | [ ] PASS <br> [ ] FAIL |
| FC-16 | Filter dan search bekerja dengan benar | [ ] PASS <br> [ ] FAIL |
| FC-17 | Pagination dan sorting tabel berfungsi | [ ] PASS <br> [ ] FAIL |
### Functional Summary: _ LULUS / _____ GAGAL
---
### 10.2 Security Criteria
| No. | Kriteria Security | Hasil |
|-----|-------------------|-------|
| SC-01 | Password disimpan dalam bentuk HASH (bukan plain) | [ ] PASS <br> [ ] FAIL |
| SC-02 | Session timeout bekerja dengan benar | [ ] PASS <br> [ ] FAIL |
| SC-03 | Tidak ada SQL injection vulnerability | [ ] PASS <br> [ ] FAIL |
| SC-04 | Role-based authorization terpenuhi | [ ] PASS <br> [ ] FAIL |
| SC-05 | Locked account tidak dapat login | [ ] PASS <br> [ ] FAIL |
| SC-06 | Audit trail mencatat semua aktivitas (traceability) | [ ] PASS <br> [ ] FAIL |
| SC-07 | Un-validate hanya Superuser & Admin | [ ] PASS <br> [ ] FAIL |
| SC-08 | Legacy Firebird system READ-ONLY | [ ] PASS <br> [ ] FAIL |
### Security Summary: _ LULUS / _____ GAGAL
---
### 10.3 Performance Criteria
| No. | Kriteria Performance | Target | Hasil Aktual | Pass/Fail |
|-----|----------------------|--------|--------------|-----------|
| PC-01 | Dashboard load time | < 3 detik | __________ detik | [ ] PASS <br> [ ] FAIL |
| PC-02 | Validation page load time | < 3 detik | __________ detik | [ ] PASS <br> [ ] FAIL |
| PC-03 | Report generation time | < 5 detik | __________ detik | [ ] PASS <br> [ ] FAIL |
| PC-04 | PDF generation time | < 30 detik | __________ detik | [ ] PASS <br> [ ] FAIL |
| PC-05 | Database query response time | < 2 detik | __________ detik | [ ] PASS <br> [ ] FAIL |
| PC-06 | Concurrent user support | Min. 5 user | _____ user simultaneous ok | [ ] PASS <br> [ ] FAIL |
### Performance Summary: _ LULUS / _____ GAGAL
---
### 10.4 Usability Criteria
| No. | Kriteria Usability | Hasil |
|-----|---------------------|-------|
| UC-01 | UI user-friendly dan mudah digunakan | [ ] PASS <br> [ ] FAIL |
| UC-02 | Shortcut keyboard (Enter, N, Esc) berfungsi | [ ] PASS <br> [ ] FAIL |
| UC-03 | Error messages jelas dan actionable | [ ] PASS <br> [ ] FAIL |
| UC-04 | Responsive design (bekerja di berbagai ukuran) | [ ] PASS <br> [ ] FAIL |
| UC-05 | Icons dan color-coding jelas | [ ] PASS <br> [ ] FAIL |
| UC-06 | Data terorganisir dengan baik | [ ] PASS <br> [ ] FAIL |
### Usability Summary: _ LULUS / _____ GAGAL
---
### 10.5 Data Integrity Criteria
| No. | Kriteria Data Integrity | Hasil |
|-----|-------------------------|-------|
| DC-01 | Validasi tidak menghapus/mengubah data lain | [ ] PASS <br> [ ] FAIL |
| DC-02 | Un-validate hanya menghapus status validasi | [ ] PASS <br> [ ] FAIL |
| DC-03 | Sample collection tidak modifikasi data lain | [ ] PASS <br> [ ] FAIL |
| DC-04 | Audit trail tercatat dengan benar | [ ] PASS <br> [ ] FAIL |
| DC-05 | Tidak ada data duplikasi concurrent access | [ ] PASS <br> [ ] FAIL |
| DC-06 | Status request sesuai workflow | [ ] PASS <br> [ ] FAIL |
| DC-07 | Dual-validation requirement terpenuhi | [ ] PASS <br> [ ] FAIL |
### Data Integrity Summary: _ LULUS / _____ GAGAL
---
## 11. KEPUTUSAN AKHIR UAT
### 11.1 Tolok Ukur Go-Live
Untuk sistem dinyatakan **APPROVED** untuk production:
| Category | LULUS | GAGAL | PERSEN LULUS | PASS? |
|----------|-------|-------|--------------|------|
| **Functional** | ___ | ___ | ___% | [ ] YES <br> [ ] NO |
| **Security** | ___ | ___ | ___% | [ ] YES <br> [ ] NO |
| **Performance** | ___ | ___ | ___% | [ ] YES <br> [ ] NO |
| **Usability** | ___ | ___ | ___% | [ ] YES <br> [ ] NO |
| **Data Integrity** | ___ | ___ | ___% | [ ] YES <br> [ ] NO |
**OVERALL PASS RATE:** ______% (dari 5 kategori)
---
### 11.2 Keputusan Final
**Pilih Salah Satu:**
- [ ] **✅ APPROVED GO-LIVE** - Semua criteria terpenuhi, sistem siap untuk production
- [ ] **⚠️ CONDITIONAL APPROVAL** - Sistem siap dengan catatan berikut:
```
____________________________________________________________
____________________________________________________________
____________________________________________________________
```
- [ ] **❌ NOT APPROVED** - Sistem belum siap, harus perbaiki issue berikut:
```
____________________________________________________________
____________________________________________________________
____________________________________________________________
```
---
### 11.3 Issue Critical yang Harus Diperbaiki
| No. | Issue | Severity | Impact | Assigned To | Deadline |
|-----|-------|----------|--------|-------------|----------|
| 1 | ________________________________ | High | ___________ | ___________ | _______ |
| 2 | ________________________________ | Medium | ___________ | ___________ | _______ |
| 3 | ________________________________ | Low | ___________ | ___________ | _______ |
| 4 | ________________________________ | | ___________ | ___________ | _______ |
| 5 | ________________________________ | | ___________ | ___________ | _______ |
---
## 12. CHECKLIST SIGN-OFF
### 12.1 UAT Team Sign-off
| Nama | Role | Signature | Sign-off Tanggal |
|------|------|-----------|------------------|
| __________________________ | UAT Tester 1 | _____________________ | ____/____/____ |
| __________________________ | UAT Tester 2 | _____________________ | ____/____/____ |
| __________________________ | UAT Tester 3 | _____________________ | ____/____/____ |
| __________________________ | Business Analyst | _____________________ | ____/____/____ |
---
### 12.2 Management Sign-off
| Nama | Role | Signature | Sign-off Tanggal |
|------|------|-----------|------------------|
| __________________________ | Project Manager | _____________________ | ____/____/____ |
| __________________________ | Testing/QA Manager | _____________________ | ____/____/____ |
---
### 12.3 Stakeholder Sign-off
| Nama | Role | Signature | Sign-off Tanggal |
|------|------|-----------|------------------|
| __________________________ | Lab Manager | _____________________ | ____/____/____ |
| __________________________ | Hospital IT Manager | _____________________ | ____/____/____ |
| __________________________ | End User Representative | _____________________ | ____/____/____ |
---
## 13. APPENDIX
### 13.1 Test Data Used
#### Users (Test Data):
| User ID | Username | Role | Password |
|---------|----------|------|----------|
| 10001 | test_superuser | 0 | supersu123 |
| 10002 | test_admin | 1 | admin123 |
| 10003 | test_lab | 2 | lab123 |
| 10004 | test_phlebo | 3 | phlebo123 |
| 10005 | test_cs | 4 | cs123 |
#### Requests (Test Data):
- [ ] Request A: Status "Pending" - order baru
- [ ] Request B: Status "Coll" - sample collected
- [ ] Request C: Status "Recv" - sample received
- [ ] Request D: Status "Fin" - ready for validation
- [ ] Request E: Status "FinV" - duavalidated
- [ ] Request F: Hanya VAL1
- [ ] Request G: Dengan test codes alphabetical (untuk Lab filter)
---
### 13.2 Known Issues / Limitations Found
| ID | Issue | Severity | Workaround | Fixed? |
|----|-------|----------|------------|--------|
| ISS-01 | ________________________________ | [Low/Med/High] | _____________ | [ ] Yes <br> [ ] No |
| ISS-02 | ________________________________ | [Low/Med/High] | _____________ | [ ] Yes <br> [ ] No |
| ISS-03 | ________________________________ | [Low/Med/High] | _____________ | [ ] Yes <br> [ ] No |
---
### 13.3 Contact Information
| Role | Nama | Contact |
|------|------|---------|
| UAT Lead | ________________________________ | _______________________ |
| Technical Lead | ________________________________ | _______________________ |
| Dev Team | ________________________________ | _______________________ |
---
### 13.4 Notes Tambahan
```
_____________________________________________________________________________
_____________________________________________________________________________
_____________________________________________________________________________
_____________________________________________________________________________
_____________________________________________________________________________
_____________________________________________________________________________
```
---
## 14. SUMMARY STATISTICS
### 14.1 Test Execution Summary
| Kategori | Total Cases | LULUS | GAGAL | N/A | BLOCKED | % LULUS |
|----------|-------------|-------|-------|-----|---------|---------|
| **Autentikasi** | 9 | ___ | ___ | ___ | ___ | ___% |
| **Superuser** | 39 | ___ | ___ | ___ | ___ | ___% |
| **Admin** | 22 | ___ | ___ | ___ | ___ | ___% |
| **Lab Analyst** | 19 | ___ | ___ | ___ | ___ | ___% |
| **Phlebotomist** | 12 | ___ | ___ | ___ | ___ | ___% |
| **Customer Service** | 12 | ___ | ___ | ___ | ___ | ___% |
| **Cross-Role** | 15 | ___ | ___ | ___ | ___ | ___% |
| **TOTAL** | **128** | ___ | ___ | ___ | ___ | ___% |
---
### 14.2 Defect Summary
| Severity | Jumlah |
|----------|--------|
| Critical | ___ |
| High | ___ |
| Medium | ___ |
| Low | ___ |
| **TOTAL DEFECTS** | ___ |
---
---
## CATATAN PENTING
1. ✅ **Centang checklist [ ]** untuk test case yang LULUS
2. ❌ **Tulis "X" dalam kotak [X]** untuk test case yang GAGAL
3. 📝 **Isi kolom "Issue/Jawaban"** untuk kasus FAIL/N/A/BLOCKED
4. 📊 **Hitung total LULUS/GAGAL** di ringkasan setiap kategori
5. 🎯 **Review kriteria penerimaan** di Section 10 sebelum keputusan
6. ✍️ **Lakukan sign-off** di Section 12 jika semua criteria terpenuhi
7. 📋 **Cetak dokumen ini** dan gunakan selama sesi UAT
---
**TOTAL ESTIMASI WAKTU UAT: 3-4 hari (dengan 3-5 tester dan 150+ test cases)**
---
### 🚀 GO-LIVE CHECKLIST (Final)
Setelah sign-off, persiapan production:
- [ ] Semua test case sudah dijalankan
- [ ] Semua critical & high defects sudah diperbaiki
- [ ] Security review selesai (password hashing, SQL injection, role access)
- [ ] Performance test selesai (load time, concurrent users)
- [ ] Data backup production-ready
- [ ] Migration plan approved
- [ ] Rollback plan ready
- [ ] Training user sudah dilakukan
- [ ] Documentation updated
- [ ] Stakeholder approval obtained
**Production Go-Live Date:** ____/____/____
---
**DOKUMEN INI MERUPAKAN MILIK PROPERTI INTELEKTUAL GDC CMOD LABORATORY MANAGEMENT SYSTEM**
**BOLEH DISEBARKAN UNTUK KEPERLUAN UAT SAJA**
---
## END OF UAT CHECKLIST
*Total: 150+ test cases organized dalam format checklist yang mudah di-print dan di-fill secara manual.*

48
app/Config/Printers.php Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Printers extends BaseConfig
{
public array $printers = [
'lab' => [
'name' => 'Lab Printer',
'command' => 'copy /B file.txt \\\\print-server\\lab-printer',
],
'phlebo' => [
'name' => 'Phlebo Printer',
'command' => 'copy /B file.txt \\\\print-server\\phlebo-printer',
],
'reception' => [
'name' => 'Reception Printer',
'command' => 'copy /B file.txt \\\\print-server\\reception-printer',
],
];
public array $roleDefaults = [
0 => 'lab',
1 => 'lab',
2 => 'lab',
3 => 'phlebo',
4 => 'reception',
];
public string $defaultPrinter = 'lab';
public function getPrinter(string $printerKey): ?array
{
return $this->printers[$printerKey] ?? null;
}
public function getCommand(string $printerKey): ?string
{
return $this->printers[$printerKey]['command'] ?? null;
}
public function getDefaultForRole(int $roleId): string
{
return $this->roleDefaults[$roleId] ?? $this->defaultPrinter;
}
}

View File

@ -16,9 +16,9 @@ $routes->post('/login', 'AuthController::login', ['filter' => 'guest']);
$routes->get('/logout', 'AuthController::logout');
$routes->patch('/setPassword', 'AuthController::setPassword');
$routes->get('label/coll/(:any)', 'LabelController::coll/$1');
$routes->get('label/dispatch/(:any)/(:any)', 'LabelController::dispatch/$1/$2');
$routes->get('label/all/(:any)', 'LabelController::print_all/$1');
$routes->get('label/coll/(:any)/(:any)', 'LabelController::coll/$1/$2');
$routes->get('label/dispatch/(:any)/(:any)/(:any)', 'LabelController::dispatch/$1/$2/$3');
$routes->get('label/all/(:any)/(:any)', 'LabelController::print_all/$1/$2');
$routes->get('print/(:num)', 'Home::printReport/$1', ['filter' => 'role:0,1,2,3,4']);

View File

@ -3,8 +3,10 @@ namespace App\Controllers;
class LabelController extends BaseController
{
public function coll($reqnum)
{
use ResponseTrait;
public function coll($reqnum, $printer = 'lab')
{
$db = \Config\Database::connect();
$userid = session()->get('userid') ?? 'system';
@ -61,15 +63,21 @@ A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1\n]";
$handle = fopen("./file.txt", "a+");
fwrite($handle, $bar);
fclose($handle);
/*exec($command);*/
$printers = new \Config\Printers();
$command = $printers->getCommand($printer);
if ($command) {
$handle = fopen("./file.txt", "w+");
fwrite($handle, $bar);
fclose($handle);
exec($command);
return $this->respond(['status' => 'success', 'message' => 'Label printed'], 200);
}
return $this->respond(['status' => 'error', 'message' => 'Invalid printer'], 400);
}
public function dispatch($reqnum, $samid)
{
public function dispatch($reqnum, $samid, $printer = 'lab')
{
$db = \Config\Database::connect();
$userid = session()->get('userid') ?? 'system';
@ -140,24 +148,31 @@ A190,190,0,2,1,1,N,\"$date\"
P1
]";
$handle = fopen("./file.txt", "a+");
fwrite($handle, $bar);
fclose($handle);
//exec($command);
$printers = new \Config\Printers();
$command = $printers->getCommand($printer);
if ($command) {
$handle = fopen("./file.txt", "w+");
fwrite($handle, $bar);
fclose($handle);
exec($command);
return $this->respond(['status' => 'success', 'message' => 'Label printed'], 200);
}
return $this->respond(['status' => 'error', 'message' => 'Invalid printer'], 400);
}
public function print_all($accessnumber)
public function print_all($accessnumber, $printer = 'lab')
{
$db = \Config\Database::connect();
$userid = session()->get('userid') ?? 'system';
$this->coll($accessnumber);
$this->coll($accessnumber, $printer);
$sql = "select SAMPCODE from GDC_CMOD.dbo.v_sp_reqtube where SP_ACCESSNUMBER='$accessnumber'";
$rows = $db->query($sql)->getResultArray();
foreach ($rows as $row) {
$sampcode = $row['SAMPCODE'];
$this->dispatch($accessnumber, $sampcode);
$this->dispatch($accessnumber, $sampcode, $printer);
}
return $this->respond(['status' => 'success', 'message' => 'All labels printed'], 200);
}
}

View File

@ -21,6 +21,13 @@ class ReportController extends BaseController
$ispdf = $this->request->getVar('ispdf') ?? 0;
}
if ($eng == 0) {
// Read REPORT_LANG from CM_REQUESTS if not provided
$sql = "SELECT REPORT_LANG FROM GDC_CMOD.dbo.CM_REQUESTS WHERE ACCESSNUMBER=?";
$row = $this->db->query($sql, [$accessnumber])->getRowArray();
$eng = $row['REPORT_LANG'] ?? 0;
}
return $this->renderReport($accessnumber, $eng, $ispdf, false);
}
@ -30,6 +37,13 @@ class ReportController extends BaseController
$ispdf = $this->request->getVar('ispdf') ?? 0;
}
if ($eng == 0) {
// Read REPORT_LANG from CM_REQUESTS if not provided
$sql = "SELECT REPORT_LANG FROM GDC_CMOD.dbo.CM_REQUESTS WHERE ACCESSNUMBER=?";
$row = $this->db->query($sql, [$accessnumber])->getRowArray();
$eng = $row['REPORT_LANG'] ?? 0;
}
return $this->renderReport($accessnumber, $eng, $ispdf, true);
}
@ -62,6 +76,14 @@ class ReportController extends BaseController
public function preview($accessnumber)
{
$eng = $this->request->getVar('eng') ?? 0;
// If eng not provided, read REPORT_LANG from CM_REQUESTS
if ($eng == 0) {
$sql = "SELECT REPORT_LANG FROM GDC_CMOD.dbo.CM_REQUESTS WHERE ACCESSNUMBER=?";
$row = $this->db->query($sql, [$accessnumber])->getRowArray();
$eng = $row['REPORT_LANG'] ?? 0;
}
return $this->renderReport($accessnumber, $eng, 0, false);
}
@ -72,7 +94,17 @@ class ReportController extends BaseController
return $this->response->setStatusCode(403)->setJSON(['success' => false, 'error' => 'Unauthorized']);
}
$eng = $this->request->getVar('eng') ?? 0;
// Get language: URL parameter > REPORT_LANG > default (0)
$engParam = $this->request->getVar('eng') ?? null;
if ($engParam !== null) {
$eng = (int) $engParam;
} else {
// Read REPORT_LANG from CM_REQUESTS
$sql = "SELECT REPORT_LANG FROM GDC_CMOD.dbo.CM_REQUESTS WHERE ACCESSNUMBER=?";
$row = $this->db->query($sql, [$accessnumber])->getRowArray();
$eng = (int) ($row['REPORT_LANG'] ?? 0);
}
$data = $this->reportHelper->getReportData($accessnumber, $eng);
$data['eng'] = $eng;
$data['accessnumber'] = $accessnumber;
@ -87,7 +119,8 @@ class ReportController extends BaseController
'success' => true,
'jobId' => $jobId,
'message' => 'PDF queued for generation',
'status' => 'queued'
'status' => 'queued',
'lang' => $eng == 1 ? 'English' : 'Indonesian'
]);
} catch (\Exception $e) {
log_message('error', "PDF generation failed: " . $e->getMessage());

View File

@ -3,6 +3,8 @@ namespace App\Controllers;
use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController;
// This is just to add ResponseTrait import - actual edit will be in LabelController
class SamplesController extends BaseController
{
use ResponseTrait;

View File

@ -10,6 +10,7 @@ $roleConfig = $config['admin'];
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -10,6 +10,7 @@ $roleConfig = $config['cs'];
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -10,6 +10,7 @@ $roleConfig = $config['lab'];
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -10,6 +10,7 @@ $roleConfig = $config['phlebo'];
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
</main>
<?= $this->endSection(); ?>

View File

@ -22,6 +22,7 @@ return [
'sampleDialog' => [
'commentEditable' => true,
'showCollectButtons' => true,
'defaultPrinter' => 'lab',
],
'menuItems' => [
['label' => 'Dashboard', 'href' => 'admin', 'icon' => 'chart-bar'],
@ -33,6 +34,7 @@ return [
'sampleDialog' => [
'commentEditable' => false,
'showCollectButtons' => false,
'defaultPrinter' => 'reception',
],
'menuItems' => [
['label' => 'Dashboard', 'href' => 'cs', 'icon' => 'chart-bar'],
@ -43,6 +45,7 @@ return [
'sampleDialog' => [
'commentEditable' => true,
'showCollectButtons' => true,
'defaultPrinter' => 'lab',
],
'menuItems' => [
['label' => 'Dashboard', 'href' => 'lab', 'icon' => 'chart-bar'],
@ -54,6 +57,7 @@ return [
'sampleDialog' => [
'commentEditable' => false,
'showCollectButtons' => false,
'defaultPrinter' => 'phlebo',
],
'menuItems' => [
['label' => 'Dashboard', 'href' => 'phlebo', 'icon' => 'chart-bar'],
@ -64,6 +68,7 @@ return [
'sampleDialog' => [
'commentEditable' => true,
'showCollectButtons' => true,
'defaultPrinter' => 'lab',
],
'menuItems' => [
['label' => 'Dashboard', 'href' => 'superuser', 'icon' => 'chart-bar'],

View File

@ -77,49 +77,46 @@
</div>
<div class="flex-1 overflow-y-auto px-4 pb-4">
<template x-if="isLoading">
<template x-if="isLoading">
<table class="table table-xs table-zebra w-full">
<thead class="bg-base-100 sticky top-0 z-10">
<tr>
<th style='width:8%;'>
<th style='width:9%;'>
<div class="skeleton h-4 w-20"></div>
</th>
<th style='width:15%;'>
<th style='width:18%;'>
<div class="skeleton h-4 w-32"></div>
</th>
<th style='width:7%;'>
<th style='width:9%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<th style='width:9%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<th style='width:9%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<th style='width:9%;'>
<div class="skeleton h-4 w-20"></div>
</th>
<th style='width:15%;'>
<th style='width:20%;'>
<div class="skeleton h-4 w-32"></div>
</th>
<th style='width:3%;'>
<div class="skeleton h-4 w-12"></div>
</th>
<th style='width:5%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:5%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:4%;'>
<div class="skeleton h-4 w-12"></div>
</th>
<th style='width:8%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:5%;'>
<div class="skeleton h-4 w-12"></div>
</th>
</tr>
</thead>
<tbody>
<template x-for="i in 5" :key="i">
<tr>
<td colspan="11">
<td colspan="10">
<div class="skeleton h-4 w-full"></div>
</td>
</tr>
@ -137,7 +134,7 @@
<table class="table table-xs table-zebra w-full">
<thead class="bg-base-100 sticky top-0 z-10">
<tr>
<th style='width:8%;' @click="sort('REQDATE')"
<th style='width:9%;' @click="sort('REQDATE')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Order Datetime
@ -145,7 +142,7 @@
:class="sortCol === 'REQDATE' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:15%;' @click="sort('Name')"
<th style='width:18%;' @click="sort('Name')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Patient Name
@ -153,7 +150,7 @@
:class="sortCol === 'Name' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:7%;' @click="sort('SP_ACCESSNUMBER')"
<th style='width:9%;' @click="sort('SP_ACCESSNUMBER')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
No Lab
@ -161,7 +158,7 @@
:class="sortCol === 'SP_ACCESSNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:8%;' @click="sort('HOSTORDERNUMBER')"
<th style='width:9%;' @click="sort('HOSTORDERNUMBER')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
No Register
@ -169,7 +166,7 @@
:class="sortCol === 'HOSTORDERNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:8%;' @click="sort('REFF')"
<th style='width:9%;' @click="sort('REFF')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Reff
@ -177,7 +174,7 @@
:class="sortCol === 'REFF' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:8%;' @click="sort('DOC')"
<th style='width:9%;' @click="sort('DOC')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Doctor
@ -185,12 +182,10 @@
:class="sortCol === 'DOC' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:15%;'>Tests</th>
<th style='width:3%;'>ResTo</th>
<th style='width:20%;'>Tests</th>
<th style='width:4%;'>ResTo</th>
<th style='width:5%;'>Val</th>
<th style='width:5%;'>Result</th>
<th style='width:2%;'></th>
<th style='width:2%;'></th>
<th style='width:5%;'></th>
</tr>
</thead>
<tbody>
@ -205,55 +200,71 @@
<td x-text="req.TESTS" :class="statusRowBg[req.STATS]"></td>
<td x-text="req.ODR_CRESULT_TO"></td>
<td>
<div class='flex gap-1 items-center'>
<div class='w-15'>
<p>1: <span x-text="req.VAL1USER"></span></p>
<p>2: <span x-text="req.VAL2USER"></span></p>
</div>
<template x-if="req.ISVAL == 1 && req.ISPENDING != 1">
<div class='text-center'>
<?php if (session()->get('userlevel') <= 1): ?>
<template
x-if="req.VAL1USER == '<?= session('userid'); ?>' || req.VAL2USER == '<?= session('userid'); ?>'">
<button class="btn btn-xs btn-outline btn-secondary"
@click="openUnvalDialog(req.SP_ACCESSNUMBER)">
<span class="text-error font-bold">UnVal</span>
</button>
</template>
<?php endif; ?>
</div>
</template>
<div class='text-xs'>
<p>1: <span x-text="req.VAL1USER"></span></p>
<p>2: <span x-text="req.VAL2USER"></span></p>
</div>
</td>
<td>
<template x-if="req.VAL1USER && req.VAL2USER">
<div class='flex flex-col gap-1'>
<div class="flex gap-1">
<a :href="'<?=base_url('report/');?>' + req.SP_ACCESSNUMBER" target="_blank" class="btn btn-xs btn-outline btn-primary">Print</a>
<a :href="'<?=base_url('report/');?>' + req.SP_ACCESSNUMBER" target="_blank" class="btn btn-xs btn-outline btn-primary">PDF</a>
</div>
<button class="btn btn-xs btn-warning btn-outline"
@click="retryPdf(req.SP_ACCESSNUMBER)"
title="Retry PDF generation">
<i class="fa fa-sync-alt" :class="{ 'fa-spin': retryingPdf[req.SP_ACCESSNUMBER] }"></i>
<span x-text="retryingPdf[req.SP_ACCESSNUMBER] ? 'Retrying...' : 'Retry PDF'"></span>
</button>
<div class="dropdown dropdown-end dropdown-hover">
<div tabindex="0" role="button" class="btn btn-xs btn-primary w-full">
<i class="fa fa-cog mr-1"></i> Actions
</div>
</template>
</td>
<td>
<button class="btn btn-xs btn-ghost"
@click="openSampleDialog(req.SP_ACCESSNUMBER)"
title="View Samples">
<i class="fa fa-vial text-success"></i>
</button>
</td>
<td>
<button class="btn btn-xs btn-ghost"
@click="openAuditDialog(req.SP_ACCESSNUMBER)"
title="View Audit Trail">
<i class="fa fa-history text-info"></i>
</button>
<ul tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-48 p-2 shadow-lg border border-base-300">
<li x-show="req.ISVAL == 1 && req.ISPENDING != 1 && (req.VAL1USER == '<?= session('userid'); ?>' || req.VAL2USER == '<?= session('userid'); ?>')">
<?php if (session()->get('userlevel') <= 1): ?>
<a @click="openUnvalDialog(req.SP_ACCESSNUMBER)" class="text-error hover:bg-error/10">
<i class="fa fa-times-circle mr-2"></i> UnVal
</a>
<?php endif; ?>
</li>
<template x-if="req.VAL1USER && req.VAL2USER">
<div>
<li>
<a :href="'<?=base_url('report/');?>' + req.SP_ACCESSNUMBER" target="_blank">
<i class="fa fa-print mr-2"></i> Print
</a>
</li>
<!-- Admin/Lab: Generate Dialog -->
<template x-if="<?= in_array(session()->get('userroleid'), [1, 2]) ? 'true' : 'false' ?>">
<li>
<a @click="openGenerateDialog(req.SP_ACCESSNUMBER)">
<i class="fa fa-file-pdf mr-2"></i> Generate
</a>
</li>
</template>
<!-- Read-only users: Direct PDF link -->
<template x-if="<?= !in_array(session()->get('userroleid'), [1, 2]) ? 'true' : 'false' ?>">
<li>
<a :href="'<?=base_url('report/');?>' + req.SP_ACCESSNUMBER + '/pdf'" target="_blank">
<i class="fa fa-file-pdf mr-2"></i> PDF
</a>
</li>
</template>
<li>
<a @click="retryPdf(req.SP_ACCESSNUMBER)">
<i class="fa fa-sync-alt mr-2" :class="{ 'fa-spin': retryingPdf[req.SP_ACCESSNUMBER] }"></i>
<span x-text="retryingPdf[req.SP_ACCESSNUMBER] ? 'Retrying...' : 'Retry PDF'"></span>
</a>
</li>
</div>
</template>
<li>
<a @click="openSampleDialog(req.SP_ACCESSNUMBER)">
<i class="fa fa-vial mr-2 text-success"></i> View Samples
</a>
</li>
<li>
<a @click="openAuditDialog(req.SP_ACCESSNUMBER)">
<i class="fa fa-history mr-2 text-info"></i> View Audit Trail
</a>
</li>
</ul>
</div>
</td>
</tr>
</template>

View File

@ -0,0 +1,108 @@
<div x-show="isGenerateDialogOpen" class="modal modal-open" style="z-index: 9999;">
<div class="modal-box max-w-4xl h-[85vh] flex flex-col">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">
Generate Result: <span class="font-mono" x-text="generateAccessnumber"></span>
</h3>
<button @click="closeGenerateDialog()" class="btn btn-sm btn-circle btn-ghost">
</button>
</div>
<!-- Language Selector -->
<div class="flex items-center gap-4 mb-4 p-4 bg-base-200 rounded-lg">
<span class="font-semibold">Language:</span>
<div class="join">
<button
@click="generateLang = 0"
:class="generateLang === 0 ? 'btn-active' : ''"
class="btn join-item">
<i class="fa fa-language mr-1"></i> Indo
</button>
<button
@click="generateLang = 1"
:class="generateLang === 1 ? 'btn-active' : ''"
class="btn join-item">
<i class="fa fa-language mr-1"></i> Eng
</button>
</div>
<div class="flex-1"></div>
<button
@click="generatePdfFromDialog()"
:disabled="pdfStatus === 'queued' || pdfStatus === 'processing'"
class="btn btn-primary">
<i class="fa fa-file-pdf mr-1"></i> Generate PDF
</button>
</div>
<!-- PDF Display Area -->
<div class="flex-1 flex flex-col min-h-0">
<!-- Loading State -->
<template x-if="pdfStatus === 'queued' || pdfStatus === 'processing'">
<div class="flex-1 flex items-center justify-center">
<div class="text-center">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="mt-4 text-lg">
Generating <span x-text="generateLang === 1 ? 'English' : 'Indonesian'"></span> PDF...
</p>
<p class="text-sm text-base-content/60 mt-2">Please wait</p>
</div>
</div>
</template>
<!-- Success State - Show PDF -->
<template x-if="pdfStatus === 'completed' && pdfUrl">
<iframe
:src="pdfUrl"
class="w-full h-full border-0 rounded-lg"
style="min-height: 60vh;"></iframe>
</template>
<!-- Idle State - Before First Generation -->
<template x-if="pdfStatus === 'idle'">
<div class="flex-1 flex items-center justify-center">
<div class="text-center text-base-content/60">
<i class="fa fa-file-pdf text-6xl mb-4 opacity-30"></i>
<p class="text-lg">Select language and click Generate PDF</p>
<p class="text-sm mt-2">Indonesian PDF is automatically generated after validation.</p>
</div>
</div>
</template>
<!-- Failed State -->
<template x-if="pdfStatus === 'failed'">
<div class="flex-1 flex items-center justify-center">
<div class="text-center text-error">
<i class="fa fa-exclamation-circle text-6xl mb-4"></i>
<p class="text-lg font-semibold">PDF Generation Failed</p>
<button
@click="generatePdfFromDialog()"
class="btn btn-sm btn-outline btn-error mt-4">
<i class="fa fa-sync-alt mr-1"></i> Retry
</button>
</div>
</div>
</template>
</div>
<!-- Footer -->
<div class="flex justify-between items-center mt-4 pt-4 border-t border-base-300">
<div class="text-sm text-base-content/60">
<template x-if="pdfJobId">
<span>Job ID: <span class="font-mono" x-text="pdfJobId"></span></span>
</template>
</div>
<div class="flex gap-2">
<template x-if="pdfStatus === 'completed' && pdfUrl">
<a :href="pdfUrl" target="_blank" class="btn btn-sm btn-outline">
<i class="fa fa-external-link-alt mr-1"></i> Open in New Tab
</a>
</template>
<button @click="closeGenerateDialog()" class="btn btn-sm">
Close
</button>
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,16 @@
<dialog class="modal" :open="isDialogSampleOpen">
<div class="modal-box w-2/3 max-w-5xl">
<div class='flex justify-between items-center mb-2'>
<div class='flex items-center gap-2'>
<label class="text-xs font-medium text-base-content/70">Printer:</label>
<select x-model="selectedPrinter" class="select select-bordered select-xs w-32">
<option value="lab">Lab Printer</option>
<option value="phlebo">Phlebo Printer</option>
<option value="reception">Reception Printer</option>
</select>
</div>
<div class='flex gap-2 ml-auto'>
<template x-if="item.accessnumber">
<button class="btn btn-xs btn-outline btn-info" @click="openAuditDialog(item.accessnumber)">
@ -60,11 +70,13 @@
<tbody>
<tr>
<td></td>
<td></td>
<td>All</td>
<td></td>
<td></td>
<td>
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
<button class="btn btn-sm btn-secondary px-2 py-1" @click="printAllLabels(item.accessnumber)"><i class="fa-solid fa-print"></i></button>
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
<button class="btn btn-sm btn-success px-2 py-1" onclick="">
<h6 class="p-0 m-0">Coll.</h6>
@ -77,7 +89,15 @@
<td>Collection</td>
<td></td>
<td></td>
<td><button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button></td>
<td>
<button class="btn btn-sm btn-secondary px-2 py-1" @click="printCollectionLabel(item.accessnumber)"><i class="fa-solid fa-print"></i></button>
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
<button class="btn btn-sm btn-success px-2 py-1" onclick="">
<h6 class="p-0 m-0">Coll.</h6>
</button>
<?php endif; ?>
</td>
</tr>
<template x-for="sample in item.samples">
@ -91,7 +111,8 @@
<input type="checkbox" class="checkbox" x-bind:checked="sample.tubestatus != 0" disabled>
</td>
<td>
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
<button class="btn btn-sm btn-secondary px-2 py-1" @click="printSampleLabel(item.accessnumber, sample.sampcode)"><i class="fa-solid fa-print"></i></button>
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
<template x-if="sample.colstatus == 0">
<button class="btn btn-sm btn-success px-2 py-1"

View File

@ -26,10 +26,11 @@
<p class="mb-2 flex gap-2">
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
@click="validate(valAccessnumber, '<?=session('userid');?>')"
:disabled="isValidating"
:disabled="!isIframeLoaded || isValidating || validationDelayRemaining > 0"
@keydown.enter.prevent="validate(valAccessnumber, '<?=session('userid');?>')"
@keydown.tab="focusNext($event)">
<i class="fa fa-check"></i> Validate (Enter)
<span x-text="!isIframeLoaded ? 'Loading...' : (validationDelayRemaining > 0 ? `Validating in ${validationDelayRemaining}...` : 'Validate (Enter)')"></span>
<span x-show="!isIframeLoaded && $el.disabled" class="loading loading-spinner loading-xs ml-1"></span>
</button>
<button class="btn btn-sm btn-neutral" @click="skipToNext()" @keydown.tab="focusPrev($event)">
<i class="fa fa-arrow-right"></i> Skip (N)
@ -38,7 +39,7 @@
Close (Esc)
</button>
</p>
<iframe id="result-iframe" x-ref="resultIframe" :src="getPreviewUrl()" width="100%" height="500px"
<iframe id="result-iframe" x-ref="resultIframe" :src="getPreviewUrl()" @load="onIframeLoad()" width="100%" height="500px"
class="border border-base-300 rounded"></iframe>
<!-- Loading overlay -->

View File

@ -7,6 +7,15 @@ document.addEventListener('alpine:init', () => {
isLoading: false,
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
retryingPdf: {},
selectedPrinter: 'lab',
// PDF Generation Dialog
isGenerateDialogOpen: false,
generateAccessnumber: null,
generateLang: 0,
pdfStatus: 'idle',
pdfJobId: null,
pdfUrl: '',
pdfPollInterval: null,
statusColor: {
Pend: 'bg-white text-black font-bold',
PartColl: 'bg-[#ff99aa] text-black font-bold',
@ -81,13 +90,17 @@ document.addEventListener('alpine:init', () => {
init() {
this.today = new Date().toISOString().slice(0, 10);
// Production: default to today
this.filter.date1 = this.today;
this.filter.date2 = this.today;
this.$watch('filterTable', () => {
this.currentPage = 1;
});
const defaultPrinter = '<?= $config[session()->get("userrole")]["sampleDialog"]["defaultPrinter"] ?? "lab" ?>';
this.selectedPrinter = defaultPrinter || 'lab';
this.fetchList();
},
@ -332,6 +345,134 @@ document.addEventListener('alpine:init', () => {
}
},
/*
PDF Generation Dialog methods
*/
openGenerateDialog(accessnumber) {
this.generateAccessnumber = accessnumber;
this.generateLang = 0;
this.pdfStatus = 'idle';
this.pdfJobId = null;
this.pdfUrl = '';
this.isGenerateDialogOpen = true;
// Try to show auto-generated Indo PDF immediately after VAL2
// Since it's generated immediately, assume it's ready
// The spooler will serve the latest PDF
this.pdfUrl = `http://glenlis:3000/api/pdf/view/${accessnumber}.pdf`;
this.pdfStatus = 'completed';
},
closeGenerateDialog() {
this.isGenerateDialogOpen = false;
this.generateAccessnumber = null;
if (this.pdfPollInterval) {
clearInterval(this.pdfPollInterval);
this.pdfPollInterval = null;
}
},
async generatePdfFromDialog() {
if (this.pdfStatus === 'queued' || this.pdfStatus === 'processing') return;
this.pdfStatus = 'queued';
this.pdfJobId = null;
this.pdfUrl = '';
const eng = this.generateLang === 1 ? '?eng=1' : '';
try {
const res = await fetch(`${BASEURL}/report/${this.generateAccessnumber}/pdf${eng}`);
const data = await res.json();
if (data.success) {
this.pdfJobId = data.jobId;
this.pdfStatus = 'queued';
this.showToast(`${data.lang} PDF queued for generation`, 'success');
this.startPdfStatusPolling();
} else {
this.pdfStatus = 'failed';
this.showToast('PDF generation failed', 'error');
}
} catch (e) {
this.pdfStatus = 'failed';
this.showToast('PDF generation failed - try again', 'error');
}
},
startPdfStatusPolling() {
if (this.pdfPollInterval) clearInterval(this.pdfPollInterval);
this.pdfPollInterval = setInterval(() => {
this.checkPdfStatus();
}, 2000);
},
async checkPdfStatus() {
if (!this.pdfJobId || this.pdfStatus === 'completed' || this.pdfStatus === 'failed') return;
try {
const res = await fetch(`${BASEURL}/report/status/${this.pdfJobId}`);
const data = await res.json();
this.pdfStatus = data.status;
if (this.pdfStatus === 'processing') {
// Continue polling
} else if (this.pdfStatus === 'completed' && data.pdfUrl) {
this.pdfUrl = data.pdfUrl;
clearInterval(this.pdfPollInterval);
this.showToast('PDF generated successfully', 'success');
} else if (this.pdfStatus === 'failed') {
clearInterval(this.pdfPollInterval);
this.showToast('PDF generation failed', 'error');
}
} catch (e) {
console.error('PDF status check failed:', e);
}
},
selectedPrinter: 'lab',
printAllLabels(accessnumber) {
const printer = this.selectedPrinter || 'lab';
fetch(`${BASEURL}/label/all/${accessnumber}/${printer}`, { method: 'GET' })
.then(res => {
if (res.ok) {
this.showToast('All labels printed', 'success');
} else {
this.showToast('Print failed', 'error');
}
})
.catch(() => this.showToast('Print failed', 'error'));
},
printCollectionLabel(accessnumber) {
const printer = this.selectedPrinter || 'lab';
fetch(`${BASEURL}/label/coll/${accessnumber}/${printer}`, { method: 'GET' })
.then(res => {
if (res.ok) {
this.showToast('Collection label printed', 'success');
} else {
this.showToast('Print failed', 'error');
}
})
.catch(() => this.showToast('Print failed', 'error'));
},
printSampleLabel(accessnumber, sampcode) {
const printer = this.selectedPrinter || 'lab';
fetch(`${BASEURL}/label/dispatch/${accessnumber}/${sampcode}/${printer}`, { method: 'GET' })
.then(res => {
if (res.ok) {
this.showToast('Sample label printed', 'success');
} else {
this.showToast('Print failed', 'error');
}
})
.catch(() => this.showToast('Print failed', 'error'));
},
showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} fixed top-4 right-4 z-50`;

View File

@ -119,6 +119,9 @@ document.addEventListener('alpine:init', () => {
valItem: null,
currentIndex: 0,
isDialogValOpen: false,
isIframeLoaded: false,
validationDelayRemaining: 0,
validationTimer: null,
toast: { show: false, message: '', type: 'success' },
showToast(message, type = 'success') {
@ -128,6 +131,18 @@ document.addEventListener('alpine:init', () => {
}, 2000);
},
onIframeLoad() {
this.isIframeLoaded = true;
this.validationDelayRemaining = 1;
this.validationTimer = setInterval(() => {
this.validationDelayRemaining--;
if (this.validationDelayRemaining <= 0) {
clearInterval(this.validationTimer);
this.validationTimer = null;
}
}, 1000);
},
openValDialogByIndex(index) {
const filtered = this.unvalidatedFiltered;
if (index < 0 || index >= filtered.length) {
@ -135,12 +150,18 @@ document.addEventListener('alpine:init', () => {
this.closeValDialog();
return;
}
// Reset iframe loading state
this.isIframeLoaded = false;
this.validationDelayRemaining = 0;
if (this.validationTimer) {
clearInterval(this.validationTimer);
this.validationTimer = null;
}
const item = filtered[index];
this.currentIndex = index;
this.valAccessnumber = item.SP_ACCESSNUMBER;
this.valItem = item;
this.isDialogValOpen = true;
// Focus validate button after dialog renders - use setTimeout for reliability
setTimeout(() => {
const btn = document.getElementById('validate-btn');
if (btn) btn.focus();
@ -162,6 +183,12 @@ document.addEventListener('alpine:init', () => {
this.isDialogValOpen = false;
this.valAccessnumber = null;
this.valItem = null;
if (this.validationTimer) {
clearInterval(this.validationTimer);
this.validationTimer = null;
}
this.validationDelayRemaining = 0;
this.isIframeLoaded = false;
},
skipToNext() {
@ -172,6 +199,10 @@ document.addEventListener('alpine:init', () => {
},
validate(accessnumber, userid) {
if (!this.isIframeLoaded || this.validationDelayRemaining > 0) {
this.showToast('Please wait for the report to load', 'error');
return;
}
if (!confirm(`Validate request ${accessnumber}?`)) return;
this.isValidating = true;
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {

View File

@ -6,6 +6,7 @@
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
<?= $this->include('shared/dialog_unval'); ?>
<?= $this->include('shared/dialog_audit'); ?>
<?= $this->include('shared/dialog_results_generate'); ?>
</main>
<?= $this->endSection(); ?>

112
docs/barcode_print_all.php Normal file
View File

@ -0,0 +1,112 @@
<?php
$reqnum = $_GET['req'];
$reqnum = str_pad($reqnum, 10, 0, STR_PAD_LEFT);
$sql = "select p.PATNUMBER,
[Name] = case
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
else ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'')
end,
format(p.BIRTHDATE,'dd/MMM/yyyy') as dob,
age = case
when year(spr.COLLECTIONDATE) - year(p.BIRTHDATE) > 0 then (
case
when format(p.BIRTHDATE,'MMdd')=format(spr.COLLECTIONDATE,'MMdd') then cast(DATEDIFF(YEAR,p.BIRTHDATE, spr.COLLECTIONDATE) as varchar) + 'Y'
else cast( DATEDIFF(hour,p.BIRTHDATE, spr.COLLECTIONDATE)/8766 as varchar) + 'Y' end
)
when month(spr.COLLECTIONDATE) - month(p.BIRTHDATE) > 0 then cast( DATEDIFF(MM,p.BIRTHDATE,spr.COLLECTIONDATE) as varchar) + 'M'
else cast ( floor ( ( day(spr.COLLECTIONDATE) - day(p.BIRTHDATE) ) / 7) as varchar ) + 'W'
end,
[Gender] = case
when p.SEX = 1 then 'M'
when p.SEX = 2 then 'F'
else ''
end,
spr.HOSTORDERNUMBER
from SP_REQUESTS spr
left join PATIENTS p on spr.PATID=p.PATID
left join DICT_TEXTS tx on tx.TEXTID=p.TITLEID
where spr.SP_ACCESSNUMBER='$reqnum'";
$stmt = sqlsrv_query( $conn1, $sql );
if( $stmt == false) { die( print_r( sqlsrv_errors(), true) ); }
$row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_NUMERIC) ;
$patnum = $row[0];
$patnum = substr($patnum,14);
//$patnum = str_pad(substr($row[0],5),17," ");
$patname = $row[1];
$dob = $row[2];
$age = $row[3];
$sex = $row[4];
$hospnum = $row[5];
$date = date("d/M/Y H:i");
$bar = "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname\"
A10,27,0,2,1,1,N,\"$sex $dob $age\"
A225,27,0,3,1,1,N,\"$reqnum\"
B120,50,0,1,2,8,90,N,\"$reqnum\"
A80,150,0,2,2,1,N,\"$hospnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1\n]";
$sql = "select SAMPCODE, SHORTTEXT, TESTS from v_sp_reqtube where SP_ACCESSNUMBER='$reqnum'";
$stmt = sqlsrv_query( $conn2, $sql );
if( $stmt == false) { die( print_r( sqlsrv_errors(), true) ); }
while($row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_NUMERIC)) {
$sampcode = $row[0];
$tubename = $row[1];
$tests = $row[2];
$tubeid = $sampcode.substr("$reqnum",5,5);
/*$bar .= "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname \"
A10,27,0,2,1,1,N,\"$sex $age\"
B160,50,0,1,2,8,90,N,\"$tubeid\"
A380,27,5,3,1,1,N,\"$tubeid\"
A10,80,0,2,1,2,R,\"$tubename\"
A10,150,0,2,1,1,N,\"$tests\"
A10,180,0,1,1,1,N,\"LIS : $reqnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1
]";
*/
$bar .= "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname \"
A10,27,0,2,1,1,N,\"$sex $age\"
B130,50,0,1,2,8,90,N,\"$tubeid\"
A380,27,5,3,1,1,N,\"$tubeid\"
A10,80,0,2,1,2,R,\"$tubename\"
A10,150,0,2,1,1,N,\"$tests\"
A10,180,0,1,1,1,N,\"LIS : $reqnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1
]";
}
$handle = fopen("./file.txt","w+");
fwrite($handle,$bar);
fclose($handle);
exec($command);
?>
Barcode Printed
<script>
window.close();
</script>

View File

@ -0,0 +1,65 @@
<?php
$reqnum = $_GET['req'];
$reqnum = str_pad($reqnum, 10, 0, STR_PAD_LEFT);
$sql = "select p.PATNUMBER,
[Name] = case
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
else ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'')
end,
format(p.BIRTHDATE,'dd/MMM/yyyy') as dob,
age = case
when year(spr.COLLECTIONDATE) - year(p.BIRTHDATE) > 0 then (
case
when format(p.BIRTHDATE,'MMdd')=format(spr.COLLECTIONDATE,'MMdd') then cast(DATEDIFF(YEAR,p.BIRTHDATE, spr.COLLECTIONDATE) as varchar) + 'Y'
else cast( DATEDIFF(hour,p.BIRTHDATE, spr.COLLECTIONDATE)/8766 as varchar) + 'Y' end
)
when month(spr.COLLECTIONDATE) - month(p.BIRTHDATE) > 0 then cast( DATEDIFF(MM,p.BIRTHDATE,spr.COLLECTIONDATE) as varchar) + 'M'
else cast ( floor ( ( day(spr.COLLECTIONDATE) - day(p.BIRTHDATE) ) / 7) as varchar ) + 'W'
end,
[Gender] = case
when p.SEX = 1 then 'M'
when p.SEX = 2 then 'F'
else ''
end,
spr.HOSTORDERNUMBER
from SP_REQUESTS spr
left join PATIENTS p on spr.PATID=p.PATID
left join DICT_TEXTS tx on tx.TEXTID=p.TITLEID
where spr.SP_ACCESSNUMBER='$reqnum'";
$stmt = sqlsrv_query( $conn1, $sql );
if( $stmt == false) { die( print_r( sqlsrv_errors(), true) ); }
$row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_NUMERIC) ;
$patnum = $row[0];
$patnum = substr($patnum,14);
//$patnum = str_pad(substr($row[0],5),17," ");
$patname = $row[1];
$dob = $row[2];
$age = $row[3];
$sex = $row[4];
$hospnum = $row[5];
$date = date("d/M/Y H:i");
$bar = "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname\"
A10,27,0,2,1,1,N,\"$sex $dob $age\"
A225,27,0,3,1,1,N,\"$reqnum\"
B120,50,0,1,2,8,90,N,\"$reqnum\"
A80,150,0,2,2,1,N,\"$hospnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1\n]";
$handle = fopen("./file.txt","w+");
fwrite($handle,$bar);
fclose($handle);
exec($command);
?>
Barcode Printed
<script>
window.close();
</script>

134
docs/barcode_print_disp.php Normal file
View File

@ -0,0 +1,134 @@
<?php
$reqnum = $_GET['req'];
$reqnum = str_pad($reqnum, 10, 0, STR_PAD_LEFT);
$samid = $_GET['sam'];
$sql = "select p.PATNUMBER,
[Name] = case
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
else ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'')
end,
format(p.BIRTHDATE,'dd/MMM/yyyy') as dob,
age = case
when year(spr.COLLECTIONDATE) - year(p.BIRTHDATE) > 0 then (
case
when format(p.BIRTHDATE,'MMdd')=format(spr.COLLECTIONDATE,'MMdd') then cast(DATEDIFF(YEAR,p.BIRTHDATE, spr.COLLECTIONDATE) as varchar) + 'Y'
else cast( DATEDIFF(hour,p.BIRTHDATE, spr.COLLECTIONDATE)/8766 as varchar) + 'Y' end
)
when month(spr.COLLECTIONDATE) - month(p.BIRTHDATE) > 0 then cast( DATEDIFF(MM,p.BIRTHDATE,spr.COLLECTIONDATE) as varchar) + 'M'
else cast ( floor ( ( day(spr.COLLECTIONDATE) - day(p.BIRTHDATE) ) / 7) as varchar ) + 'W'
end,
[Gender] = case
when p.SEX = 1 then 'M'
when p.SEX = 2 then 'F'
else ''
end,
spr.HOSTORDERNUMBER
from SP_REQUESTS spr
left join PATIENTS p on spr.PATID=p.PATID
left join DICT_TEXTS tx on tx.TEXTID=p.TITLEID
where spr.SP_ACCESSNUMBER='$reqnum'";
/*
select p.PATNUMBER, ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') as [Name],
age = case
when year(spr.REQDATE) - year(p.BIRTHDATE) > 0 then cast(floor(DATEDIFF(MM,p.BIRTHDATE,spr.REQDATE)/12) as varchar) + 'Y'
when month(spr.REQDATE) - month(p.BIRTHDATE) > 0 then cast( DATEDIFF(MM,p.BIRTHDATE,spr.REQDATE) as varchar) + 'M'
else cast ( floor ( ( day(spr.REQDATE) - day(p.BIRTHDATE) ) / 7) as varchar ) + 'W'
end,
[Gender] = case
when p.SEX = 1 then 'M'
when p.SEX = 2 then 'F'
else ''
end,
spr.HOSTORDERNUMBER
from SP_REQUESTS spr, PATIENTS p
where spr.PATID=p.PATID and spr.SP_ACCESSNUMBER='$reqnum'";
*/
$stmt = sqlsrv_query( $conn1, $sql );
if( $stmt == false) { die( print_r( sqlsrv_errors(), true) ); }
$row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_NUMERIC) ;
$patnum = $row[0];
$patnum = substr($patnum,14);
//$patnum = substr($row[0],9);
$patname = $row[1];
$age = $row[2];
$sex = $row[3];
$hospnum = $row[4];
$sql = "select SAMPCODE, SHORTTEXT, TESTS, TESTS1 from v_sp_reqtube where SP_ACCESSNUMBER='$reqnum' and SAMPTYPEID='$samid'";
$stmt = sqlsrv_query( $conn2, $sql );
if( $stmt == false) { die( print_r( sqlsrv_errors(), true) ); }
$row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_NUMERIC);
$sampcode = $row[0];
$samptext = $row[1];
$tests = $row[2];
$tests1 = $row[3];
if($tests == '') {$tests = $tests1;}
$tubeid = $sampcode.substr("$reqnum",5,5);
$date = date("d/M/Y H:i");
/*
$bar = "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname \"
A10,27,0,2,1,1,N,\"$sex $age\"
B20,50,0,1,2,8,90,N,\"$tubeid\"
A380,27,5,3,1,1,N,\"$tubeid\"
A350,27,5,3,1,1,R,\"$samptext\"
A10,150,0,2,1,1,N,\"$tests\"
A10,180,0,1,1,1,N,\"LIS : $reqnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1
]";
*/
$bar = "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname \"
A10,27,0,2,1,1,N,\"$sex $age\"
B130,50,0,1,2,8,90,N,\"$tubeid\"
A380,27,5,3,1,1,N,\"$tubeid\"
A10,80,0,2,1,2,R,\"$samptext\"
A10,150,0,2,1,1,N,\"$tests\"
A10,180,0,1,1,1,N,\"LIS : $reqnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1
]";
/*
$bar = "[
N
OD
q400
Q224,24+0
I8,A,001
D10
A10,3,0,3,1,1,N,\"$patname \"
A10,27,0,2,1,1,N,\"$sex $age\"
B100,50,0,1,3,9,90,N,\"$tubeid\"
A10,150,0,2,1,1,N,\"$tests\"
A10,180,0,1,1,1,N,\"LIS : $reqnum\"
A10,195,0,1,1,1,N,\"HIS : $hospnum\"
A190,190,0,2,1,1,N,\"$date\"
P1
]";
*/
$handle = fopen("./file.txt","w+");
fwrite($handle,$bar);
fclose($handle);
exec($command);
?>
Barcode Printed
<script>
window.close();
</script>