feature :

- add table sort
- add sidebar menu
- remove IND, ENG on Preview
This commit is contained in:
mahdahar 2026-01-19 16:46:05 +07:00
parent 582faacdad
commit e7cacba1c3
7 changed files with 994 additions and 749 deletions

82
CHECKLIST.md Normal file
View File

@ -0,0 +1,82 @@
# Project Checklist: Glen RME & Lab Management System
**Last Updated:** January 19, 2026
**Source:** PROJECT_BACKLOG.md
---
## P0 - Critical (Access Control & Security)
_Must be completed first to ensure basic process flow is correct._
- [ ] **T-002:** Hide/Disable 'Validation' button after 2nd validation
- Prevent redundant validation actions
- [ ] **T-003:** Restrict Print/Save-to-PDF to CS Role only
- Lab can only preview, CS can print/save
- [ ] **T-004:** Update User Role levels
- Standardize roles: Superuser, Admin, Lab, Phlebo, CS
---
## P1 - High (Dashboard & UI Improvements)
_Features that improve speed and correctness of lab operations._
- [ ] **T-005:** Role-Based Dashboard Filtering
- Filter by patient_status or service_type (Klinik+Lab vs Lab Only)
- [ ] **T-006:** Create Clinical Patients Dashboard
- Hide "No Lab" column for clinical workflows
- [X] **T-007:** Fix Table Sorting
- Enable sorting by "No Register" and "Patient Name"
- [X] **T-008:** Fix Language Toggle (ID/EN)
- Toggle lab result preview between Indonesian and English
- [X] **T-009:** Apply Row Color-Coding
- Color-code "No Register" column (Yellow/Blue/Green)
- [ ] **T-010:** Update PDF Report Metadata
- Replace 'Printed By' with validating user's name
- Add 'Finish Validation' status per sample
- [X] **T-011:** Initialize RME Sidebar Menu
- Create menu items: Dashboard, Patient, Hasil Lab, Validation, Unreceived, Report, Sample Collection, User Management, Unvalidate
- [ ] **T-012:** Create 'Detail Unvalidated' History Log/View
- Log unvalidation actions with timestamp, user ID, and reason
- [ ] **T-013:** Enhanced Patient Detail Logging
- Track: Sample Collection Time, Sample Received Time, Print History
---
## P2 - Medium (Maintenance & UX)
_UI improvements and backend optimizations._
- [ ] **T-014:** Add Dedicated Print Button
- Trigger browser/system print dialog
- [ ] **T-015:** Add Error Handling for Preview Button
- Handle empty data gracefully
- [ ] **T-016:** Ensure 'Uncollect' Feature Functional
- Maintain Uncollect feature functionality
- [ ] **T-017:** Backend Performance & Connectivity
- Investigate intermittent connection issues with Server 253
- Plan SSD upgrade for database server
- Verify API integration: GDC_cmod, GDC_CS2, Report2
- [X] **T-018: Delayed** Dashboard Performance
- When getting data more than 100 rows, it load too slow.
- Answer : Its Alpine Limitation, later will create pagination for dashboard.
---
## Quick Progress Summary
| Priority | Total | Completed |
|----------|-------|-----------|
| P0 - Critical | 4 | 0 |
| P1 - High | 9 | 0 |
| P2 - Medium | 4 | 0 |
| **Total** | **17** | **0** |
---
## Legend
- Tasks are ordered by priority (P0 → P1 → P2)
- Check items as you complete them
- Refer to PROJECT_BACKLOG.md for detailed technical specifications

View File

@ -1,120 +0,0 @@
# Project Backlog: Glen RME & Lab Management System
**Last Updated:** January 19, 2026
**Sources:** TODO.md, TODO.json
---
## P0 - Critical (Access Control & Security)
_Must be completed first to ensure basic process flow is correct._
| ID | Task | Source | Status |
|----|------|--------|--------|
| T-001 | **Restrict 'Unvalidate' button to specific User IDs**<br><br>User Story: As an Admin, I want an "Unvalidate" menu to revert finalized records in case of data entry errors.<br><br>Technical: Only Doctors and Bu Yani should have access. | Both | Pending |
| T-002 | **Hide/Disable 'Validation' button after 2nd validation**<br><br>User Story: As a Lab Validator, I want the "Validate" button to disappear or disable once 2-level validation is complete to prevent redundant actions. | Both | Pending |
| T-003 | **Restrict Print/Save-to-PDF to CS Role only**<br><br>User Story: As a Manager, I want to restrict the "Print Result" permission to the CS Role only, so that the Lab team cannot bypass the official release process.<br><br>Technical: Lab can only preview, CS can print/save. | Both | Pending |
| T-004 | **Update User Role levels**<br><br>Technical: Standardize roles to: Superuser, Admin, Lab, Phlebo, and CS. | JSON | Pending |
---
## P1 - High (Dashboard & UI Improvements)
_Features that improve speed and correctness of lab operations._
| ID | Task | Source | Status |
|----|------|--------|--------|
| T-005 | **Role-Based Dashboard Filtering**<br><br>User Story: As a Lab Staff, I want the dashboard to only show "Klinik+Lab" or "Lab Only" patients so I can focus on relevant tasks.<br><br>User Story: As a CS Staff, I want to see all patients to monitor the entire facility flow.<br><br>Technical: Implement filter logic based on patient_status or service_type field. | MD | Pending |
| T-006 | **Create Clinical Patients Dashboard**<br><br>Technical: Dedicated dashboard that hides the "No Lab" column for clinical workflows. | JSON | Pending |
| T-007 | **Fix Table Sorting**<br><br>User Story: As a User, I want to sort dashboard tables by "No Register" and "Patient Name" to find specific records quickly.<br><br>Technical: Fix sorting functionality on all table headers. | Both | Pending |
| T-008 | **Fix Language Toggle (ID/EN)**<br><br>User Story: As a Lab Staff, I want to toggle the lab result preview between Indonesian and English so I can provide reports for international requirements. | Both | Pending |
| T-009 | **Apply Row Color-Coding**<br><br>User Story: As a User, I want the "No Register" column to be color-coded (Yellow/Blue/Green) based on legacy system logic for quick status recognition. | Both | Pending |
| T-010 | **Update PDF Report Metadata**<br><br>Technical: Replace 'Printed By' with name of validating user. Add 'Finish Validation' status per sample in PDF output. | JSON | Pending |
---
## P1 - High (RME Module Development)
| ID | Task | Source | Status |
|----|------|--------|--------|
| T-011 | **Initialize RME Sidebar Menu**<br><br>Technical: Create menu items for Dashboard, Patient, Hasil Lab, Validation, Unreceived, Report, Sample Collection, User Management, and Unvalidate. | JSON | Pending |
| T-012 | **Create 'Detail Unvalidated' History Log/View**<br><br>User Story: As a User, I want the "Reason for Unvalidation" to be visible in the Patient Detail view so I know why a record was reopened.<br><br>Technical: Log all unvalidation actions with timestamp, user ID, and reason. | Both | Pending |
| T-013 | **Enhanced Patient Detail Logging**<br><br>User Story: As a Staff member, I want to see a detailed history in the patient profile including:<br><br>- Sample Collection Time (categorized by type: EDTA, Serum, etc.)<br>- Sample Received Time<br>- Print History (Who printed and when) | MD | Pending |
---
## P2 - Medium (Maintenance & UX)
_UI improvements and backend optimizations._
| ID | Task | Source | Status |
|----|------|--------|--------|
| T-014 | **Add Dedicated Print Button**<br><br>User Story: As a User, I want a clear "Print" button that triggers the browser/system print dialog, as standard shortcuts (Ctrl+P) are currently unreliable in the app. | MD | Pending |
| T-015 | **Add Error Handling for Preview Button**<br><br>Technical: Ensure Preview doesn't crash when data is empty. | JSON | Pending |
| T-016 | **Ensure 'Uncollect' Feature Functional**<br><br>Technical: Maintain Uncollect feature functionality in current phase. | JSON | Pending |
| T-017 | **Backend Performance & Connectivity**<br><br>Technical Tasks:<br>- Investigate intermittent connection issues with Server 253<br>- Plan and execute SSD Upgrade for database server<br>- Verify API integration between GDC_cmod, GDC_CS2, and Report2 for sample reception module | MD | Pending |
---
## Management Notes
| ID | Note | Source |
|----|------|--------|
| NOTE-001 | Mas Rizqi is the IT person in charge. | JSON |
| NOTE-002 | Merging of 'Hasil' and 'Validation' menus is pending management approval. | JSON |
---
## Acceptance Criteria Summary
### Code Consistency
- All new UI elements (buttons/toggles) must match the existing design system.
### Audit Trail
- Every status change (Validate/Unvalidate) must be logged with a timestamp and user ID.
### Cross-Browser
- The "Print" functionality must work across Chrome and Edge browsers.
---
## Source Mapping Appendix
| Task ID | Source | Original Description |
|---------|--------|---------------------|
| T-001 | TODO.json | Restrict 'Unvalidate' button to specific User IDs |
| T-002 | Both | Hide/disable Validation button after 2nd validation |
| T-003 | Both | Restrict Print/Save-to-PDF to CS Role only |
| T-004 | TODO.json | Update User Role levels |
| T-005 | TODO.md | Role-Based Dashboard Filtering |
| T-006 | TODO.json | Create Clinical Patients Dashboard |
| T-007 | Both | Fix Table Sorting |
| T-008 | Both | Fix Language Toggle (ID/EN) |
| T-009 | Both | Apply row color-coding |
| T-010 | TODO.json | Update PDF reports metadata |
| T-011 | TODO.json | Initialize RME Sidebar Menu |
| T-012 | Both | Detail Unvalidated history log/view |
| T-013 | TODO.md | Enhanced Patient Detail Logging |
| T-014 | TODO.md | Dedicated Print Button |
| T-015 | TODO.json | Error handling for Preview button |
| T-016 | TODO.json | Ensure Uncollect feature functional |
| T-017 | TODO.md | Backend Performance & Connectivity |
---
## Quick Reference: Task Count by Priority
| Priority | Count | Items |
|----------|-------|-------|
| P0 - Critical | 4 | T-001 through T-004 |
| P1 - High | 9 | T-005 through T-013 |
| P2 - Medium | 4 | T-014 through T-017 |
| **Total** | **17** | |
---
## Legend
- **MD** = TODO.md (User story focused)
- **JSON** = TODO.json (Developer task focused)
- **Both** = Content merged from both sources

