Update semua role pada tempatnya done
This commit is contained in:
parent
a81a919d68
commit
207948aeb3
38
P0_log.txt
38
P0_log.txt
@ -19,5 +19,41 @@
|
||||
('Phlebotomist', 3, 'Pengambilan dan pencatatan spesimen'),
|
||||
('Customer Service', 4, 'Monitoring & pelayanan informasi pasien');
|
||||
|
||||
2. Tambahkan [USERROLEID] dan [NAME] pada Tabel USERS
|
||||
2. Hapus semua isi USERS lalu :
|
||||
INSERT INTO USERS(USERID, [PASSWORD], USERROLEID, NAME)
|
||||
VALUES
|
||||
('LISFSE', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 0, 'SYSTEM'),
|
||||
('ABB', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Ardea Bagus Bimantara, S.Tr.Kes'),
|
||||
('AHT', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 1, 'dr. Arifoe Hajat, Sp.PK(K)'),
|
||||
|
||||
('ASW', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Asti Sri Wiyanti, A.Md.AK'),
|
||||
('BYS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Betha Yogyanti Setyarini, A.Md.AK'),
|
||||
|
||||
('FKS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Fairushafa Khairunnisa Sasmita, S.Tr.Kes'),
|
||||
('HAY', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Hewi Aryanti, A.Md.Kep'),
|
||||
|
||||
('KNS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Kartya Nur Sholihatul Umah, S.Kep.Ns'),
|
||||
('LPS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Lintang Pramuli Suradi, S.S.T'),
|
||||
|
||||
('LDK', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Lidya Dwindana Kartikasari, A.Md.Kes'),
|
||||
|
||||
('RID', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 4, 'Rhemanda Ivena Dinta, A.Md.Bns'),
|
||||
('MDW', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Margareta Dwi Widiani, A.Md.Kep'),
|
||||
('MJS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Mentari Jaya Sari, A.Md.AK'),
|
||||
('MRS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Maria Scholastica, A.Md.Kep'),
|
||||
|
||||
('AQP', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Anjani Okta Prastiwi, A.Md.Kep'),
|
||||
|
||||
('RSW', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 0, 'Ratna Setyowati, A.Md.T'),
|
||||
('SAI', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 1, 'Sri Andayani, A.Md.Kes'),
|
||||
('SFB', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Stevani Florentina Bihi, S.Pd., S.Tr.Kes'),
|
||||
|
||||
('VSO', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Veronica Sulistyo, A.Md.Kes'),
|
||||
('YAA', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 2, 'Yulia Anita, A.Md.Kes'),
|
||||
('SYA', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Sutyi Yuliyana, A.Md.Kep'),
|
||||
|
||||
('MTP', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 0, 'Muhammad Tegar Prasetya, S.Kom'),
|
||||
('NRR', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 0, 'Nur Rizky Romadhon, M.Tr.Kom'),
|
||||
('LAS', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 3, 'Luthfi Anindyani Sulistiono, S.Kep.Ns'),
|
||||
('HSI', '$2y$10$qUKTBKk.gJsgIKKlNB5QwuJ4TFLBl6buARUjaY9eSSmdDX3EO/tSi', 1, 'Dr. dr. Hermi Suprati, M.Kes');
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class Lab extends BaseController {
|
||||
class Cs extends BaseController {
|
||||
|
||||
public function __construct() {
|
||||
helper(['url', 'form', 'text']);
|
||||
|
||||
54
app/Views/cs/dialog_preview.php
Normal file
54
app/Views/cs/dialog_preview.php
Normal file
@ -0,0 +1,54 @@
|
||||
<dialog class="modal" :open="isDialogPreviewOpen">
|
||||
<template x-if="previewAccessnumber">
|
||||
<div class="modal-box w-11/12 max-w-7xl h-[90vh] flex flex-col p-0 overflow-hidden bg-base-100">
|
||||
<!-- Header -->
|
||||
<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">
|
||||
<i class="fa fa-eye text-primary"></i>
|
||||
Preview
|
||||
<span class="badge badge-ghost text-xs" x-text="previewAccessnumber"></span>
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="join shadow-sm" x-show="previewItem && previewItem.VAL1USER && previewItem.VAL2USER">
|
||||
<button @click="setPreviewType('preview')"
|
||||
:class="previewType === 'preview' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
|
||||
class="btn btn-sm join-item">Default</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>
|
||||
<button class="btn btn-sm btn-circle btn-ghost" @click="closePreviewDialog()">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-3 bg-base-200 border-t border-base-300 flex justify-end items-center gap-4">
|
||||
<label class="label cursor-pointer gap-2 mb-0">
|
||||
<input type="checkbox" x-model="reviewed" class="checkbox checkbox-sm checkbox-primary" />
|
||||
<span class="label-text text-sm">I have reviewed the results</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-ghost" @click="closePreviewDialog()">Cancel</button>
|
||||
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
|
||||
@click="validate(previewAccessnumber, '<?= session('userid'); ?>')" :disabled="!reviewed">
|
||||
<i class="fa fa-check mr-1"></i> Validate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dialog>
|
||||
28
app/Views/cs/dialog_setPassword.php
Normal file
28
app/Views/cs/dialog_setPassword.php
Normal file
@ -0,0 +1,28 @@
|
||||
<dialog class="modal" :open="isDialogSetPasswordOpen">
|
||||
<div class="modal-box w-96">
|
||||
<h3 class="font-bold text-lg mb-4">Change Password</h3>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">New Password</span>
|
||||
</label>
|
||||
<input type="password" x-model="password" class="input input-bordered w-full" placeholder="Enter new password" />
|
||||
</div>
|
||||
<div class="form-control w-full mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text">Confirm Password</span>
|
||||
</label>
|
||||
<input type="password" x-model="confirm_password" class="input input-bordered w-full" placeholder="Confirm new password" />
|
||||
</div>
|
||||
<div x-show="error" class="alert alert-error mt-3 text-sm">
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" @click="closeDialogSetPassword()">Cancel</button>
|
||||
<button class="btn btn-primary" @click="savePassword('<?=session('userid'); ?>')" :disabled="isLoading">
|
||||
<span x-show="isLoading" class="loading loading-spinner loading-sm"></span>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop bg-black/30" @click="closeDialogSetPassword()"></div>
|
||||
</dialog>
|
||||
@ -1,176 +1,299 @@
|
||||
<?= $this->extend('cs/main'); ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<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-body p-0 h-full flex flex-col">
|
||||
|
||||
<!-- Header & Filters -->
|
||||
<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-1">
|
||||
<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
|
||||
</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>
|
||||
<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-body p-0 h-full flex flex-col">
|
||||
|
||||
<!-- Header & Filters -->
|
||||
<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-1">
|
||||
<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
|
||||
</h2>
|
||||
</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 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>
|
||||
|
||||
<div class="form-control w-full md:w-auto">
|
||||
<div class="relative">
|
||||
<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" />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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 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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<td colspan="11">
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="i in 5" :key="i">
|
||||
<tr>
|
||||
<td colspan="9"><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>
|
||||
</template>
|
||||
<template x-if="!isLoading && list.length">
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th style='width:7%;'>Order Datetime</th>
|
||||
<th style='width:15%;'>Patient Name</th>
|
||||
<th style='width:7%;'>No Lab</th>
|
||||
<th style='width:8%;'>Reff</th>
|
||||
<th style='width:8%;'>Doctor</th>
|
||||
<th style='width:15%;'>Tests</th>
|
||||
<th style='width:5%;'>Result To</th>
|
||||
<th style='width:5%;'>Validation</th>
|
||||
<th style='width:4%;'>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="req in filtered" :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"></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'>
|
||||
</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>
|
||||
</template>
|
||||
<template x-if="!isLoading && list.length">
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<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 cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
<td x-text="req.HOSTORDERNUMBER" class="font-bold cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></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>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>
|
||||
</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>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<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>
|
||||
</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 x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="font-bold cursor-pointer"
|
||||
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</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 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>
|
||||
|
||||
<?php echo $this->include('admin/dialog_sample'); ?>
|
||||
<?php echo $this->include('admin/dialog_val'); ?>
|
||||
<?php echo $this->include('admin/dialog_unval'); ?>
|
||||
|
||||
</main>
|
||||
|
||||
<?php echo $this->include('cs/dialog_sample'); ?>
|
||||
<?php echo $this->include('cs/dialog_unval'); ?>
|
||||
<?php echo $this->include('cs/dialog_preview'); ?>
|
||||
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section('script') ?>
|
||||
<script type="module">
|
||||
import Alpine from '<?=base_url("js/app.js");?>';
|
||||
<script type="module">
|
||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("dashboard", ()=> ({
|
||||
Alpine.data("dashboard", () => ({
|
||||
// dashboard
|
||||
today: "",
|
||||
filter: { date1: "", date2: "" },
|
||||
list: [],
|
||||
isLoading: false,
|
||||
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
|
||||
filter: { date1: "", date2: "" },
|
||||
list: [],
|
||||
isLoading: false,
|
||||
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
|
||||
statusColor: {
|
||||
Pend: 'bg-white text-black font-bold',
|
||||
PartColl: 'bg-orange-300 text-white font-bold',
|
||||
@ -180,44 +303,88 @@
|
||||
Inc: 'bg-yellow-500 text-white font-bold',
|
||||
Fin: 'bg-green-500 text-white font-bold',
|
||||
},
|
||||
filterTable :"",
|
||||
filterTable: "",
|
||||
filterKey: 'Total',
|
||||
filterKey: 'Total',
|
||||
statusMap: {
|
||||
Total: [],
|
||||
Pend: ['Pend'],
|
||||
Coll: ['Coll', 'PartColl'],
|
||||
Recv: ['Recv'],
|
||||
Recv: ['Recv', 'PartRecv'],
|
||||
Inc: ['Inc'],
|
||||
Fin: ['Fin'],
|
||||
},
|
||||
|
||||
// Sorting & Pagination
|
||||
sortCol: 'REQDATE',
|
||||
sortAsc: false,
|
||||
currentPage: 1,
|
||||
pageSize: 15,
|
||||
|
||||
sort(col) {
|
||||
if (this.sortCol === col) {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
} else {
|
||||
this.sortCol = col;
|
||||
this.sortAsc = true;
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) this.currentPage++;
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) this.currentPage--;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.filtered.length / this.pageSize) || 1;
|
||||
},
|
||||
|
||||
get sorted() {
|
||||
return this.filtered.slice().sort((a, b) => {
|
||||
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.$watch('filterTable', () => {
|
||||
this.currentPage = 1;
|
||||
});
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
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();
|
||||
// reset counters before processing
|
||||
for (let k in this.counters) { this.counters[k] = 0; }
|
||||
fetch(`${BASEURL}/api/requests?${param}`, {
|
||||
method: 'GET',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).then(res => res.json()).then(data => {
|
||||
this.list = data.data ?? [];
|
||||
this.filterKey = 'Total';
|
||||
// count + sort in a single loop
|
||||
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++;
|
||||
else {
|
||||
if (item.STATS == 'PartColl') { this.counters.Coll++; }
|
||||
else if (item.STATS == 'PartRecv') { this.counters.Recv++; }
|
||||
this.counters.Total++;
|
||||
}
|
||||
});
|
||||
this.list.sort((a, b) => {
|
||||
@ -236,10 +403,14 @@
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
isValidated (item) {
|
||||
isValidated(item) {
|
||||
return item.ISVAL == 1 && item.ISPENDING != 1;
|
||||
},
|
||||
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));
|
||||
@ -262,152 +433,139 @@
|
||||
get validatedCount() {
|
||||
return this.list.filter(r => this.isValidated(r)).length;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
sample dialog
|
||||
*/
|
||||
item : '',
|
||||
isDialogSampleOpen : false,
|
||||
item: '',
|
||||
isDialogSampleOpen: false,
|
||||
isSampleLoading: false,
|
||||
|
||||
openSampleDialog (accessnumber) {
|
||||
openSampleDialog(accessnumber) {
|
||||
this.isDialogSampleOpen = true;
|
||||
this.fetchItem(accessnumber)
|
||||
},
|
||||
|
||||
closeSampleDialog () {
|
||||
|
||||
closeSampleDialog() {
|
||||
this.isDialogSampleOpen = false;
|
||||
},
|
||||
|
||||
fetchItem(accessnumber){
|
||||
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 => {
|
||||
this.item = data.data ?? {};
|
||||
if (!Array.isArray(this.item.samples)) this.item.samples = [];
|
||||
}).finally(() => {
|
||||
this.isSampleLoading = false;
|
||||
});
|
||||
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
|
||||
.then(res => res.json()).then(data => {
|
||||
this.item = data.data ?? {};
|
||||
if (!Array.isArray(this.item.samples)) this.item.samples = [];
|
||||
}).finally(() => {
|
||||
this.isSampleLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
collect(sampcode, accessnumber) {
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
uncollect(sampcode, accessnumber) {
|
||||
if(!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'DELETE', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
|
||||
if (!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unreceive(sampcode, accessnumber) {
|
||||
if(!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid : '<?= session('userid'); ?>'})
|
||||
if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
validate dialog
|
||||
preview dialog
|
||||
*/
|
||||
isDialogValOpen : false,
|
||||
isValidateEnabled: false,
|
||||
valAccessnumber : null,
|
||||
openValDialog (accessnumber) {
|
||||
this.isDialogValOpen = true;
|
||||
this.valAccessnumber = accessnumber;
|
||||
this.$nextTick(() => {
|
||||
// refs will be available after render
|
||||
const iframe = this.$root.querySelector('#result-iframe') || (this.$refs && this.$refs.resultIframe);
|
||||
const validateBtn = this.$root.querySelector('#validate-btn') || (this.$refs && this.$refs.validateBtn);
|
||||
if (!iframe || !validateBtn) return;
|
||||
|
||||
const setup = () => {
|
||||
try {
|
||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
const scrollable = doc.documentElement || doc.body;
|
||||
const checkScroll = () => {
|
||||
try {
|
||||
const atBottom = (scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight) < 2;
|
||||
this.isValidateEnabled = atBottom;
|
||||
validateBtn.disabled = !atBottom;
|
||||
} catch (e) { /* cross-origin or not ready */ }
|
||||
};
|
||||
iframe.contentWindow.removeEventListener('scroll', checkScroll);
|
||||
iframe.contentWindow.addEventListener('scroll', checkScroll);
|
||||
checkScroll();
|
||||
} catch (e) { /* ignore cross-origin */ }
|
||||
};
|
||||
|
||||
// If iframe already loaded, setup immediately; otherwise wait for load
|
||||
if (iframe.contentWindow && (iframe.contentDocument && iframe.contentDocument.readyState === 'complete')) {
|
||||
setup();
|
||||
} else {
|
||||
iframe.addEventListener('load', setup);
|
||||
}
|
||||
});
|
||||
isDialogPreviewOpen: false,
|
||||
reviewed: false,
|
||||
previewItem: null,
|
||||
openPreviewDialog(accessnumber, type, item) {
|
||||
this.previewAccessnumber = accessnumber;
|
||||
this.previewItem = item;
|
||||
this.previewType = type;
|
||||
this.isDialogPreviewOpen = true;
|
||||
this.reviewed = false;
|
||||
},
|
||||
closeValDialog () {
|
||||
this.isDialogValOpen = false;
|
||||
closePreviewDialog() {
|
||||
this.isDialogPreviewOpen = false;
|
||||
this.previewItem = null;
|
||||
},
|
||||
setPreviewType(type) {
|
||||
this.previewType = type;
|
||||
},
|
||||
getPreviewUrl() {
|
||||
let base = 'http://glenlis/spooler_db/main_dev.php';
|
||||
let url = `${base}?acc=${this.previewAccessnumber}`;
|
||||
if (this.previewType === 'ind') url += '&lang=ID';
|
||||
if (this.previewType === 'eng') url += '&lang=EN';
|
||||
if (this.previewType === 'pdf') url += '&output=pdf';
|
||||
|
||||
// Keep fallback for local dev if needed, but the above is the expected logic
|
||||
// return "http://localhost/application.html";
|
||||
return url;
|
||||
},
|
||||
validate(accessnumber, userid) {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ userid:`${userid}` })
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userid: `${userid}` })
|
||||
}).then(response => {
|
||||
this.closeValDialog();
|
||||
this.closePreviewDialog();
|
||||
this.fetchList();
|
||||
console.log('Validate clicked for', this.valAccessnumber, 'by user', userid);
|
||||
console.log('Validate clicked for', this.previewAccessnumber, 'by user', userid);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
unvalidate dialog
|
||||
*/
|
||||
isDialogUnvalOpen : false,
|
||||
unvalReason : '',
|
||||
unvalAccessnumber : null,
|
||||
openUnvalDialog (accessnumber) {
|
||||
isDialogUnvalOpen: false,
|
||||
unvalReason: '',
|
||||
unvalAccessnumber: null,
|
||||
openUnvalDialog(accessnumber) {
|
||||
this.unvalReason = '';
|
||||
this.isDialogUnvalOpen = true;
|
||||
this.unvalAccessnumber = accessnumber;
|
||||
},
|
||||
unvalidate(accessnumber, userid) {
|
||||
if(!confirm(`Unvalidate request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
if (!confirm(`Unvalidate request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "DELETE",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ userid:`${userid}`, comment: this.unvalReason.trim() })
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userid: `${userid}`, comment: this.unvalReason.trim() })
|
||||
}).then(response => {
|
||||
this.closeUnvalDialog();
|
||||
this.fetchList();
|
||||
console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`);
|
||||
});
|
||||
},
|
||||
closeUnvalDialog () {
|
||||
closeUnvalDialog() {
|
||||
this.isDialogUnvalOpen = false;
|
||||
},
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
</script>
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="corporate">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@ -14,49 +15,82 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.2rem 1rem;
|
||||
min-height: 0rem;
|
||||
}
|
||||
.card-body {
|
||||
|
||||
.card-body {
|
||||
font-size: 0.71rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-base-200 min-h-screen flex flex-col">
|
||||
|
||||
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
||||
<div class='flex-1'>
|
||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'>
|
||||
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Customer Service Dashboard</span>
|
||||
</a>
|
||||
</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"><?= session()->get('userrole') ?></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>
|
||||
<body class="bg-base-200 min-h-screen" x-data="main">
|
||||
<div class="drawer">
|
||||
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
||||
<div class="flex-none">
|
||||
<label for="main-drawer" aria-label="open sidebar" class="btn btn-square btn-ghost">
|
||||
<i class="fa fa-bars"></i>
|
||||
</label>
|
||||
</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">
|
||||
<li><a class="active:bg-primary" href="<?=base_url('lab') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
|
||||
<li><a class="active:bg-primary" href="<?=base_url('setPassword') ?>"><i class="fa fa-key mr-2"></i> Set Password</a></li>
|
||||
<li class="divider my-1"></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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='flex-1'>
|
||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2 ml-2'>
|
||||
<i class="fa fa-cube"></i> CMOD <span
|
||||
class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Customer Service Dashboard</span>
|
||||
</a>
|
||||
</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"><?= session()->get('userrole') ?></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('cs/dialog_setPassword'); ?>
|
||||
|
||||
<footer class='bg-base-100 p-1 mt-auto'>© <?= date('Y'); ?> - 5Panda</footer>
|
||||
</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('cs') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</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>
|
||||
|
||||
<footer class='bg-base-100 p-1'>© <?=date('Y');?> - 5Panda</footer>
|
||||
|
||||
<script>
|
||||
window.BASEURL = "<?=base_url("lab");?>";
|
||||
window.BASEURL = "<?= base_url("cs"); ?>";
|
||||
</script>
|
||||
<?=$this->renderSection('script');?>
|
||||
<?= $this->renderSection('script'); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
54
app/Views/lab/dialog_preview.php
Normal file
54
app/Views/lab/dialog_preview.php
Normal file
@ -0,0 +1,54 @@
|
||||
<dialog class="modal" :open="isDialogPreviewOpen">
|
||||
<template x-if="previewAccessnumber">
|
||||
<div class="modal-box w-11/12 max-w-7xl h-[90vh] flex flex-col p-0 overflow-hidden bg-base-100">
|
||||
<!-- Header -->
|
||||
<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">
|
||||
<i class="fa fa-eye text-primary"></i>
|
||||
Preview
|
||||
<span class="badge badge-ghost text-xs" x-text="previewAccessnumber"></span>
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="join shadow-sm" x-show="previewItem && previewItem.VAL1USER && previewItem.VAL2USER">
|
||||
<button @click="setPreviewType('preview')"
|
||||
:class="previewType === 'preview' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
|
||||
class="btn btn-sm join-item">Default</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>
|
||||
<button class="btn btn-sm btn-circle btn-ghost" @click="closePreviewDialog()">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-3 bg-base-200 border-t border-base-300 flex justify-end items-center gap-4">
|
||||
<label class="label cursor-pointer gap-2 mb-0">
|
||||
<input type="checkbox" x-model="reviewed" class="checkbox checkbox-sm checkbox-primary" />
|
||||
<span class="label-text text-sm">I have reviewed the results</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-ghost" @click="closePreviewDialog()">Cancel</button>
|
||||
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
|
||||
@click="validate(previewAccessnumber, '<?= session('userid'); ?>')" :disabled="!reviewed">
|
||||
<i class="fa fa-check mr-1"></i> Validate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dialog>
|
||||
28
app/Views/lab/dialog_setPassword.php
Normal file
28
app/Views/lab/dialog_setPassword.php
Normal file
@ -0,0 +1,28 @@
|
||||
<dialog class="modal" :open="isDialogSetPasswordOpen">
|
||||
<div class="modal-box w-96">
|
||||
<h3 class="font-bold text-lg mb-4">Change Password</h3>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">New Password</span>
|
||||
</label>
|
||||
<input type="password" x-model="password" class="input input-bordered w-full" placeholder="Enter new password" />
|
||||
</div>
|
||||
<div class="form-control w-full mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text">Confirm Password</span>
|
||||
</label>
|
||||
<input type="password" x-model="confirm_password" class="input input-bordered w-full" placeholder="Confirm new password" />
|
||||
</div>
|
||||
<div x-show="error" class="alert alert-error mt-3 text-sm">
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" @click="closeDialogSetPassword()">Cancel</button>
|
||||
<button class="btn btn-primary" @click="savePassword('<?=session('userid'); ?>')" :disabled="isLoading">
|
||||
<span x-show="isLoading" class="loading loading-spinner loading-sm"></span>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop bg-black/30" @click="closeDialogSetPassword()"></div>
|
||||
</dialog>
|
||||
@ -74,11 +74,10 @@
|
||||
<span class="flex-1"></span>
|
||||
|
||||
<div class="form-control w-full md:w-auto">
|
||||
<div class="relative">
|
||||
<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" />
|
||||
</div>
|
||||
<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>
|
||||
@ -97,6 +96,9 @@
|
||||
<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>
|
||||
@ -106,12 +108,15 @@
|
||||
<th style='width:15%;'>
|
||||
<div class="skeleton h-4 w-32"></div>
|
||||
</th>
|
||||
<th style='width:5%;'>
|
||||
<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>
|
||||
@ -120,7 +125,7 @@
|
||||
<tbody>
|
||||
<template x-for="i in 5" :key="i">
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<td colspan="11">
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -138,23 +143,78 @@
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th style='width:7%;'>Order Datetime</th>
|
||||
<th style='width:15%;'>Patient Name</th>
|
||||
<th style='width:7%;'>No Lab</th>
|
||||
<th style='width:8%;'>Reff</th>
|
||||
<th style='width:8%;'>Doctor</th>
|
||||
<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:5%;'>Result To</th>
|
||||
<th style='width:5%;'>Validation</th>
|
||||
<th style='width:4%;'>Status</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>
|
||||
<template x-for="req in filtered" :key="req.SP_ACCESSNUMBER">
|
||||
<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"></td>
|
||||
<td x-text="req.SP_ACCESSNUMBER" class="font-bold cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
<td x-text="req.HOSTORDERNUMBER" class="font-bold cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
<td x-text="req.REFF"></td>
|
||||
<td x-text="req.DOC"></td>
|
||||
<td x-text="req.TESTS"></td>
|
||||
@ -173,17 +233,18 @@
|
||||
@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>
|
||||
<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 x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="font-bold cursor-pointer"
|
||||
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@ -191,11 +252,32 @@
|
||||
</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 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>
|
||||
|
||||
<?php echo $this->include('admin/dialog_sample'); ?>
|
||||
<?php echo $this->include('admin/dialog_val'); ?>
|
||||
<?php echo $this->include('admin/dialog_unval'); ?>
|
||||
<?php echo $this->include('lab/dialog_sample'); ?>
|
||||
<?php echo $this->include('lab/dialog_unval'); ?>
|
||||
<?php echo $this->include('lab/dialog_preview'); ?>
|
||||
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -223,19 +305,65 @@
|
||||
},
|
||||
filterTable: "",
|
||||
filterKey: 'Total',
|
||||
filterKey: 'Total',
|
||||
statusMap: {
|
||||
Total: [],
|
||||
Pend: ['Pend'],
|
||||
Coll: ['Coll', 'PartColl'],
|
||||
Recv: ['Recv'],
|
||||
Recv: ['Recv', 'PartRecv'],
|
||||
Inc: ['Inc'],
|
||||
Fin: ['Fin'],
|
||||
},
|
||||
|
||||
// Sorting & Pagination
|
||||
sortCol: 'REQDATE',
|
||||
sortAsc: false,
|
||||
currentPage: 1,
|
||||
pageSize: 15,
|
||||
|
||||
sort(col) {
|
||||
if (this.sortCol === col) {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
} else {
|
||||
this.sortCol = col;
|
||||
this.sortAsc = true;
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) this.currentPage++;
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) this.currentPage--;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.filtered.length / this.pageSize) || 1;
|
||||
},
|
||||
|
||||
get sorted() {
|
||||
return this.filtered.slice().sort((a, b) => {
|
||||
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.$watch('filterTable', () => {
|
||||
this.currentPage = 1;
|
||||
});
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
@ -244,7 +372,6 @@
|
||||
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();
|
||||
// reset counters before processing
|
||||
for (let k in this.counters) { this.counters[k] = 0; }
|
||||
fetch(`${BASEURL}/api/requests?${param}`, {
|
||||
method: 'GET',
|
||||
@ -252,7 +379,6 @@
|
||||
}).then(res => res.json()).then(data => {
|
||||
this.list = data.data ?? [];
|
||||
this.filterKey = 'Total';
|
||||
// count + sort in a single loop
|
||||
this.list.forEach(item => {
|
||||
if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; }
|
||||
else {
|
||||
@ -281,6 +407,10 @@
|
||||
return item.ISVAL == 1 && item.ISPENDING != 1;
|
||||
},
|
||||
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));
|
||||
@ -365,47 +495,35 @@
|
||||
},
|
||||
|
||||
/*
|
||||
validate dialog
|
||||
preview dialog
|
||||
*/
|
||||
isDialogValOpen: false,
|
||||
isValidateEnabled: false,
|
||||
valAccessnumber: null,
|
||||
openValDialog(accessnumber) {
|
||||
this.isDialogValOpen = true;
|
||||
this.valAccessnumber = accessnumber;
|
||||
this.$nextTick(() => {
|
||||
// refs will be available after render
|
||||
const iframe = this.$root.querySelector('#result-iframe') || (this.$refs && this.$refs.resultIframe);
|
||||
const validateBtn = this.$root.querySelector('#validate-btn') || (this.$refs && this.$refs.validateBtn);
|
||||
if (!iframe || !validateBtn) return;
|
||||
|
||||
const setup = () => {
|
||||
try {
|
||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
const scrollable = doc.documentElement || doc.body;
|
||||
const checkScroll = () => {
|
||||
try {
|
||||
const atBottom = (scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight) < 2;
|
||||
this.isValidateEnabled = atBottom;
|
||||
validateBtn.disabled = !atBottom;
|
||||
} catch (e) { /* cross-origin or not ready */ }
|
||||
};
|
||||
iframe.contentWindow.removeEventListener('scroll', checkScroll);
|
||||
iframe.contentWindow.addEventListener('scroll', checkScroll);
|
||||
checkScroll();
|
||||
} catch (e) { /* ignore cross-origin */ }
|
||||
};
|
||||
|
||||
// If iframe already loaded, setup immediately; otherwise wait for load
|
||||
if (iframe.contentWindow && (iframe.contentDocument && iframe.contentDocument.readyState === 'complete')) {
|
||||
setup();
|
||||
} else {
|
||||
iframe.addEventListener('load', setup);
|
||||
}
|
||||
});
|
||||
isDialogPreviewOpen: false,
|
||||
reviewed: false,
|
||||
previewItem: null,
|
||||
openPreviewDialog(accessnumber, type, item) {
|
||||
this.previewAccessnumber = accessnumber;
|
||||
this.previewItem = item;
|
||||
this.previewType = type;
|
||||
this.isDialogPreviewOpen = true;
|
||||
this.reviewed = false;
|
||||
},
|
||||
closeValDialog() {
|
||||
this.isDialogValOpen = false;
|
||||
closePreviewDialog() {
|
||||
this.isDialogPreviewOpen = false;
|
||||
this.previewItem = null;
|
||||
},
|
||||
setPreviewType(type) {
|
||||
this.previewType = type;
|
||||
},
|
||||
getPreviewUrl() {
|
||||
let base = 'http://glenlis/spooler_db/main_dev.php';
|
||||
let url = `${base}?acc=${this.previewAccessnumber}`;
|
||||
if (this.previewType === 'ind') url += '&lang=ID';
|
||||
if (this.previewType === 'eng') url += '&lang=EN';
|
||||
if (this.previewType === 'pdf') url += '&output=pdf';
|
||||
|
||||
// Keep fallback for local dev if needed, but the above is the expected logic
|
||||
// return "http://localhost/application.html";
|
||||
return url;
|
||||
},
|
||||
validate(accessnumber, userid) {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
@ -413,9 +531,9 @@
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userid: `${userid}` })
|
||||
}).then(response => {
|
||||
this.closeValDialog();
|
||||
this.closePreviewDialog();
|
||||
this.fetchList();
|
||||
console.log('Validate clicked for', this.valAccessnumber, 'by user', userid);
|
||||
console.log('Validate clicked for', this.previewAccessnumber, 'by user', userid);
|
||||
});
|
||||
},
|
||||
|
||||
@ -445,7 +563,6 @@
|
||||
closeUnvalDialog() {
|
||||
this.isDialogUnvalOpen = false;
|
||||
},
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-base-200 min-h-screen">
|
||||
<body class="bg-base-200 min-h-screen" x-data="main">
|
||||
<div class="drawer">
|
||||
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
@ -59,6 +59,7 @@
|
||||
|
||||
<!-- Page Content -->
|
||||
<?= $this->renderSection('content'); ?>
|
||||
<?= $this->include('lab/dialog_setPassword'); ?>
|
||||
|
||||
<footer class='bg-base-100 p-1 mt-auto'>© <?= date('Y'); ?> - 5Panda</footer>
|
||||
</div>
|
||||
@ -78,7 +79,7 @@
|
||||
|
||||
<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 @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>
|
||||
|
||||
54
app/Views/phlebo/dialog_preview.php
Normal file
54
app/Views/phlebo/dialog_preview.php
Normal file
@ -0,0 +1,54 @@
|
||||
<dialog class="modal" :open="isDialogPreviewOpen">
|
||||
<template x-if="previewAccessnumber">
|
||||
<div class="modal-box w-11/12 max-w-7xl h-[90vh] flex flex-col p-0 overflow-hidden bg-base-100">
|
||||
<!-- Header -->
|
||||
<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">
|
||||
<i class="fa fa-eye text-primary"></i>
|
||||
Preview
|
||||
<span class="badge badge-ghost text-xs" x-text="previewAccessnumber"></span>
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="join shadow-sm" x-show="previewItem && previewItem.VAL1USER && previewItem.VAL2USER">
|
||||
<button @click="setPreviewType('preview')"
|
||||
:class="previewType === 'preview' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
|
||||
class="btn btn-sm join-item">Default</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>
|
||||
<button class="btn btn-sm btn-circle btn-ghost" @click="closePreviewDialog()">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-3 bg-base-200 border-t border-base-300 flex justify-end items-center gap-4">
|
||||
<label class="label cursor-pointer gap-2 mb-0">
|
||||
<input type="checkbox" x-model="reviewed" class="checkbox checkbox-sm checkbox-primary" />
|
||||
<span class="label-text text-sm">I have reviewed the results</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-ghost" @click="closePreviewDialog()">Cancel</button>
|
||||
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
|
||||
@click="validate(previewAccessnumber, '<?= session('userid'); ?>')" :disabled="!reviewed">
|
||||
<i class="fa fa-check mr-1"></i> Validate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dialog>
|
||||
28
app/Views/phlebo/dialog_setPassword.php
Normal file
28
app/Views/phlebo/dialog_setPassword.php
Normal file
@ -0,0 +1,28 @@
|
||||
<dialog class="modal" :open="isDialogSetPasswordOpen">
|
||||
<div class="modal-box w-96">
|
||||
<h3 class="font-bold text-lg mb-4">Change Password</h3>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">New Password</span>
|
||||
</label>
|
||||
<input type="password" x-model="password" class="input input-bordered w-full" placeholder="Enter new password" />
|
||||
</div>
|
||||
<div class="form-control w-full mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text">Confirm Password</span>
|
||||
</label>
|
||||
<input type="password" x-model="confirm_password" class="input input-bordered w-full" placeholder="Confirm new password" />
|
||||
</div>
|
||||
<div x-show="error" class="alert alert-error mt-3 text-sm">
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" @click="closeDialogSetPassword()">Cancel</button>
|
||||
<button class="btn btn-primary" @click="savePassword('<?=session('userid'); ?>')" :disabled="isLoading">
|
||||
<span x-show="isLoading" class="loading loading-spinner loading-sm"></span>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop bg-black/30" @click="closeDialogSetPassword()"></div>
|
||||
</dialog>
|
||||
@ -1,176 +1,299 @@
|
||||
<?= $this->extend('phlebo/main'); ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<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-body p-0 h-full flex flex-col">
|
||||
|
||||
<!-- Header & Filters -->
|
||||
<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-1">
|
||||
<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
|
||||
</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>
|
||||
<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-body p-0 h-full flex flex-col">
|
||||
|
||||
<!-- Header & Filters -->
|
||||
<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-1">
|
||||
<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
|
||||
</h2>
|
||||
</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 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>
|
||||
|
||||
<div class="form-control w-full md:w-auto">
|
||||
<div class="relative">
|
||||
<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" />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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 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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<td colspan="11">
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="i in 5" :key="i">
|
||||
<tr>
|
||||
<td colspan="9"><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>
|
||||
</template>
|
||||
<template x-if="!isLoading && list.length">
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th style='width:7%;'>Order Datetime</th>
|
||||
<th style='width:15%;'>Patient Name</th>
|
||||
<th style='width:7%;'>No Lab</th>
|
||||
<th style='width:8%;'>Reff</th>
|
||||
<th style='width:8%;'>Doctor</th>
|
||||
<th style='width:15%;'>Tests</th>
|
||||
<th style='width:5%;'>Result To</th>
|
||||
<th style='width:5%;'>Validation</th>
|
||||
<th style='width:4%;'>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="req in filtered" :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"></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'>
|
||||
</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>
|
||||
</template>
|
||||
<template x-if="!isLoading && list.length">
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<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 cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
<td x-text="req.HOSTORDERNUMBER" class="font-bold cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></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>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>
|
||||
</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>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<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>
|
||||
</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 x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="font-bold cursor-pointer"
|
||||
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</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 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>
|
||||
|
||||
<?php echo $this->include('admin/dialog_sample'); ?>
|
||||
<?php echo $this->include('admin/dialog_val'); ?>
|
||||
<?php echo $this->include('admin/dialog_unval'); ?>
|
||||
|
||||
</main>
|
||||
|
||||
<?php echo $this->include('phlebo/dialog_sample'); ?>
|
||||
<?php echo $this->include('phlebo/dialog_unval'); ?>
|
||||
<?php echo $this->include('phlebo/dialog_preview'); ?>
|
||||
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section('script') ?>
|
||||
<script type="module">
|
||||
import Alpine from '<?=base_url("js/app.js");?>';
|
||||
<script type="module">
|
||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("dashboard", ()=> ({
|
||||
Alpine.data("dashboard", () => ({
|
||||
// dashboard
|
||||
today: "",
|
||||
filter: { date1: "", date2: "" },
|
||||
list: [],
|
||||
isLoading: false,
|
||||
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
|
||||
filter: { date1: "", date2: "" },
|
||||
list: [],
|
||||
isLoading: false,
|
||||
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
|
||||
statusColor: {
|
||||
Pend: 'bg-white text-black font-bold',
|
||||
PartColl: 'bg-orange-300 text-white font-bold',
|
||||
@ -180,44 +303,88 @@
|
||||
Inc: 'bg-yellow-500 text-white font-bold',
|
||||
Fin: 'bg-green-500 text-white font-bold',
|
||||
},
|
||||
filterTable :"",
|
||||
filterTable: "",
|
||||
filterKey: 'Total',
|
||||
filterKey: 'Total',
|
||||
statusMap: {
|
||||
Total: [],
|
||||
Pend: ['Pend'],
|
||||
Coll: ['Coll', 'PartColl'],
|
||||
Recv: ['Recv'],
|
||||
Recv: ['Recv', 'PartRecv'],
|
||||
Inc: ['Inc'],
|
||||
Fin: ['Fin'],
|
||||
},
|
||||
|
||||
// Sorting & Pagination
|
||||
sortCol: 'REQDATE',
|
||||
sortAsc: false,
|
||||
currentPage: 1,
|
||||
pageSize: 15,
|
||||
|
||||
sort(col) {
|
||||
if (this.sortCol === col) {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
} else {
|
||||
this.sortCol = col;
|
||||
this.sortAsc = true;
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) this.currentPage++;
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) this.currentPage--;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.filtered.length / this.pageSize) || 1;
|
||||
},
|
||||
|
||||
get sorted() {
|
||||
return this.filtered.slice().sort((a, b) => {
|
||||
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.$watch('filterTable', () => {
|
||||
this.currentPage = 1;
|
||||
});
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
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();
|
||||
// reset counters before processing
|
||||
for (let k in this.counters) { this.counters[k] = 0; }
|
||||
fetch(`${BASEURL}/api/requests?${param}`, {
|
||||
method: 'GET',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).then(res => res.json()).then(data => {
|
||||
this.list = data.data ?? [];
|
||||
this.filterKey = 'Total';
|
||||
// count + sort in a single loop
|
||||
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++;
|
||||
else {
|
||||
if (item.STATS == 'PartColl') { this.counters.Coll++; }
|
||||
else if (item.STATS == 'PartRecv') { this.counters.Recv++; }
|
||||
this.counters.Total++;
|
||||
}
|
||||
});
|
||||
this.list.sort((a, b) => {
|
||||
@ -236,10 +403,14 @@
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
isValidated (item) {
|
||||
isValidated(item) {
|
||||
return item.ISVAL == 1 && item.ISPENDING != 1;
|
||||
},
|
||||
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));
|
||||
@ -262,152 +433,139 @@
|
||||
get validatedCount() {
|
||||
return this.list.filter(r => this.isValidated(r)).length;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
sample dialog
|
||||
*/
|
||||
item : '',
|
||||
isDialogSampleOpen : false,
|
||||
item: '',
|
||||
isDialogSampleOpen: false,
|
||||
isSampleLoading: false,
|
||||
|
||||
openSampleDialog (accessnumber) {
|
||||
openSampleDialog(accessnumber) {
|
||||
this.isDialogSampleOpen = true;
|
||||
this.fetchItem(accessnumber)
|
||||
},
|
||||
|
||||
closeSampleDialog () {
|
||||
|
||||
closeSampleDialog() {
|
||||
this.isDialogSampleOpen = false;
|
||||
},
|
||||
|
||||
fetchItem(accessnumber){
|
||||
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 => {
|
||||
this.item = data.data ?? {};
|
||||
if (!Array.isArray(this.item.samples)) this.item.samples = [];
|
||||
}).finally(() => {
|
||||
this.isSampleLoading = false;
|
||||
});
|
||||
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
|
||||
.then(res => res.json()).then(data => {
|
||||
this.item = data.data ?? {};
|
||||
if (!Array.isArray(this.item.samples)) this.item.samples = [];
|
||||
}).finally(() => {
|
||||
this.isSampleLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
collect(sampcode, accessnumber) {
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
uncollect(sampcode, accessnumber) {
|
||||
if(!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'DELETE', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
|
||||
if (!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unreceive(sampcode, accessnumber) {
|
||||
if(!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid : '<?= session('userid'); ?>'})
|
||||
if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
validate dialog
|
||||
preview dialog
|
||||
*/
|
||||
isDialogValOpen : false,
|
||||
isValidateEnabled: false,
|
||||
valAccessnumber : null,
|
||||
openValDialog (accessnumber) {
|
||||
this.isDialogValOpen = true;
|
||||
this.valAccessnumber = accessnumber;
|
||||
this.$nextTick(() => {
|
||||
// refs will be available after render
|
||||
const iframe = this.$root.querySelector('#result-iframe') || (this.$refs && this.$refs.resultIframe);
|
||||
const validateBtn = this.$root.querySelector('#validate-btn') || (this.$refs && this.$refs.validateBtn);
|
||||
if (!iframe || !validateBtn) return;
|
||||
|
||||
const setup = () => {
|
||||
try {
|
||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
const scrollable = doc.documentElement || doc.body;
|
||||
const checkScroll = () => {
|
||||
try {
|
||||
const atBottom = (scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight) < 2;
|
||||
this.isValidateEnabled = atBottom;
|
||||
validateBtn.disabled = !atBottom;
|
||||
} catch (e) { /* cross-origin or not ready */ }
|
||||
};
|
||||
iframe.contentWindow.removeEventListener('scroll', checkScroll);
|
||||
iframe.contentWindow.addEventListener('scroll', checkScroll);
|
||||
checkScroll();
|
||||
} catch (e) { /* ignore cross-origin */ }
|
||||
};
|
||||
|
||||
// If iframe already loaded, setup immediately; otherwise wait for load
|
||||
if (iframe.contentWindow && (iframe.contentDocument && iframe.contentDocument.readyState === 'complete')) {
|
||||
setup();
|
||||
} else {
|
||||
iframe.addEventListener('load', setup);
|
||||
}
|
||||
});
|
||||
isDialogPreviewOpen: false,
|
||||
reviewed: false,
|
||||
previewItem: null,
|
||||
openPreviewDialog(accessnumber, type, item) {
|
||||
this.previewAccessnumber = accessnumber;
|
||||
this.previewItem = item;
|
||||
this.previewType = type;
|
||||
this.isDialogPreviewOpen = true;
|
||||
this.reviewed = false;
|
||||
},
|
||||
closeValDialog () {
|
||||
this.isDialogValOpen = false;
|
||||
closePreviewDialog() {
|
||||
this.isDialogPreviewOpen = false;
|
||||
this.previewItem = null;
|
||||
},
|
||||
setPreviewType(type) {
|
||||
this.previewType = type;
|
||||
},
|
||||
getPreviewUrl() {
|
||||
let base = 'http://glenlis/spooler_db/main_dev.php';
|
||||
let url = `${base}?acc=${this.previewAccessnumber}`;
|
||||
if (this.previewType === 'ind') url += '&lang=ID';
|
||||
if (this.previewType === 'eng') url += '&lang=EN';
|
||||
if (this.previewType === 'pdf') url += '&output=pdf';
|
||||
|
||||
// Keep fallback for local dev if needed, but the above is the expected logic
|
||||
// return "http://localhost/application.html";
|
||||
return url;
|
||||
},
|
||||
validate(accessnumber, userid) {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ userid:`${userid}` })
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userid: `${userid}` })
|
||||
}).then(response => {
|
||||
this.closeValDialog();
|
||||
this.closePreviewDialog();
|
||||
this.fetchList();
|
||||
console.log('Validate clicked for', this.valAccessnumber, 'by user', userid);
|
||||
console.log('Validate clicked for', this.previewAccessnumber, 'by user', userid);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
unvalidate dialog
|
||||
*/
|
||||
isDialogUnvalOpen : false,
|
||||
unvalReason : '',
|
||||
unvalAccessnumber : null,
|
||||
openUnvalDialog (accessnumber) {
|
||||
isDialogUnvalOpen: false,
|
||||
unvalReason: '',
|
||||
unvalAccessnumber: null,
|
||||
openUnvalDialog(accessnumber) {
|
||||
this.unvalReason = '';
|
||||
this.isDialogUnvalOpen = true;
|
||||
this.unvalAccessnumber = accessnumber;
|
||||
},
|
||||
unvalidate(accessnumber, userid) {
|
||||
if(!confirm(`Unvalidate request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
if (!confirm(`Unvalidate request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "DELETE",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ userid:`${userid}`, comment: this.unvalReason.trim() })
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userid: `${userid}`, comment: this.unvalReason.trim() })
|
||||
}).then(response => {
|
||||
this.closeUnvalDialog();
|
||||
this.fetchList();
|
||||
console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`);
|
||||
});
|
||||
},
|
||||
closeUnvalDialog () {
|
||||
closeUnvalDialog() {
|
||||
this.isDialogUnvalOpen = false;
|
||||
},
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
</script>
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="corporate">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@ -14,49 +15,82 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.2rem 1rem;
|
||||
min-height: 0rem;
|
||||
}
|
||||
.card-body {
|
||||
|
||||
.card-body {
|
||||
font-size: 0.71rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-base-200 min-h-screen flex flex-col">
|
||||
|
||||
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
||||
<div class='flex-1'>
|
||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'>
|
||||
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Phlebotomist Dashboard</span>
|
||||
</a>
|
||||
</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"><?= session()->get('userrole') ?></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>
|
||||
<body class="bg-base-200 min-h-screen" x-data="main">
|
||||
<div class="drawer">
|
||||
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
||||
<div class="flex-none">
|
||||
<label for="main-drawer" aria-label="open sidebar" class="btn btn-square btn-ghost">
|
||||
<i class="fa fa-bars"></i>
|
||||
</label>
|
||||
</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">
|
||||
<li><a class="active:bg-primary" href="<?=base_url('lab') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
|
||||
<li><a class="active:bg-primary" href="<?=base_url('setPassword') ?>"><i class="fa fa-key mr-2"></i> Set Password</a></li>
|
||||
<li class="divider my-1"></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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='flex-1'>
|
||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2 ml-2'>
|
||||
<i class="fa fa-cube"></i> CMOD <span
|
||||
class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Phlebotomist Dashboard</span>
|
||||
</a>
|
||||
</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"><?= session()->get('userrole') ?></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('phlebo/dialog_setPassword'); ?>
|
||||
|
||||
<footer class='bg-base-100 p-1 mt-auto'>© <?= date('Y'); ?> - 5Panda</footer>
|
||||
</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('phlebo') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</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>
|
||||
|
||||
<footer class='bg-base-100 p-1'>© <?=date('Y');?> - 5Panda</footer>
|
||||
|
||||
<script>
|
||||
window.BASEURL = "<?=base_url("lab");?>";
|
||||
window.BASEURL = "<?= base_url("phlebo"); ?>";
|
||||
</script>
|
||||
<?=$this->renderSection('script');?>
|
||||
<?= $this->renderSection('script'); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,401 +1,571 @@
|
||||
<?= $this->extend('superuser/main'); ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<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-body p-0 h-full flex flex-col">
|
||||
|
||||
<!-- Header & Filters -->
|
||||
<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-1">
|
||||
<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
|
||||
</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>
|
||||
<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-body p-0 h-full flex flex-col">
|
||||
|
||||
<!-- Header & Filters -->
|
||||
<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-1">
|
||||
<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
|
||||
</h2>
|
||||
</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 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>
|
||||
|
||||
<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>
|
||||
<!-- 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 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>
|
||||
<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>
|
||||
<!-- 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>
|
||||
</template>
|
||||
<template x-if="!isLoading && list.length">
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<th style='width:7%;'>Order Datetime</th>
|
||||
<th style='width:15%;'>Patient Name</th>
|
||||
<th style='width:7%;'>No Lab</th>
|
||||
<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>
|
||||
<td colspan="11">
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="req in filtered" :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"></td>
|
||||
<td x-text="req.HOSTORDERNUMBER"></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'>
|
||||
</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>
|
||||
</template>
|
||||
<template x-if="!isLoading && list.length">
|
||||
<table class="table table-xs table-zebra w-full">
|
||||
<thead class="bg-base-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<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 cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
<td x-text="req.HOSTORDERNUMBER" class="font-bold cursor-pointer" :class="statusColor[req.STATS]"
|
||||
@click="openSampleDialog(req.SP_ACCESSNUMBER)"></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>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>
|
||||
</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')">Preview</button>
|
||||
<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>
|
||||
</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>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
</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 x-text="req.STATS === 'Fin' ? 'Final' : req.STATS" class="font-bold cursor-pointer"
|
||||
:class="statusColor[req.STATS]" @click="openSampleDialog(req.SP_ACCESSNUMBER)"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</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 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>
|
||||
|
||||
|
||||
<?php echo $this->include('superuser/dialog_sample'); ?>
|
||||
<?php echo $this->include('superuser/dialog_unval'); ?>
|
||||
<?php echo $this->include('superuser/dialog_preview'); ?>
|
||||
|
||||
</main>
|
||||
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section('script') ?>
|
||||
<script type="module">
|
||||
import Alpine from '<?=base_url("js/app.js");?>';
|
||||
<script type="module">
|
||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("dashboard", ()=> ({
|
||||
// dashboard
|
||||
today: "",
|
||||
filter: { date1: "", date2: "" },
|
||||
list: [],
|
||||
isLoading: false,
|
||||
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
|
||||
statusColor: {
|
||||
Pend: 'bg-white text-black font-bold',
|
||||
PartColl: 'bg-orange-300 text-white font-bold',
|
||||
Coll: 'bg-orange-500 text-white font-bold',
|
||||
PartRecv: 'bg-blue-200 text-black font-bold',
|
||||
Recv: 'bg-blue-500 text-white font-bold',
|
||||
Inc: 'bg-yellow-500 text-white font-bold',
|
||||
Fin: 'bg-green-500 text-white font-bold',
|
||||
},
|
||||
filterTable :"",
|
||||
filterKey: 'Total',
|
||||
statusMap: {
|
||||
Total: [],
|
||||
Pend: ['Pend'],
|
||||
Coll: ['Coll', 'PartColl'],
|
||||
Recv: ['Recv', 'PartRecv'],
|
||||
Inc: ['Inc'],
|
||||
Fin: ['Fin'],
|
||||
},
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("dashboard", () => ({
|
||||
// dashboard
|
||||
today: "",
|
||||
filter: { date1: "", date2: "" },
|
||||
list: [],
|
||||
isLoading: false,
|
||||
counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 },
|
||||
statusColor: {
|
||||
Pend: 'bg-white text-black font-bold',
|
||||
PartColl: 'bg-orange-300 text-white font-bold',
|
||||
Coll: 'bg-orange-500 text-white font-bold',
|
||||
PartRecv: 'bg-blue-200 text-black font-bold',
|
||||
Recv: 'bg-blue-500 text-white font-bold',
|
||||
Inc: 'bg-yellow-500 text-white font-bold',
|
||||
Fin: 'bg-green-500 text-white font-bold',
|
||||
},
|
||||
filterTable: "",
|
||||
filterKey: 'Total',
|
||||
filterKey: 'Total',
|
||||
statusMap: {
|
||||
Total: [],
|
||||
Pend: ['Pend'],
|
||||
Coll: ['Coll', 'PartColl'],
|
||||
Recv: ['Recv', 'PartRecv'],
|
||||
Inc: ['Inc'],
|
||||
Fin: ['Fin'],
|
||||
},
|
||||
|
||||
init() {
|
||||
this.today = new Date().toISOString().slice(0, 10);
|
||||
this.filter.date1 = this.today;
|
||||
this.filter.date2 = this.today;
|
||||
this.fetchList();
|
||||
},
|
||||
// Sorting & Pagination
|
||||
sortCol: 'REQDATE',
|
||||
sortAsc: false,
|
||||
currentPage: 1,
|
||||
pageSize: 15,
|
||||
|
||||
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++;
|
||||
}
|
||||
});
|
||||
this.list.sort((a, b) => {
|
||||
let codeA = statusOrder[a.STATS] ?? 0;
|
||||
let codeB = statusOrder[b.STATS] ?? 0;
|
||||
return codeA - codeB;
|
||||
});
|
||||
}).finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
sort(col) {
|
||||
if (this.sortCol === col) {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
} else {
|
||||
this.sortCol = col;
|
||||
this.sortAsc = true;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.filter.date1 = this.today;
|
||||
this.filter.date2 = this.today;
|
||||
this.fetchList();
|
||||
},
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) this.currentPage++;
|
||||
},
|
||||
|
||||
isValidated (item) {
|
||||
return item.ISVAL == 1 && item.ISPENDING != 1;
|
||||
},
|
||||
get filtered() {
|
||||
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));
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) this.currentPage--;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.filtered.length / this.pageSize) || 1;
|
||||
},
|
||||
|
||||
get sorted() {
|
||||
return this.filtered.slice().sort((a, b) => {
|
||||
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.$watch('filterTable', () => {
|
||||
this.currentPage = 1;
|
||||
});
|
||||
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) {
|
||||
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,
|
||||
});
|
||||
this.list.sort((a, b) => {
|
||||
let codeA = statusOrder[a.STATS] ?? 0;
|
||||
let codeB = statusOrder[b.STATS] ?? 0;
|
||||
return codeA - codeB;
|
||||
});
|
||||
}).finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
openSampleDialog (accessnumber) {
|
||||
this.isDialogSampleOpen = true;
|
||||
this.fetchItem(accessnumber)
|
||||
},
|
||||
|
||||
closeSampleDialog () {
|
||||
this.isDialogSampleOpen = false;
|
||||
},
|
||||
reset() {
|
||||
this.filter.date1 = this.today;
|
||||
this.filter.date2 = this.today;
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
fetchItem(accessnumber){
|
||||
this.isSampleLoading = true;
|
||||
this.item = [];
|
||||
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: {'Content-Type': 'application/json'}})
|
||||
isValidated(item) {
|
||||
return item.ISVAL == 1 && item.ISPENDING != 1;
|
||||
},
|
||||
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 => {
|
||||
this.item = data.data ?? {};
|
||||
if (!Array.isArray(this.item.samples)) this.item.samples = [];
|
||||
this.item = data.data ?? {};
|
||||
if (!Array.isArray(this.item.samples)) this.item.samples = [];
|
||||
}).finally(() => {
|
||||
this.isSampleLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
collect(sampcode, accessnumber) {
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
|
||||
})
|
||||
collect(sampcode, accessnumber) {
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
uncollect(sampcode, accessnumber) {
|
||||
if(!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'DELETE', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
|
||||
})
|
||||
uncollect(sampcode, accessnumber) {
|
||||
if (!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
||||
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
unreceive(sampcode, accessnumber) {
|
||||
if(!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({samplenumber: sampcode, userid : '<?= session('userid'); ?>'})
|
||||
})
|
||||
unreceive(sampcode, accessnumber) {
|
||||
if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
||||
})
|
||||
.then(res => res.json()).then(data => {
|
||||
this.fetchItem(accessnumber);
|
||||
this.fetchItem(accessnumber);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
preview dialog
|
||||
*/
|
||||
isDialogPreviewOpen : false,
|
||||
reviewed: false,
|
||||
previewAccessnumber : null,
|
||||
previewType : 'preview',
|
||||
openPreviewDialog (accessnumber, type) {
|
||||
this.previewAccessnumber = accessnumber;
|
||||
this.previewType = type;
|
||||
this.isDialogPreviewOpen = true;
|
||||
this.reviewed = false;
|
||||
},
|
||||
closePreviewDialog () {
|
||||
this.isDialogPreviewOpen = false;
|
||||
},
|
||||
setPreviewType(type) {
|
||||
this.previewType = type;
|
||||
},
|
||||
getPreviewUrl() {
|
||||
let base = 'http://glenlis/spooler_db/main_dev.php';
|
||||
let url = `${base}?acc=${this.previewAccessnumber}`;
|
||||
if (this.previewType === 'ind') url += '&lang=ID';
|
||||
if (this.previewType === 'eng') url += '&lang=EN';
|
||||
if (this.previewType === 'pdf') url += '&output=pdf';
|
||||
return url;
|
||||
},
|
||||
validate(accessnumber, userid) {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
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,
|
||||
openUnvalDialog (accessnumber) {
|
||||
this.unvalReason = '';
|
||||
this.isDialogUnvalOpen = true;
|
||||
this.unvalAccessnumber = accessnumber;
|
||||
},
|
||||
unvalidate(accessnumber, userid) {
|
||||
if(!confirm(`Unvalidate request ${accessnumber}?`)) { return ;}
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "DELETE",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ userid:`${userid}`, comment: this.unvalReason.trim() })
|
||||
}).then(response => {
|
||||
this.closeUnvalDialog();
|
||||
this.fetchList();
|
||||
console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`);
|
||||
});
|
||||
},
|
||||
closeUnvalDialog () {
|
||||
this.isDialogUnvalOpen = false;
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
/*
|
||||
preview dialog
|
||||
*/
|
||||
isDialogPreviewOpen: false,
|
||||
reviewed: false,
|
||||
previewItem: null,
|
||||
openPreviewDialog(accessnumber, type, item) {
|
||||
this.previewAccessnumber = accessnumber;
|
||||
this.previewItem = item;
|
||||
this.previewType = type;
|
||||
this.isDialogPreviewOpen = true;
|
||||
this.reviewed = false;
|
||||
},
|
||||
closePreviewDialog() {
|
||||
this.isDialogPreviewOpen = false;
|
||||
this.previewItem = null;
|
||||
},
|
||||
setPreviewType(type) {
|
||||
this.previewType = type;
|
||||
},
|
||||
getPreviewUrl() {
|
||||
let base = 'http://glenlis/spooler_db/main_dev.php';
|
||||
let url = `${base}?acc=${this.previewAccessnumber}`;
|
||||
if (this.previewType === 'ind') url += '&lang=ID';
|
||||
if (this.previewType === 'eng') url += '&lang=EN';
|
||||
if (this.previewType === 'pdf') url += '&output=pdf';
|
||||
|
||||
// Keep fallback for local dev if needed, but the above is the expected logic
|
||||
// return "http://localhost/application.html";
|
||||
return url;
|
||||
},
|
||||
validate(accessnumber, userid) {
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
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,
|
||||
openUnvalDialog(accessnumber) {
|
||||
this.unvalReason = '';
|
||||
this.isDialogUnvalOpen = true;
|
||||
this.unvalAccessnumber = accessnumber;
|
||||
},
|
||||
unvalidate(accessnumber, userid) {
|
||||
if (!confirm(`Unvalidate request ${accessnumber}?`)) { return; }
|
||||
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userid: `${userid}`, comment: this.unvalReason.trim() })
|
||||
}).then(response => {
|
||||
this.closeUnvalDialog();
|
||||
this.fetchList();
|
||||
console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`);
|
||||
});
|
||||
},
|
||||
closeUnvalDialog() {
|
||||
this.isDialogUnvalOpen = false;
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="corporate">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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-size: 0.71rem;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.2rem 1rem;
|
||||
min-height: 0rem;
|
||||
}
|
||||
.card-body {
|
||||
|
||||
.card-body {
|
||||
font-size: 0.71rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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">
|
||||
<div class='flex-1'>
|
||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'>
|
||||
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Superuser Dashboard</span>
|
||||
</a>
|
||||
</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"><?= session()->get('userrole') ?></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>
|
||||
<body class="bg-base-200 min-h-screen" x-data="main">
|
||||
<div class="drawer">
|
||||
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
||||
<div class="flex-none">
|
||||
<label for="main-drawer" aria-label="open sidebar" class="btn btn-square btn-ghost">
|
||||
<i class="fa fa-bars"></i>
|
||||
</label>
|
||||
</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">
|
||||
<li><a class="active:bg-primary" href="<?=base_url('superuser') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
|
||||
<li><a class="active:bg-primary" href="<?=base_url('superuser/users') ?>"><i class="fa fa-users mr-2"></i> Users </a></li>
|
||||
<li><a @click.prevent="openDialogSetPassword()" class="active:bg-primary"><i class="fa fa-key mr-2"></i> Change Password</a></li>
|
||||
<li class="divider my-1"></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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class='flex-1'>
|
||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2 ml-2'>
|
||||
<i class="fa fa-cube"></i> CMOD <span
|
||||
class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Superuser Dashboard</span>
|
||||
</a>
|
||||
</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"><?= session()->get('userrole') ?></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('superuser/dialog_setPassword'); ?>
|
||||
|
||||
<footer class='bg-base-100 p-1 mt-auto'>© <?= 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('superuser') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
|
||||
<li><a href="<?= base_url('superuser/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('superuser/dialog_setPassword');?>
|
||||
<footer class='bg-base-100 p-1'>© <?=date('Y');?> - 5Panda</footer>
|
||||
<script>
|
||||
window.BASEURL = "<?=base_url("superuser");?>";
|
||||
window.BASEURL = "<?= base_url("superuser"); ?>";
|
||||
</script>
|
||||
<?=$this->renderSection('script');?>
|
||||
<?= $this->renderSection('script'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user