View File

@ -4,16 +4,24 @@
<!-- Header --> <!-- Header -->
<div class="flex justify-between items-center p-3 bg-base-200 border-b border-base-300"> <div class="flex justify-between items-center p-3 bg-base-200 border-b border-base-300">
<h3 class="font-bold text-lg flex items-center gap-2"> <h3 class="font-bold text-lg flex items-center gap-2">
<i class="fa fa-eye text-primary"></i> <i class="fa fa-eye text-primary"></i>
Preview Preview
<span class="badge badge-ghost text-xs" x-text="previewAccessnumber"></span> <span class="badge badge-ghost text-xs" x-text="previewAccessnumber"></span>
</h3> </h3>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="join shadow-sm"> <div class="join shadow-sm" x-show="previewItem && previewItem.VAL1USER && previewItem.VAL2USER">
<button @click="previewType = 'preview'" :class="previewType === 'preview' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">Preview</button> <button @click="setPreviewType('preview')"
<button @click="setPreviewType('ind')" :class="previewType === 'ind' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">IND</button> :class="previewType === 'preview' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
<button @click="setPreviewType('eng')" :class="previewType === 'eng' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">ENG</button> class="btn btn-sm join-item">Default</button>
<button @click="setPreviewType('pdf')" :class="previewType === 'pdf' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">PDF</button> <button @click="setPreviewType('ind')"
:class="previewType === 'ind' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
class="btn btn-sm join-item">ID</button>
<button @click="setPreviewType('eng')"
:class="previewType === 'eng' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
class="btn btn-sm join-item">EN</button>
<button @click="setPreviewType('pdf')"
:class="previewType === 'pdf' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
class="btn btn-sm join-item">PDF</button>
</div> </div>
<button class="btn btn-sm btn-circle btn-ghost" @click="closePreviewDialog()"> <button class="btn btn-sm btn-circle btn-ghost" @click="closePreviewDialog()">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
@ -23,7 +31,8 @@
<!-- Content --> <!-- Content -->
<div class="flex-1 bg-base-300 relative p-1"> <div class="flex-1 bg-base-300 relative p-1">
<iframe id="preview-iframe" x-ref="previewIframe" :src="getPreviewUrl()" class="w-full h-full rounded shadow-sm bg-white"></iframe> <iframe id="preview-iframe" x-ref="previewIframe" :src="getPreviewUrl()"
class="w-full h-full rounded shadow-sm bg-white"></iframe>
</div> </div>
<!-- Footer --> <!-- Footer -->
@ -34,12 +43,12 @@
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<button class="btn btn-sm btn-ghost" @click="closePreviewDialog()">Cancel</button> <button class="btn btn-sm btn-ghost" @click="closePreviewDialog()">Cancel</button>
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success" <button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
@click="validate(previewAccessnumber, '<?=session('userid');?>')" :disabled="!reviewed"> @click="validate(previewAccessnumber, '<?= session('userid'); ?>')" :disabled="!reviewed">
<i class="fa fa-check mr-1"></i> Validate <i class="fa fa-check mr-1"></i> Validate
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</dialog> </dialog>

View File

@ -1,401 +1,566 @@
<?= $this->extend('admin/main'); ?> <?= $this->extend('admin/main'); ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard"> <main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
<div class="card bg-base-100 shadow-xl h-full border border-base-200 overflow-hidden"> <div class="card bg-base-100 shadow-xl h-full border border-base-200 overflow-hidden">
<div class="card-body p-0 h-full flex flex-col"> <div class="card-body p-0 h-full flex flex-col">
<!-- Header & Filters --> <!-- Header & Filters -->
<div class="p-4 border-b border-base-200 bg-base-50"> <div class="p-4 border-b border-base-200 bg-base-50">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-4"> <div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-4">
<div class="flex-1"> <div class="flex-1">
<h2 class="text-2xl font-bold flex items-center gap-2 text-base-content"> <h2 class="text-2xl font-bold flex items-center gap-2 text-base-content">
<i class="fa fa-chart-bar text-primary"></i> Requests Overview <i class="fa fa-chart-bar text-primary"></i> Requests Overview
</h2> </h2>
</div>
<!-- Status Filters -->
<div class="join shadow-sm bg-base-100 rounded-lg">
<button @click="filterKey = 'Total'" :class="filterKey === 'Total' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">
All <span class="badge badge-sm badge-ghost ml-1" x-text="counters.Total"></span>
</button>
<button @click="filterKey = 'Pend'" :class="filterKey === 'Pend' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">
Pending <span class="badge badge-sm ml-1" x-text="counters.Pend"></span>
</button>
<button @click="filterKey = 'Coll'" :class="filterKey === 'Coll' ? 'btn-active btn-warning text-white' : 'btn-ghost'" class="btn btn-sm join-item">
Coll <span class="badge badge-sm badge-warning ml-1" x-text="counters.Coll"></span>
</button>
<button @click="filterKey = 'Recv'" :class="filterKey === 'Recv' ? 'btn-active btn-info text-white' : 'btn-ghost'" class="btn btn-sm join-item">
Recv <span class="badge badge-sm badge-info ml-1" x-text="counters.Recv"></span>
</button>
<button @click="filterKey = 'Inc'" :class="filterKey === 'Inc' ? 'btn-active btn-error text-white' : 'btn-ghost'" class="btn btn-sm join-item">
Inc <span class="badge badge-sm badge-error ml-1" x-text="counters.Inc"></span>
</button>
<button @click="filterKey = 'Fin'" :class="filterKey === 'Fin' ? 'btn-active btn-success text-white' : 'btn-ghost'" class="btn btn-sm join-item">
Fin <span class="badge badge-sm badge-success ml-1" x-text="counters.Fin"></span>
</button>
<button @click="filterKey = 'Validated'" :class="filterKey === 'Validated' ? 'btn-active btn-primary text-white' : 'btn-ghost'" class="btn btn-sm join-item">
Val <span class="badge badge-sm badge-primary ml-1" x-text="validatedCount"></span>
</button>
</div>
</div> </div>
<!-- Search & Date Filter --> <!-- Status Filters -->
<div class="flex flex-col md:flex-row gap-3 items-end bg-base-100 p-3 rounded-lg border border-base-200 shadow-sm"> <div class="join shadow-sm bg-base-100 rounded-lg">
<div class="form-control"> <button @click="filterKey = 'Total'"
<label class="label text-xs font-bold py-1 text-base-content/60">Date Range</label> :class="filterKey === 'Total' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
<div class="join"> class="btn btn-sm join-item">
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date1"/> All <span class="badge badge-sm badge-ghost ml-1" x-text="counters.Total"></span>
<span class="join-item btn btn-sm btn-ghost no-animation bg-base-200 font-normal px-2">-</span> </button>
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date2"/> <button @click="filterKey = 'Pend'"
</div> :class="filterKey === 'Pend' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
</div> class="btn btn-sm join-item">
Pending <span class="badge badge-sm ml-1" x-text="counters.Pend"></span>
<div class="flex gap-2"> </button>
<button class="btn btn-sm btn-primary" @click='fetchList()'><i class='fa fa-search'></i> Search</button> <button @click="filterKey = 'Coll'"
<button class="btn btn-sm btn-neutral" @click='reset()'><i class='fa fa-sync-alt'></i> Reset</button> :class="filterKey === 'Coll' ? 'btn-active btn-warning text-white' : 'btn-ghost'"
</div> class="btn btn-sm join-item">
Coll <span class="badge badge-sm badge-warning ml-1" x-text="counters.Coll"></span>
<span class="flex-1"></span> </button>
<button @click="filterKey = 'Recv'"
<div class="form-control w-full md:w-auto"> :class="filterKey === 'Recv' ? 'btn-active btn-info text-white' : 'btn-ghost'"
<label class='input input-sm input-bordered'> class="btn btn-sm join-item">
<i class="fa fa-filter"></i> Recv <span class="badge badge-sm badge-info ml-1" x-text="counters.Recv"></span>
<input type="text" placeholder="Type to filter..." x-model="filterTable" /> </button>
</label> <button @click="filterKey = 'Inc'"
</div> :class="filterKey === 'Inc' ? 'btn-active btn-error text-white' : 'btn-ghost'"
class="btn btn-sm join-item">
Inc <span class="badge badge-sm badge-error ml-1" x-text="counters.Inc"></span>
</button>
<button @click="filterKey = 'Fin'"
:class="filterKey === 'Fin' ? 'btn-active btn-success text-white' : 'btn-ghost'"
class="btn btn-sm join-item">
Fin <span class="badge badge-sm badge-success ml-1" x-text="counters.Fin"></span>
</button>
<button @click="filterKey = 'Validated'"
:class="filterKey === 'Validated' ? 'btn-active btn-primary text-white' : 'btn-ghost'"
class="btn btn-sm join-item">
Val <span class="badge badge-sm badge-primary ml-1" x-text="validatedCount"></span>
</button>
</div> </div>
</div> </div>
<div class="flex-1 overflow-y-auto px-4 pb-4"> <!-- Search & Date Filter -->
<template x-if="isLoading"> <div
<table class="table table-xs table-zebra w-full"> class="flex flex-col md:flex-row gap-3 items-end bg-base-100 p-3 rounded-lg border border-base-200 shadow-sm">
<thead class="bg-base-100 sticky top-0 z-10"> <div class="form-control">
<tr> <label class="label text-xs font-bold py-1 text-base-content/60">Date Range</label>
<th style='width:7%;'><div class="skeleton h-4 w-20"></div></th> <div class="join">
<th style='width:15%;'><div class="skeleton h-4 w-32"></div></th> <input type="date" class="input input-sm input-bordered join-item" x-model="filter.date1" />
<th style='width:7%;'><div class="skeleton h-4 w-16"></div></th> <span class="join-item btn btn-sm btn-ghost no-animation bg-base-200 font-normal px-2">-</span>
<th style='width:7%;'><div class="skeleton h-4 w-16"></div></th> <input type="date" class="input input-sm input-bordered join-item" x-model="filter.date2" />
<th style='width:8%;'><div class="skeleton h-4 w-16"></div></th>
<th style='width:8%;'><div class="skeleton h-4 w-20"></div></th>
<th style='width:15%;'><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>
</tr>
</thead>
<tbody>
<template x-for="i in 5" :key="i">
<tr>
<td colspan="11"><div class="skeleton h-4 w-full"></div></td>
</tr>
</template>
</tbody>
</table>
</template>
<template x-if="!isLoading && !list.length">
<div class="text-center py-10">
<i class="fa fa-inbox text-4xl mb-2 opacity-50"></i>
<p>No records found</p>
</div> </div>
</template> </div>
<template x-if="!isLoading && list.length">
<table class="table table-xs table-zebra w-full"> <div class="flex gap-2">
<thead class="bg-base-100 sticky top-0 z-10"> <button class="btn btn-sm btn-primary" @click='fetchList()'><i class='fa fa-search'></i> Search</button>
<button class="btn btn-sm btn-neutral" @click='reset()'><i class='fa fa-sync-alt'></i> Reset</button>
</div>
<span class="flex-1"></span>
<div class="form-control w-full md:w-auto">
<label class='input input-sm input-bordered'>
<i class="fa fa-filter"></i>
<input type="text" placeholder="Type to filter..." x-model="filterTable" />
</label>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto px-4 pb-4">
<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:7%;'>
<div class="skeleton h-4 w-20"></div>
</th>
<th style='width:15%;'>
<div class="skeleton h-4 w-32"></div>
</th>
<th style='width:7%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:7%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<div class="skeleton h-4 w-20"></div>
</th>
<th style='width:15%;'>
<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>
</tr>
</thead>
<tbody>
<template x-for="i in 5" :key="i">
<tr> <tr>
<th style='width:7%;'>Order Datetime</th> <td colspan="11">
<th style='width:15%;'>Patient Name</th> <div class="skeleton h-4 w-full"></div>
<th style='width:7%;'>No Lab</th> </td>
<th style='width:7%;'>No Register</th>
<th style='width:8%;'>Reff</th>
<th style='width:8%;'>Doctor</th>
<th style='width:15%;'>Tests</th>
<th style='width:3%;'>ResTo</th>
<th style='width:5%;'>Val</th>
<th style='width:5%;'>Result</th>
<th style='width:4%;'>Status</th>
</tr> </tr>
</thead> </template>
<tbody> </tbody>
<template x-for="req in filtered" :key="req.SP_ACCESSNUMBER"> </table>
<tr class="hover:bg-base-300"> </template>
<td x-text="req.REQDATE"></td> <template x-if="!isLoading && !list.length">
<td x-text="req.Name"></td> <div class="text-center py-10">
<td x-text="req.SP_ACCESSNUMBER"></td> <i class="fa fa-inbox text-4xl mb-2 opacity-50"></i>
<td x-text="req.HOSTORDERNUMBER"></td> <p>No records found</p>
<td x-text="req.REFF"></td> </div>
<td x-text="req.DOC"></td> </template>
<td x-text="req.TESTS"></td> <template x-if="!isLoading && list.length">
<td x-text="req.ODR_CRESULT_TO"></td> <table class="table table-xs table-zebra w-full">
<td> <thead class="bg-base-100 sticky top-0 z-10">
<div class='flex gap-1 items-center'> <tr>
<div class='w-15'> <th style='width:7%;' @click="sort('REQDATE')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Order Datetime
<i class="fa text-xs"
:class="sortCol === 'REQDATE' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:15%;' @click="sort('Name')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Patient Name
<i class="fa text-xs"
: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')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
No Lab
<i class="fa text-xs"
:class="sortCol === 'SP_ACCESSNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:7%;' @click="sort('HOSTORDERNUMBER')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
No Register
<i class="fa text-xs"
:class="sortCol === 'HOSTORDERNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:8%;' @click="sort('REFF')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Reff
<i class="fa text-xs"
:class="sortCol === 'REFF' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
<th style='width:8%;' @click="sort('DOC')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Doctor
<i class="fa text-xs"
: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:5%;'>Val</th>
<th style='width:5%;'>Result</th>
<th style='width:4%;' @click="sort('STATS')"
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
<div class="flex items-center gap-1">
Status
<i class="fa text-xs"
:class="sortCol === 'STATS' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
</div>
</th>
</tr>
</thead>
<tbody>
<tbody>
<template x-for="req in paginated" :key="req.SP_ACCESSNUMBER">
<tr class="hover:bg-base-300">
<td x-text="req.REQDATE"></td>
<td x-text="req.Name"></td>
<td x-text="req.SP_ACCESSNUMBER" class="font-bold" :class="statusColor[req.STATS]"></td>
<td x-text="req.HOSTORDERNUMBER" class="font-bold" :class="statusColor[req.STATS]"></td>
<td x-text="req.REFF"></td>
<td x-text="req.DOC"></td>
<td x-text="req.TESTS"></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>1: <span x-text="req.VAL1USER"></span></p>
<p>2: <span x-text="req.VAL2USER"></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'>
<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)"><i class="fa-solid fa-rotate-right"></i></button>
</template>
</div>
</template>
</div> </div>
</td> <template x-if="req.ISVAL == 1 && req.ISPENDING != 1">
<td> <div class='text-center'>
<template x-if="req.STATS !== 'PartColl' && req.STATS !== 'Coll' && req.STATS !== 'Pend'"> <template
<button class="btn btn-xs btn-outline btn-primary" @click="openPreviewDialog(req.SP_ACCESSNUMBER, 'preview')">Preview</button> x-if="req.VAL1USER == '<?= session('userid'); ?>' || req.VAL2USER == '<?= session('userid'); ?>'">
<button class="btn btn-xs btn-outline btn-secondary"
@click="openUnvalDialog(req.SP_ACCESSNUMBER)"><i
class="fa-solid fa-rotate-right"></i></button>
</template>
</div>
</template> </template>
</td> </div>
<td><button x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="btn btn-xs" </td>
<td>
<template x-if="req.STATS !== 'PartColl' && req.STATS !== 'Coll' && req.STATS !== 'Pend'">
<button class="btn btn-xs btn-outline btn-primary"
@click="openPreviewDialog(req.SP_ACCESSNUMBER, 'preview', req)">Preview</button>
</template>
</td>
<td><button x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="btn btn-xs"
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></button></td> :class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></button></td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
</table> </table>
</template> </template>
</div>
<!-- Pagination Control -->
<div class="p-2 border-t border-base-200 bg-base-50 flex justify-between items-center"
x-show="!isLoading && list.length > 0">
<div class="text-xs text-base-content/60">
Showing <span class="font-bold" x-text="((currentPage - 1) * pageSize) + 1"></span> to
<span class="font-bold" x-text="Math.min(currentPage * pageSize, filtered.length)"></span> of
<span class="font-bold" x-text="filtered.length"></span> entries
</div> </div>
<div class="join">
<button class="join-item btn btn-sm" @click="prevPage()" :disabled="currentPage === 1">
<i class="fa fa-chevron-left"></i>
</button>
<button class="join-item btn btn-sm no-animation bg-base-100 cursor-default">
Page <span x-text="currentPage"></span> / <span x-text="totalPages"></span>
</button>
<button class="join-item btn btn-sm" @click="nextPage()" :disabled="currentPage === totalPages">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div> </div>
<?php echo $this->include('admin/dialog_sample'); ?> <?php echo $this->include('admin/dialog_sample'); ?>
<?php echo $this->include('admin/dialog_unval'); ?> <?php echo $this->include('admin/dialog_unval'); ?>
<?php echo $this->include('admin/dialog_preview'); ?> <?php echo $this->include('admin/dialog_preview'); ?>
</main> </main>
<?= $this->endSection(); ?> <?= $this->endSection(); ?>
<?= $this->section('script') ?> <?= $this->section('script') ?>
<script type="module"> <script type="module">
import Alpine from '<?=base_url("js/app.js");?>'; import Alpine from '<?= base_url("js/app.js"); ?>';
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data("dashboard", ()=> ({ Alpine.data("dashboard", () => ({
// dashboard // dashboard
today: "", today: "",
filter: { date1: "", date2: "" }, filter: { date1: "", date2: "" },
list: [], list: [],
isLoading: false, isLoading: false,
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 }, counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
statusColor: { statusColor: {
Pend: 'bg-white text-black font-bold', Pend: 'bg-white text-black font-bold',
PartColl: 'bg-orange-300 text-white font-bold', PartColl: 'bg-orange-300 text-white font-bold',
Coll: 'bg-orange-500 text-white font-bold', Coll: 'bg-orange-500 text-white font-bold',
PartRecv: 'bg-blue-200 text-black font-bold', PartRecv: 'bg-blue-200 text-black font-bold',
Recv: 'bg-blue-500 text-white font-bold', Recv: 'bg-blue-500 text-white font-bold',
Inc: 'bg-yellow-500 text-white font-bold', Inc: 'bg-yellow-500 text-white font-bold',
Fin: 'bg-green-500 text-white font-bold', Fin: 'bg-green-500 text-white font-bold',
}, },
filterTable :"", filterTable: "",
filterKey: 'Total', filterKey: 'Total',
statusMap: { filterKey: 'Total',
Total: [], statusMap: {
Pend: ['Pend'], Total: [],
Coll: ['Coll', 'PartColl'], Pend: ['Pend'],
Recv: ['Recv', 'PartRecv'], Coll: ['Coll', 'PartColl'],
Inc: ['Inc'], Recv: ['Recv', 'PartRecv'],
Fin: ['Fin'], Inc: ['Inc'],
}, Fin: ['Fin'],
},
init() { // Sorting & Pagination
this.today = new Date().toISOString().slice(0, 10); sortCol: 'REQDATE',
this.filter.date1 = this.today; sortAsc: false,
this.filter.date2 = this.today; currentPage: 1,
this.fetchList(); pageSize: 15,
},
fetchList(){ sort(col) {
this.isLoading = true; if (this.sortCol === col) {
this.list = []; this.sortAsc = !this.sortAsc;
let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 }; } else {
let param = new URLSearchParams(this.filter).toString(); this.sortCol = col;
for (let k in this.counters) { this.counters[k] = 0; } this.sortAsc = true;
fetch(`${BASEURL}/api/requests?${param}`, { }
method: 'GET', },
headers: {'Content-Type': 'application/json'},
}).then(res => res.json()).then(data => {
this.list = data.data ?? [];
this.filterKey = 'Total';
this.list.forEach(item => {
if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; }
else {
if(item.STATS == 'PartColl') { this.counters.Coll++; }
else if(item.STATS == 'PartRecv') { this.counters.Recv++; }
this.counters.Total++;
}
});
this.list.sort((a, b) => {
let codeA = statusOrder[a.STATS] ?? 0;
let codeB = statusOrder[b.STATS] ?? 0;
return codeA - codeB;
});
}).finally(() => {
this.isLoading = false;
});
},
reset() { nextPage() {
this.filter.date1 = this.today; if (this.currentPage < this.totalPages) this.currentPage++;
this.filter.date2 = this.today; },
this.fetchList();
},
isValidated (item) { prevPage() {
return item.ISVAL == 1 && item.ISPENDING != 1; if (this.currentPage > 1) this.currentPage--;
}, },
get filtered() {
let filteredList = this.list; get totalPages() {
if (this.filterKey === 'Validated') { return Math.ceil(this.filtered.length / this.pageSize) || 1;
filteredList = filteredList.filter(item => this.isValidated(item)); },
} else {
const validStatuses = this.statusMap[this.filterKey]; get sorted() {
if (validStatuses.length > 0) { return this.filtered.slice().sort((a, b) => {
filteredList = filteredList.filter(item => validStatuses.includes(item.STATS)); let modifier = this.sortAsc ? 1 : -1;
if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier;
if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier;
return 0;
});
},
get paginated() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.sorted.slice(start, end);
},
init() {
this.today = new Date().toISOString().slice(0, 10);
this.filter.date1 = this.today;
this.filter.date2 = this.today;
this.fetchList();
},
fetchList() {
this.isLoading = true;
this.list = [];
let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 };
let param = new URLSearchParams(this.filter).toString();
for (let k in this.counters) { this.counters[k] = 0; }
fetch(`${BASEURL}/api/requests?${param}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json()).then(data => {
this.list = data.data ?? [];
this.filterKey = 'Total';
this.list.forEach(item => {
if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; }
else {
if (item.STATS == 'PartColl') { this.counters.Coll++; }
else if (item.STATS == 'PartRecv') { this.counters.Recv++; }
this.counters.Total++;
} }
} });
if (this.filterTable) { this.list.sort((a, b) => {
const searchTerm = this.filterTable.toLowerCase(); let codeA = statusOrder[a.STATS] ?? 0;
filteredList = filteredList.filter(item => let codeB = statusOrder[b.STATS] ?? 0;
Object.values(item).some(value => return codeA - codeB;
String(value).toLowerCase().includes(searchTerm) });
) }).finally(() => {
); this.isLoading = false;
} });
return filteredList; },
},
get validatedCount() {
return this.list.filter(r => this.isValidated(r)).length;
},
/*
sample dialog
*/
item : '',
isDialogSampleOpen : false,
isSampleLoading: false,
openSampleDialog (accessnumber) { reset() {
this.isDialogSampleOpen = true; this.filter.date1 = this.today;
this.fetchItem(accessnumber) this.filter.date2 = this.today;
}, this.fetchList();
},
closeSampleDialog () {
this.isDialogSampleOpen = false;
},
fetchItem(accessnumber){ isValidated(item) {
this.isSampleLoading = true; return item.ISVAL == 1 && item.ISPENDING != 1;
this.item = []; },
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: {'Content-Type': 'application/json'}}) get filtered() {
// Reset pagination when filter changes (implied by this getter being accessed if dependencies change)
// However, side-effects in getters are tricky.
// Better to just let the user navigate back, or watch variables.
// For now, let's keep it pure.
let filteredList = this.list;
if (this.filterKey === 'Validated') {
filteredList = filteredList.filter(item => this.isValidated(item));
} else {
const validStatuses = this.statusMap[this.filterKey];
if (validStatuses.length > 0) {
filteredList = filteredList.filter(item => validStatuses.includes(item.STATS));
}
}
if (this.filterTable) {
const searchTerm = this.filterTable.toLowerCase();
filteredList = filteredList.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(searchTerm)
)
);
}
return filteredList;
},
get validatedCount() {
return this.list.filter(r => this.isValidated(r)).length;
},
/*
sample dialog
*/
item: '',
isDialogSampleOpen: false,
isSampleLoading: false,
openSampleDialog(accessnumber) {
this.isDialogSampleOpen = true;
this.fetchItem(accessnumber)
},
closeSampleDialog() {
this.isDialogSampleOpen = false;
},
fetchItem(accessnumber) {
this.isSampleLoading = true;
this.item = [];
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.item = data.data ?? {}; this.item = data.data ?? {};
if (!Array.isArray(this.item.samples)) this.item.samples = []; if (!Array.isArray(this.item.samples)) this.item.samples = [];
}).finally(() => { }).finally(() => {
this.isSampleLoading = false; this.isSampleLoading = false;
}); });
}, },
collect(sampcode, accessnumber) { collect(sampcode, accessnumber) {
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, { fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'}) body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
}) })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.fetchItem(accessnumber); this.fetchItem(accessnumber);
}); });
}, },
uncollect(sampcode, accessnumber) { uncollect(sampcode, accessnumber) {
if(!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return ;} if (!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return; }
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, { fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
method: 'DELETE', headers: {'Content-Type': 'application/json'}, method: 'DELETE', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'}) body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
}) })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.fetchItem(accessnumber); this.fetchItem(accessnumber);
}); });
}, },
unreceive(sampcode, accessnumber) { unreceive(sampcode, accessnumber) {
if(!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return ;} if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, { fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({samplenumber: sampcode, userid : '<?= session('userid'); ?>'}) body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
}) })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.fetchItem(accessnumber); this.fetchItem(accessnumber);
}); });
}, },
/* /*
preview dialog preview dialog
*/ */
isDialogPreviewOpen : false, isDialogPreviewOpen: false,
reviewed: false, reviewed: false,
previewAccessnumber : null, previewItem: null,
previewType : 'preview', openPreviewDialog(accessnumber, type, item) {
openPreviewDialog (accessnumber, type) { this.previewAccessnumber = accessnumber;
this.previewAccessnumber = accessnumber; this.previewItem = item;
this.previewType = type; this.previewType = type;
this.isDialogPreviewOpen = true; this.isDialogPreviewOpen = true;
this.reviewed = false; this.reviewed = false;
}, },
closePreviewDialog () { closePreviewDialog() {
this.isDialogPreviewOpen = false; this.isDialogPreviewOpen = false;
}, this.previewItem = null;
setPreviewType(type) { },
this.previewType = type; setPreviewType(type) {
}, this.previewType = type;
getPreviewUrl() { },
let base = 'http://glenlis/spooler_db/main_dev.php'; getPreviewUrl() {
let url = `${base}?acc=${this.previewAccessnumber}`; let base = 'http://glenlis/spooler_db/main_dev.php';
if (this.previewType === 'ind') url += '&lang=ID'; let url = `${base}?acc=${this.previewAccessnumber}`;
if (this.previewType === 'eng') url += '&lang=EN'; if (this.previewType === 'ind') url += '&lang=ID';
if (this.previewType === 'pdf') url += '&output=pdf'; if (this.previewType === 'eng') url += '&lang=EN';
return url; if (this.previewType === 'pdf') url += '&output=pdf';
},
validate(accessnumber, userid) { // Keep fallback for local dev if needed, but the above is the expected logic
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, { // return "http://localhost/application.html";
method: "POST", return url;
headers: {"Content-Type": "application/json"}, },
body: JSON.stringify({ userid:`${userid}` }) validate(accessnumber, userid) {
}).then(response => { fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
this.closePreviewDialog(); method: "POST",
this.fetchList(); headers: { "Content-Type": "application/json" },
console.log('Validate clicked for', this.previewAccessnumber, 'by user', userid); body: JSON.stringify({ userid: `${userid}` })
}); }).then(response => {
}, this.closePreviewDialog();
this.fetchList();
/* console.log('Validate clicked for', this.previewAccessnumber, 'by user', userid);
unvalidate dialog });
*/ },
isDialogUnvalOpen : false,
unvalReason : '', /*
unvalAccessnumber : null, unvalidate dialog
openUnvalDialog (accessnumber) { */
this.unvalReason = ''; isDialogUnvalOpen: false,
this.isDialogUnvalOpen = true; unvalReason: '',
this.unvalAccessnumber = accessnumber; unvalAccessnumber: null,
}, openUnvalDialog(accessnumber) {
unvalidate(accessnumber, userid) { this.unvalReason = '';
if(!confirm(`Unvalidate request ${accessnumber}?`)) { return ;} this.isDialogUnvalOpen = true;
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, { this.unvalAccessnumber = accessnumber;
method: "DELETE", },
headers: {"Content-Type": "application/json"}, unvalidate(accessnumber, userid) {
body: JSON.stringify({ userid:`${userid}`, comment: this.unvalReason.trim() }) if (!confirm(`Unvalidate request ${accessnumber}?`)) { return; }
}).then(response => { fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
this.closeUnvalDialog(); method: "DELETE",
this.fetchList(); headers: { "Content-Type": "application/json" },
console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`); body: JSON.stringify({ userid: `${userid}`, comment: this.unvalReason.trim() })
}); }).then(response => {
}, this.closeUnvalDialog();
closeUnvalDialog () { this.fetchList();
this.isDialogUnvalOpen = false; console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`);
}, });
})); },
}); closeUnvalDialog() {
this.isDialogUnvalOpen = false;
Alpine.start(); },
</script> }));
<?= $this->endSection(); ?> });
Alpine.start();
</script>
<?= $this->endSection(); ?>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="corporate"> <html lang="en" data-theme="corporate">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -14,49 +15,83 @@
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 0.71rem; font-size: 0.71rem;
} }
.navbar { .navbar {
padding: 0.2rem 1rem; padding: 0.2rem 1rem;
min-height: 0rem; min-height: 0rem;
} }
.card-body {
.card-body {
font-size: 0.71rem !important; font-size: 0.71rem !important;
} }
</style> </style>
</head> </head>
<body class="bg-base-200 min-h-screen flex flex-col" x-data="main">
<nav class="navbar bg-base-100 shadow-md px-6 z-20"> <body class="bg-base-200 min-h-screen" x-data="main">
<div class='flex-1'> <div class="drawer">
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'> <input id="main-drawer" type="checkbox" class="drawer-toggle" />
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Admin Dashboard</span> <div class="drawer-content flex flex-col min-h-screen">
</a> <!-- Navbar -->
</div> <nav class="navbar bg-base-100 shadow-md px-6 z-20">
<div class="flex gap-2"> <div class="flex-none">
<div class="text-right hidden sm:block leading-tight"> <label for="main-drawer" aria-label="open sidebar" class="btn btn-square btn-ghost">
<div class="text-sm font-bold opacity-70">Hi, <?=session('userid'); ?></div> <i class="fa fa-bars"></i>
<div class="text-xs opacity-50">Administrator</div> </label>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div> </div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-xl border border-base-200 mt-2"> <div class='flex-1'>
<li><a class="active:bg-primary" href="<?=base_url('admin') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li> <a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2 ml-2'>
<li><a class="active:bg-primary" href="<?=base_url('admin/users') ?>"><i class="fa fa-users mr-2"></i> Users </a></li> <i class="fa fa-cube"></i> CMOD <span
<li><a @click.prevent="openDialogSetPassword()" class="active:bg-primary"><i class="fa fa-key mr-2"></i> Change Password</a></li> class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Admin Dashboard</span>
<li class="divider my-1"></li> </a>
<li><a href="<?=base_url('logout')?>" class="text-error hover:bg-error/10"><i class="fa fa-sign-out-alt mr-2"></i> Logout</a></li> </div>
</ul> <div class="flex gap-2">
</div> <div class="text-right hidden sm:block leading-tight">
</div> <div class="text-sm font-bold opacity-70">Hi, <?= session('userid'); ?></div>
</nav> <div class="text-xs opacity-50">Administrator</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div>
</div>
</div>
</nav>
<!-- Page Content -->
<?= $this->renderSection('content'); ?>
<?= $this->include('admin/dialog_setPassword'); ?>
<footer class='bg-base-100 p-1 mt-auto'>&copy; <?= date('Y'); ?> - 5Panda</footer>
</div>
<!-- Sidebar -->
<div class="drawer-side z-50">
<label for="main-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-100 text-base-content min-h-full w-80 p-4 flex flex-col">
<!-- Sidebar content here -->
<li class="mb-4">
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'>
<i class="fa fa-cube"></i> CMOD
</a>
</li>
<li><a href="<?= base_url('admin') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
<li><a href="<?= base_url('admin/users') ?>"><i class="fa fa-users mr-2"></i> Users </a></li>
<div class="mt-auto">
<li class="menu-title">Account</li>
<li><a @click.prevent="openDialogSetPassword()"><i class="fa fa-key mr-2"></i> Change Password</a></li>
<li><a href="<?= base_url('logout') ?>" class="text-error hover:bg-error/10"><i
class="fa fa-sign-out-alt mr-2"></i> Logout</a></li>
</div>
</ul>
</div>
</div>
<?=$this->renderSection('content');?>
<?=$this->include('admin/dialog_setPassword');?>
<footer class='bg-base-100 p-1'>&copy; <?=date('Y');?> - 5Panda</footer>
<script> <script>
window.BASEURL = "<?=base_url("admin");?>"; window.BASEURL = "<?= base_url("admin"); ?>";
</script> </script>
<?=$this->renderSection('script');?> <?= $this->renderSection('script'); ?>
</body> </body>
</html>
</html>

View File

@ -1,176 +1,217 @@
<?= $this->extend('lab/main'); ?> <?= $this->extend('lab/main'); ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard"> <main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
<div class="card bg-base-100 shadow-xl h-full border border-base-200 overflow-hidden"> <div class="card bg-base-100 shadow-xl h-full border border-base-200 overflow-hidden">
<div class="card-body p-0 h-full flex flex-col"> <div class="card-body p-0 h-full flex flex-col">
<!-- Header & Filters --> <!-- Header & Filters -->
<div class="p-4 border-b border-base-200 bg-base-50"> <div class="p-4 border-b border-base-200 bg-base-50">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-4"> <div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-4">
<div class="flex-1"> <div class="flex-1">
<h2 class="text-2xl font-bold flex items-center gap-2 text-base-content"> <h2 class="text-2xl font-bold flex items-center gap-2 text-base-content">
<i class="fa fa-chart-bar text-primary"></i> Requests Overview <i class="fa fa-chart-bar text-primary"></i> Requests Overview
</h2> </h2>
</div> </div>
<!-- Status Filters --> <!-- Status Filters -->
<div class="join shadow-sm bg-base-100 rounded-lg"> <div class="join shadow-sm bg-base-100 rounded-lg">
<button @click="filterKey = 'Total'" :class="filterKey === 'Total' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item"> <button @click="filterKey = 'Total'"
All <span class="badge badge-sm badge-ghost ml-1" x-text="counters.Total"></span> :class="filterKey === 'Total' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
</button> class="btn btn-sm join-item">
<button @click="filterKey = 'Pend'" :class="filterKey === 'Pend' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item"> All <span class="badge badge-sm badge-ghost ml-1" x-text="counters.Total"></span>
Pending <span class="badge badge-sm ml-1" x-text="counters.Pend"></span> </button>
</button> <button @click="filterKey = 'Pend'"
<button @click="filterKey = 'Coll'" :class="filterKey === 'Coll' ? 'btn-active btn-warning text-white' : 'btn-ghost'" class="btn btn-sm join-item"> :class="filterKey === 'Pend' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
Coll <span class="badge badge-sm badge-warning ml-1" x-text="counters.Coll"></span> class="btn btn-sm join-item">
</button> Pending <span class="badge badge-sm ml-1" x-text="counters.Pend"></span>
<button @click="filterKey = 'Recv'" :class="filterKey === 'Recv' ? 'btn-active btn-info text-white' : 'btn-ghost'" class="btn btn-sm join-item"> </button>
Recv <span class="badge badge-sm badge-info ml-1" x-text="counters.Recv"></span> <button @click="filterKey = 'Coll'"
</button> :class="filterKey === 'Coll' ? 'btn-active btn-warning text-white' : 'btn-ghost'"
<button @click="filterKey = 'Inc'" :class="filterKey === 'Inc' ? 'btn-active btn-error text-white' : 'btn-ghost'" class="btn btn-sm join-item"> class="btn btn-sm join-item">
Inc <span class="badge badge-sm badge-error ml-1" x-text="counters.Inc"></span> Coll <span class="badge badge-sm badge-warning ml-1" x-text="counters.Coll"></span>
</button> </button>
<button @click="filterKey = 'Fin'" :class="filterKey === 'Fin' ? 'btn-active btn-success text-white' : 'btn-ghost'" class="btn btn-sm join-item"> <button @click="filterKey = 'Recv'"
Fin <span class="badge badge-sm badge-success ml-1" x-text="counters.Fin"></span> :class="filterKey === 'Recv' ? 'btn-active btn-info text-white' : 'btn-ghost'"
</button> class="btn btn-sm join-item">
<button @click="filterKey = 'Validated'" :class="filterKey === 'Validated' ? 'btn-active btn-primary text-white' : 'btn-ghost'" class="btn btn-sm join-item"> Recv <span class="badge badge-sm badge-info ml-1" x-text="counters.Recv"></span>
Val <span class="badge badge-sm badge-primary ml-1" x-text="validatedCount"></span> </button>
</button> <button @click="filterKey = 'Inc'"
:class="filterKey === 'Inc' ? 'btn-active btn-error text-white' : 'btn-ghost'"
class="btn btn-sm join-item">
Inc <span class="badge badge-sm badge-error ml-1" x-text="counters.Inc"></span>
</button>
<button @click="filterKey = 'Fin'"
:class="filterKey === 'Fin' ? 'btn-active btn-success text-white' : 'btn-ghost'"
class="btn btn-sm join-item">
Fin <span class="badge badge-sm badge-success ml-1" x-text="counters.Fin"></span>
</button>
<button @click="filterKey = 'Validated'"
:class="filterKey === 'Validated' ? 'btn-active btn-primary text-white' : 'btn-ghost'"
class="btn btn-sm join-item">
Val <span class="badge badge-sm badge-primary ml-1" x-text="validatedCount"></span>
</button>
</div>
</div>
<!-- Search & Date Filter -->
<div
class="flex flex-col md:flex-row gap-3 items-end bg-base-100 p-3 rounded-lg border border-base-200 shadow-sm">
<div class="form-control">
<label class="label text-xs font-bold py-1 text-base-content/60">Date Range</label>
<div class="join">
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date1" />
<span class="join-item btn btn-sm btn-ghost no-animation bg-base-200 font-normal px-2">-</span>
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date2" />
</div> </div>
</div> </div>
<!-- Search & Date Filter --> <div class="flex gap-2">
<div class="flex flex-col md:flex-row gap-3 items-end bg-base-100 p-3 rounded-lg border border-base-200 shadow-sm"> <button class="btn btn-sm btn-primary" @click='fetchList()'><i class='fa fa-search'></i> Search</button>
<div class="form-control"> <button class="btn btn-sm btn-neutral" @click='reset()'><i class='fa fa-sync-alt'></i> Reset</button>
<label class="label text-xs font-bold py-1 text-base-content/60">Date Range</label> </div>
<div class="join">
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date1"/>
<span class="join-item btn btn-sm btn-ghost no-animation bg-base-200 font-normal px-2">-</span>
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date2"/>
</div>
</div>
<div class="flex gap-2">
<button class="btn btn-sm btn-primary" @click='fetchList()'><i class='fa fa-search'></i> Search</button>
<button class="btn btn-sm btn-neutral" @click='reset()'><i class='fa fa-sync-alt'></i> Reset</button>
</div>
<span class="flex-1"></span> <span class="flex-1"></span>
<div class="form-control w-full md:w-auto"> <div class="form-control w-full md:w-auto">
<div class="relative"> <div class="relative">
<i class="fa fa-filter absolute left-3 top-2.5 text-base-content/30 text-xs"></i> <i class="fa fa-filter absolute left-3 top-2.5 text-base-content/30 text-xs"></i>
<input type="text" class="input input-sm input-bordered w-full md:w-64 pl-8" placeholder="Type to filter..." x-model="filterTable" /> <input type="text" class="input input-sm input-bordered w-full md:w-64 pl-8"
</div> placeholder="Type to filter..." x-model="filterTable" />
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="flex-1 overflow-y-auto px-4 pb-4"> <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"> <table class="table table-xs table-zebra w-full">
<thead class="bg-base-100 sticky top-0 z-10"> <thead class="bg-base-100 sticky top-0 z-10">
<tr>
<th style='width:7%;'>
<div class="skeleton h-4 w-20"></div>
</th>
<th style='width:15%;'>
<div class="skeleton h-4 w-32"></div>
</th>
<th style='width:7%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<div class="skeleton h-4 w-16"></div>
</th>
<th style='width:8%;'>
<div class="skeleton h-4 w-20"></div>
</th>
<th style='width:15%;'>
<div class="skeleton h-4 w-32"></div>
</th>
<th style='width:5%;'>
<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:4%;'>
<div class="skeleton h-4 w-12"></div>
</th>
</tr>
</thead>
<tbody>
<template x-for="i in 5" :key="i">
<tr> <tr>
<th style='width:7%;'><div class="skeleton h-4 w-20"></div></th> <td colspan="9">
<th style='width:15%;'><div class="skeleton h-4 w-32"></div></th> <div class="skeleton h-4 w-full"></div>
<th style='width:7%;'><div class="skeleton h-4 w-16"></div></th> </td>
<th style='width:8%;'><div class="skeleton h-4 w-16"></div></th>
<th style='width:8%;'><div class="skeleton h-4 w-20"></div></th>
<th style='width:15%;'><div class="skeleton h-4 w-32"></div></th>
<th style='width:5%;'><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:4%;'><div class="skeleton h-4 w-12"></div></th>
</tr> </tr>
</thead> </template>
<tbody> </tbody>
<template x-for="i in 5" :key="i"> </table>
<tr> </template>
<td colspan="9"><div class="skeleton h-4 w-full"></div></td> <template x-if="!isLoading && !list.length">
</tr> <div class="text-center py-10">
</template> <i class="fa fa-inbox text-4xl mb-2 opacity-50"></i>
</tbody> <p>No records found</p>
</table> </div>
</template> </template>
<template x-if="!isLoading && !list.length"> <template x-if="!isLoading && list.length">
<div class="text-center py-10"> <table class="table table-xs table-zebra w-full">
<i class="fa fa-inbox text-4xl mb-2 opacity-50"></i> <thead class="bg-base-100 sticky top-0 z-10">
<p>No records found</p> <tr>
</div> <th style='width:7%;'>Order Datetime</th>
</template> <th style='width:15%;'>Patient Name</th>
<template x-if="!isLoading && list.length"> <th style='width:7%;'>No Lab</th>
<table class="table table-xs table-zebra w-full"> <th style='width:8%;'>Reff</th>
<thead class="bg-base-100 sticky top-0 z-10"> <th style='width:8%;'>Doctor</th>
<tr> <th style='width:15%;'>Tests</th>
<th style='width:7%;'>Order Datetime</th> <th style='width:5%;'>Result To</th>
<th style='width:15%;'>Patient Name</th> <th style='width:5%;'>Validation</th>
<th style='width:7%;'>No Lab</th> <th style='width:4%;'>Status</th>
<th style='width:8%;'>Reff</th> </tr>
<th style='width:8%;'>Doctor</th> </thead>
<th style='width:15%;'>Tests</th> <tbody>
<th style='width:5%;'>Result To</th> <template x-for="req in filtered" :key="req.SP_ACCESSNUMBER">
<th style='width:5%;'>Validation</th> <tr class="hover:bg-base-300">
<th style='width:4%;'>Status</th> <td x-text="req.REQDATE"></td>
</tr> <td x-text="req.Name"></td>
</thead> <td x-text="req.SP_ACCESSNUMBER"></td>
<tbody> <td x-text="req.REFF"></td>
<template x-for="req in filtered" :key="req.SP_ACCESSNUMBER"> <td x-text="req.DOC"></td>
<tr class="hover:bg-base-300"> <td x-text="req.TESTS"></td>
<td x-text="req.REQDATE"></td> <td x-text="req.ODR_CRESULT_TO"></td>
<td x-text="req.Name"></td> <td>
<td x-text="req.SP_ACCESSNUMBER"></td> <div class='flex gap-1 items-center'>
<td x-text="req.REFF"></td> <div class='w-15'>
<td x-text="req.DOC"></td>
<td x-text="req.TESTS"></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>1: <span x-text="req.VAL1USER"></span></p>
<p>2: <span x-text="req.VAL2USER"></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'>
<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)"><i class="fa-solid fa-rotate-right"></i></button>
</template>
<template x-if="req.VAL1USER != '<?=session('userid');?>' && req.VAL2USER != '<?=session('userid');?>'">
<button class="btn btn-xs btn-outline btn-success" @click="openValDialog(req.SP_ACCESSNUMBER)"><i class="fa-solid fa-check"></i></button>
</template>
</div>
</template>
</div> </div>
</td> <template x-if="req.ISVAL == 1 && req.ISPENDING != 1">
<td><button x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="btn btn-xs" <div class='text-center'>
<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)"><i
class="fa-solid fa-rotate-right"></i></button>
</template>
<template
x-if="(req.VAL1USER != '<?= session('userid'); ?>' && req.VAL2USER != '<?= session('userid'); ?>') && (!req.VAL1USER || !req.VAL2USER)">
<button class="btn btn-xs btn-outline btn-success"
@click="openValDialog(req.SP_ACCESSNUMBER)"><i class="fa-solid fa-check"></i></button>
</template>
</div>
</template>
</div>
</td>
<td><button x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="btn btn-xs"
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></button></td> :class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></button></td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
</table> </table>
</template> </template>
</div> </div>
</div> </div>
<?php echo $this->include('admin/dialog_sample'); ?> <?php echo $this->include('admin/dialog_sample'); ?>
<?php echo $this->include('admin/dialog_val'); ?> <?php echo $this->include('admin/dialog_val'); ?>
<?php echo $this->include('admin/dialog_unval'); ?> <?php echo $this->include('admin/dialog_unval'); ?>
</main> </main>
<?= $this->endSection(); ?> <?= $this->endSection(); ?>
<?= $this->section('script') ?> <?= $this->section('script') ?>
<script type="module"> <script type="module">
import Alpine from '<?=base_url("js/app.js");?>'; import Alpine from '<?= base_url("js/app.js"); ?>';
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data("dashboard", ()=> ({ Alpine.data("dashboard", () => ({
// dashboard // dashboard
today: "", today: "",
filter: { date1: "", date2: "" }, filter: { date1: "", date2: "" },
list: [], list: [],
isLoading: false, isLoading: false,
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 }, counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
statusColor: { statusColor: {
Pend: 'bg-white text-black font-bold', Pend: 'bg-white text-black font-bold',
PartColl: 'bg-orange-300 text-white font-bold', PartColl: 'bg-orange-300 text-white font-bold',
@ -180,7 +221,7 @@
Inc: 'bg-yellow-500 text-white font-bold', Inc: 'bg-yellow-500 text-white font-bold',
Fin: 'bg-green-500 text-white font-bold', Fin: 'bg-green-500 text-white font-bold',
}, },
filterTable :"", filterTable: "",
filterKey: 'Total', filterKey: 'Total',
statusMap: { statusMap: {
Total: [], Total: [],
@ -198,7 +239,7 @@
this.fetchList(); this.fetchList();
}, },
fetchList(){ fetchList() {
this.isLoading = true; this.isLoading = true;
this.list = []; this.list = [];
let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 }; let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 };
@ -207,17 +248,17 @@
for (let k in this.counters) { this.counters[k] = 0; } for (let k in this.counters) { this.counters[k] = 0; }
fetch(`${BASEURL}/api/requests?${param}`, { fetch(`${BASEURL}/api/requests?${param}`, {
method: 'GET', method: 'GET',
headers: {'Content-Type': 'application/json'}, headers: { 'Content-Type': 'application/json' },
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
this.list = data.data ?? []; this.list = data.data ?? [];
this.filterKey = 'Total'; this.filterKey = 'Total';
// count + sort in a single loop // count + sort in a single loop
this.list.forEach(item => { this.list.forEach(item => {
if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; } if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; }
else { else {
if(item.STATS == 'PartColl') { this.counters.Coll++; } if (item.STATS == 'PartColl') { this.counters.Coll++; }
else if(item.STATS == 'PartRecv') { this.counters.Recv++; } else if (item.STATS == 'PartRecv') { this.counters.Recv++; }
this.counters.Total++; this.counters.Total++;
} }
}); });
this.list.sort((a, b) => { this.list.sort((a, b) => {
@ -236,7 +277,7 @@
this.fetchList(); this.fetchList();
}, },
isValidated (item) { isValidated(item) {
return item.ISVAL == 1 && item.ISPENDING != 1; return item.ISVAL == 1 && item.ISPENDING != 1;
}, },
get filtered() { get filtered() {
@ -262,74 +303,74 @@
get validatedCount() { get validatedCount() {
return this.list.filter(r => this.isValidated(r)).length; return this.list.filter(r => this.isValidated(r)).length;
}, },
/* /*
sample dialog sample dialog
*/ */
item : '', item: '',
isDialogSampleOpen : false, isDialogSampleOpen: false,
isSampleLoading: false, isSampleLoading: false,
openSampleDialog (accessnumber) { openSampleDialog(accessnumber) {
this.isDialogSampleOpen = true; this.isDialogSampleOpen = true;
this.fetchItem(accessnumber) this.fetchItem(accessnumber)
}, },
closeSampleDialog () { closeSampleDialog() {
this.isDialogSampleOpen = false; this.isDialogSampleOpen = false;
}, },
fetchItem(accessnumber){ fetchItem(accessnumber) {
this.isSampleLoading = true; this.isSampleLoading = true;
this.item = []; this.item = [];
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: {'Content-Type': 'application/json'}}) fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.item = data.data ?? {}; this.item = data.data ?? {};
if (!Array.isArray(this.item.samples)) this.item.samples = []; if (!Array.isArray(this.item.samples)) this.item.samples = [];
}).finally(() => { }).finally(() => {
this.isSampleLoading = false; this.isSampleLoading = false;
}); });
}, },
collect(sampcode, accessnumber) { collect(sampcode, accessnumber) {
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, { fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'}) body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
}) })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.fetchItem(accessnumber); this.fetchItem(accessnumber);
}); });
}, },
uncollect(sampcode, accessnumber) { uncollect(sampcode, accessnumber) {
if(!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return ;} if (!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return; }
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, { fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
method: 'DELETE', headers: {'Content-Type': 'application/json'}, method: 'DELETE', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'}) body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
}) })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.fetchItem(accessnumber); this.fetchItem(accessnumber);
}); });
}, },
unreceive(sampcode, accessnumber) { unreceive(sampcode, accessnumber) {
if(!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return ;} if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, { fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({samplenumber: sampcode, userid : '<?= session('userid'); ?>'}) body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
}) })
.then(res => res.json()).then(data => { .then(res => res.json()).then(data => {
this.fetchItem(accessnumber); this.fetchItem(accessnumber);
}); });
}, },
/* /*
validate dialog validate dialog
*/ */
isDialogValOpen : false, isDialogValOpen: false,
isValidateEnabled: false, isValidateEnabled: false,
valAccessnumber : null, valAccessnumber: null,
openValDialog (accessnumber) { openValDialog(accessnumber) {
this.isDialogValOpen = true; this.isDialogValOpen = true;
this.valAccessnumber = accessnumber; this.valAccessnumber = accessnumber;
this.$nextTick(() => { this.$nextTick(() => {
@ -363,51 +404,51 @@
} }
}); });
}, },
closeValDialog () { closeValDialog() {
this.isDialogValOpen = false; this.isDialogValOpen = false;
}, },
validate(accessnumber, userid) { validate(accessnumber, userid) {
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, { fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userid:`${userid}` }) body: JSON.stringify({ userid: `${userid}` })
}).then(response => { }).then(response => {
this.closeValDialog(); this.closeValDialog();
this.fetchList(); this.fetchList();
console.log('Validate clicked for', this.valAccessnumber, 'by user', userid); console.log('Validate clicked for', this.valAccessnumber, 'by user', userid);
}); });
}, },
/* /*
unvalidate dialog unvalidate dialog
*/ */
isDialogUnvalOpen : false, isDialogUnvalOpen: false,
unvalReason : '', unvalReason: '',
unvalAccessnumber : null, unvalAccessnumber: null,
openUnvalDialog (accessnumber) { openUnvalDialog(accessnumber) {
this.unvalReason = ''; this.unvalReason = '';
this.isDialogUnvalOpen = true; this.isDialogUnvalOpen = true;
this.unvalAccessnumber = accessnumber; this.unvalAccessnumber = accessnumber;
}, },
unvalidate(accessnumber, userid) { unvalidate(accessnumber, userid) {
if(!confirm(`Unvalidate request ${accessnumber}?`)) { return ;} if (!confirm(`Unvalidate request ${accessnumber}?`)) { return; }
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, { fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
method: "DELETE", method: "DELETE",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userid:`${userid}`, comment: this.unvalReason.trim() }) body: JSON.stringify({ userid: `${userid}`, comment: this.unvalReason.trim() })
}).then(response => { }).then(response => {
this.closeUnvalDialog(); this.closeUnvalDialog();
this.fetchList(); this.fetchList();
console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`); console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`);
}); });
}, },
closeUnvalDialog () { closeUnvalDialog() {
this.isDialogUnvalOpen = false; this.isDialogUnvalOpen = false;
}, },
})); }));
}); });
Alpine.start(); Alpine.start();
</script> </script>
<?= $this->endSection(); ?> <?= $this->endSection(); ?>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="corporate"> <html lang="en" data-theme="corporate">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -14,49 +15,81 @@
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 0.71rem; font-size: 0.71rem;
} }
.navbar { .navbar {
padding: 0.2rem 1rem; padding: 0.2rem 1rem;
min-height: 0rem; min-height: 0rem;
} }
.card-body {
.card-body {
font-size: 0.71rem !important; font-size: 0.71rem !important;
} }
</style> </style>
</head> </head>
<body class="bg-base-200 min-h-screen flex flex-col">
<nav class="navbar bg-base-100 shadow-md px-6 z-20"> <body class="bg-base-200 min-h-screen">
<div class='flex-1'> <div class="drawer">
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'> <input id="main-drawer" type="checkbox" class="drawer-toggle" />
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Admin Dashboard</span> <div class="drawer-content flex flex-col min-h-screen">
</a> <!-- Navbar -->
</div> <nav class="navbar bg-base-100 shadow-md px-6 z-20">
<div class="flex gap-2"> <div class="flex-none">
<div class="text-right hidden sm:block leading-tight"> <label for="main-drawer" aria-label="open sidebar" class="btn btn-square btn-ghost">
<div class="text-sm font-bold opacity-70">Hi, <?=session('userid'); ?></div> <i class="fa fa-bars"></i>
<div class="text-xs opacity-50">Lab User</div> </label>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div> </div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-xl border border-base-200 mt-2"> <div class='flex-1'>
<li><a class="active:bg-primary" href="<?=base_url('lab') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li> <a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2 ml-2'>
<li><a class="active:bg-primary" href="<?=base_url('setPassword') ?>"><i class="fa fa-key mr-2"></i> Set Password</a></li> <i class="fa fa-cube"></i> CMOD <span
<li class="divider my-1"></li> class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Admin Dashboard</span>
<li><a href="<?=base_url('logout')?>" class="text-error hover:bg-error/10"><i class="fa fa-sign-out-alt mr-2"></i> Logout</a></li> </a>
</ul> </div>
</div> <div class="flex gap-2">
<div class="text-right hidden sm:block leading-tight">
<div class="text-sm font-bold opacity-70">Hi, <?= session('userid'); ?></div>
<div class="text-xs opacity-50">Lab User</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div>
</div>
</div>
</nav>
<!-- Page Content -->
<?= $this->renderSection('content'); ?>
<footer class='bg-base-100 p-1 mt-auto'>&copy; <?= date('Y'); ?> - 5Panda</footer>
</div> </div>
</nav>
<?=$this->renderSection('content');?> <!-- Sidebar -->
<div class="drawer-side z-50">
<label for="main-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-100 text-base-content min-h-full w-80 p-4 flex flex-col">
<!-- Sidebar content here -->
<li class="mb-4">
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'>
<i class="fa fa-cube"></i> CMOD
</a>
</li>
<li><a href="<?= base_url('lab') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
<div class="mt-auto">
<li class="menu-title">Account</li>
<li><a href="<?= base_url('setPassword') ?>"><i class="fa fa-key mr-2"></i> Set Password</a></li>
<li><a href="<?= base_url('logout') ?>" class="text-error hover:bg-error/10"><i
class="fa fa-sign-out-alt mr-2"></i> Logout</a></li>
</div>
</ul>
</div>
</div>
<footer class='bg-base-100 p-1'>&copy; <?=date('Y');?> - 5Panda</footer>
<script> <script>
window.BASEURL = "<?=base_url("lab");?>"; window.BASEURL = "<?= base_url("lab"); ?>";
</script> </script>
<?=$this->renderSection('script');?> <?= $this->renderSection('script'); ?>
</body> </body>
</html> </html>