Update Role User (superuser, admin, lab, phlebo, cs) dan pengelompokan controller dan views
This commit is contained in:
parent
afc2e14271
commit
ddb76d4570
@ -20,6 +20,22 @@ $routes->get('label/coll/(:any)', 'Label::coll/$1');
|
|||||||
$routes->get('label/dispatch/(:any)/(:any)', 'Label::dispatch/$1/$2');
|
$routes->get('label/dispatch/(:any)/(:any)', 'Label::dispatch/$1/$2');
|
||||||
$routes->get('label/all/(:any)', 'Label::print_all/$1');
|
$routes->get('label/all/(:any)', 'Label::print_all/$1');
|
||||||
|
|
||||||
|
$routes->group('superuser', ['filter' => 'role:0'], function($routes) {
|
||||||
|
$routes->get('', 'Superuser::index');
|
||||||
|
$routes->get('users', 'Superuser::users');
|
||||||
|
$routes->get('api/users', 'Users::index');
|
||||||
|
$routes->post('api/users', 'Users::create');
|
||||||
|
$routes->patch('api/users/(:any)', 'Users::update/$1');
|
||||||
|
$routes->delete('api/users/(:any)', 'Users::delete/$1');
|
||||||
|
$routes->get('api/requests', 'Requests::index');
|
||||||
|
$routes->post('api/requests/validate/(:any)', 'Requests::val/$1');
|
||||||
|
$routes->delete('api/requests/validate/(:any)', 'Requests::unval/$1');
|
||||||
|
$routes->post('api/samples/collect/(:any)', 'Samples::collect/$1');
|
||||||
|
$routes->delete('api/samples/collect/(:any)', 'Samples::uncollect/$1');
|
||||||
|
$routes->delete('api/samples/receive/(:any)', 'Samples::unreceive/$1');
|
||||||
|
$routes->get('api/samples/(:any)', 'Samples::show/$1');
|
||||||
|
});
|
||||||
|
|
||||||
$routes->group('admin', ['filter' => 'role:1'], function($routes) {
|
$routes->group('admin', ['filter' => 'role:1'], function($routes) {
|
||||||
$routes->get('', 'Admin::index');
|
$routes->get('', 'Admin::index');
|
||||||
$routes->get('users', 'Admin::users');
|
$routes->get('users', 'Admin::users');
|
||||||
@ -45,4 +61,22 @@ $routes->group('lab', ['filter' => 'role:2'], function($routes) {
|
|||||||
$routes->get('api/samples/(:any)', 'Samples::show/$1');
|
$routes->get('api/samples/(:any)', 'Samples::show/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$routes->group('phlebo', ['filter' => 'role:3'], function($routes) {
|
||||||
|
$routes->get('', 'Phlebotomist::index');
|
||||||
|
$routes->get('api/requests', 'Requests::index');
|
||||||
|
$routes->post('api/requests/validate/(:any)', 'Requests::val/$1');
|
||||||
|
$routes->delete('api/requests/validate/(:any)', 'Requests::unval/$1');
|
||||||
|
$routes->post('api/samples/collect/(:any)', 'Samples::collect/$1');
|
||||||
|
$routes->get('api/samples/(:any)', 'Samples::show/$1');
|
||||||
|
});
|
||||||
|
|
||||||
|
$routes->group('cs', ['filter' => 'role:4'], function($routes) {
|
||||||
|
$routes->get('', 'Cs::index');
|
||||||
|
$routes->get('api/requests', 'Requests::index');
|
||||||
|
$routes->post('api/requests/validate/(:any)', 'Requests::val/$1');
|
||||||
|
$routes->delete('api/requests/validate/(:any)', 'Requests::unval/$1');
|
||||||
|
$routes->post('api/samples/collect/(:any)', 'Samples::collect/$1');
|
||||||
|
$routes->get('api/samples/(:any)', 'Samples::show/$1');
|
||||||
|
});
|
||||||
|
|
||||||
$routes->get('/dummypage', 'Home::dummyPage');
|
$routes->get('/dummypage', 'Home::dummyPage');
|
||||||
|
|||||||
@ -14,8 +14,4 @@ class Admin extends BaseController {
|
|||||||
return view('admin/index');
|
return view('admin/index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function users() {
|
|
||||||
return view('admin/users');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,19 +25,19 @@ class Auth extends BaseController {
|
|||||||
|
|
||||||
switch ((int)$user['USERROLEID']) {
|
switch ((int)$user['USERROLEID']) {
|
||||||
case 0:
|
case 0:
|
||||||
$role = 'admin';
|
$role = 'Superuser';
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
$role = 'analyst';
|
$role = 'Admin';
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
$role = 'phlebotomist';
|
$role = 'Lab Analyst';
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
$role = 'cs';
|
$role = 'Phlebotomist';
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
$role = 'cs';
|
$role = 'Customer Service';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$role = '';
|
$role = '';
|
||||||
@ -47,17 +47,19 @@ class Auth extends BaseController {
|
|||||||
$session->set([
|
$session->set([
|
||||||
'isLoggedIn' => true,
|
'isLoggedIn' => true,
|
||||||
'userid' => (string) $user['USERID'],
|
'userid' => (string) $user['USERID'],
|
||||||
'userlevel' => (int) $user['USERLEVEL'],
|
'userroleid' => (int) $user['USERROLEID'],
|
||||||
'userrole' => (string) $role,
|
'userrole' => (string) $role,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
switch ((int)$user['USERLEVEL']) {
|
switch ((int)$user['USERROLEID']) {
|
||||||
|
case 0:
|
||||||
|
return redirect()->to('superuser');
|
||||||
case 1:
|
case 1:
|
||||||
return redirect()->to('admin');
|
return redirect()->to('admin');
|
||||||
case 2:
|
case 2:
|
||||||
return redirect()->to('lab');
|
return redirect()->to('lab');
|
||||||
case 3:
|
case 3:
|
||||||
return redirect()->to('analyst');
|
return redirect()->to('phlebo');
|
||||||
case 4:
|
case 4:
|
||||||
return redirect()->to('cs');
|
return redirect()->to('cs');
|
||||||
default:
|
default:
|
||||||
|
|||||||
17
app/Controllers/Cs.php
Normal file
17
app/Controllers/Cs.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
|
class Lab extends BaseController {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
helper(['url', 'form', 'text']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index() {
|
||||||
|
return view('cs/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
app/Controllers/Phlebotomist.php
Normal file
17
app/Controllers/Phlebotomist.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
|
class Phlebotomist extends BaseController {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
helper(['url', 'form', 'text']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index() {
|
||||||
|
return view('phlebo/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
app/Controllers/Superuser.php
Normal file
21
app/Controllers/Superuser.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
|
class Superuser extends BaseController {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
helper(['url', 'form', 'text']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index() {
|
||||||
|
return view('superuser/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function users() {
|
||||||
|
return view('superuser/users');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -14,10 +14,11 @@ class GuestFilter implements FilterInterface
|
|||||||
|
|
||||||
// Kalau sudah login, arahkan ke dashboard sesuai level
|
// Kalau sudah login, arahkan ke dashboard sesuai level
|
||||||
if ($session->get('isLoggedIn')) {
|
if ($session->get('isLoggedIn')) {
|
||||||
switch ($session->get('userlevel')) {
|
switch ($session->get('userroleid')) {
|
||||||
|
case 0: return redirect()->to('/superuser');
|
||||||
case 1: return redirect()->to('/admin');
|
case 1: return redirect()->to('/admin');
|
||||||
case 2: return redirect()->to('/doctor');
|
case 2: return redirect()->to('/lab');
|
||||||
case 3: return redirect()->to('/analyst');
|
case 3: return redirect()->to('/phlebo');
|
||||||
case 4: return redirect()->to('/cs');
|
case 4: return redirect()->to('/cs');
|
||||||
default: return redirect()->to('/login');
|
default: return redirect()->to('/login');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,11 +20,11 @@ class RoleFilter implements FilterInterface
|
|||||||
// Kalau filter menerima argumen dari routes misal (role:1,2,3), lalu periksa denagn userlevel
|
// Kalau filter menerima argumen dari routes misal (role:1,2,3), lalu periksa denagn userlevel
|
||||||
if ($arguments) {
|
if ($arguments) {
|
||||||
|
|
||||||
$allowedLevels = array_map('intval', $arguments); // ubah arguments dalam bentuk array string ke array integer
|
$allowedRoles = array_map('intval', $arguments); // ubah arguments dalam bentuk array string ke array integer
|
||||||
$userLevel = (int) $session->get('userlevel');
|
$userRoleId = (int) $session->get('userroleid');
|
||||||
|
|
||||||
// Bandingkan dengan userlevel
|
// Bandingkan dengan userlevel
|
||||||
if (! in_array($userLevel, $allowedLevels)) {
|
if (! in_array($userRoleId, $allowedRoles)) {
|
||||||
return redirect()->to('/unauthorized'); // misalnya ke halaman error
|
return redirect()->to('/unauthorized'); // misalnya ke halaman error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="text-right hidden sm:block leading-tight">
|
<div class="text-right hidden sm:block leading-tight">
|
||||||
<div class="text-sm font-bold opacity-70">Hi, <?=session('userid'); ?></div>
|
<div class="text-sm font-bold opacity-70">Hi, <?=session('userid'); ?></div>
|
||||||
<div class="text-xs opacity-50">Administrator</div>
|
<div class="text-xs opacity-50"><?= session()->get('userrole') ?></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
|
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
|
||||||
|
|||||||
88
app/Views/cs/dialog_sample.php
Normal file
88
app/Views/cs/dialog_sample.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<dialog class="modal" :open="isDialogSampleOpen">
|
||||||
|
<div class="modal-box w-2/3 max-w-5xl">
|
||||||
|
<p class='text-right'><button class="btn btn-xs btn-neutral" @click="closeSampleDialog()">X</button></p>
|
||||||
|
|
||||||
|
<table class="table table-xs table-compact w-full mb-4">
|
||||||
|
<tr>
|
||||||
|
<td>MR# </td> <td x-text="': '+item.patnumber"></td>
|
||||||
|
<td>Patient Name </td> <td x-text="': '+item.patname"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>KTP# </td> <td x-text="': '+item.ktp"></td>
|
||||||
|
<td>Sex / Age </td> <td x-text="': '+item.placeofbirth+' '+item.gender+' / '+item.age"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Note</td>
|
||||||
|
<td colspan='3'>
|
||||||
|
<textarea x-text="item.comment" class="textarea textarea-bordered w-full"></textarea>
|
||||||
|
<button class="btn btn-sm btn-primary mt-2" @click="saveComment(item.accessnumber)">Save</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="table table-xs table-compact w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sample Code</th>
|
||||||
|
<th>Sample Name</th>
|
||||||
|
<th class='text-center'>Collected</th>
|
||||||
|
<th class='text-center'>Received</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td></td> <td>Collection</td> <td></td> <td></td>
|
||||||
|
<td><button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td> <td>All</td> <td></td> <td></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></i></button>
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1" onclick=""><h6 class="p-0 m-0">Coll.</h6></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<template x-for="sample in item.samples">
|
||||||
|
<tr>
|
||||||
|
<td x-text="sample.sampcode"></td>
|
||||||
|
<td x-text="sample.name"></td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.colstatus == 1" disabled>
|
||||||
|
</td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.tubestatus != 0" disabled>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
||||||
|
<template x-if="sample.colstatus == 0">
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1" @click="collect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.colstatus == 1">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1" @click="uncollect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.tubestatus != 0">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1" @click="unreceive(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Recv.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
10
app/Views/cs/dialog_unval.php
Normal file
10
app/Views/cs/dialog_unval.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<dialog class="modal" :open="isDialogUnvalOpen">
|
||||||
|
<div class="modal-box">
|
||||||
|
<textarea class="textarea textarea-bordered w-full" rows="5" x-model="unvalReason" placeholder="Enter reason for unvalidation..."></textarea>
|
||||||
|
<p class='text-right mt-2'>
|
||||||
|
<button class="btn btn-sm btn-neutral" @click="closeUnvalDialog()">Cancel</button>
|
||||||
|
<button id="unvalidate-btn" x-ref="unvalidateBtn" class="btn btn-sm btn-warning"
|
||||||
|
@click="unvalidate(unvalAccessnumber, '<?=session('userid');?>')" :disabled="!unvalReason.trim()">Unvalidate</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
13
app/Views/cs/dialog_val.php
Normal file
13
app/Views/cs/dialog_val.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<dialog class="modal" :open="isDialogValOpen">
|
||||||
|
<template x-if="valAccessnumber">
|
||||||
|
<div class="modal-box w-2/3 max-w-5xl">
|
||||||
|
<p class='text-right mx-3 mb-2'>
|
||||||
|
<button class="btn btn-sm btn-neutral" @click="closeValDialog()">Cancel</button>
|
||||||
|
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
|
||||||
|
@click="validate(valAccessnumber, '<?=session('userid');?>')" :disabled="!isValidateEnabled">Validate</button>
|
||||||
|
</p>
|
||||||
|
<!-- <iframe id="result-iframe" src="http://glenlis/spooler_db/main_dev.php?acc=" width="750px" height="600px"></iframe> -->
|
||||||
|
<iframe id="result-iframe" x-ref="resultIframe" src="<?=base_url('dummypage');?>" width="750px" height="600px"></iframe>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dialog>
|
||||||
413
app/Views/cs/index.php
Normal file
413
app/Views/cs/index.php
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
<?= $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>
|
||||||
|
</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>
|
||||||
|
</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:8%;'><div class="skeleton h-4 w-16"></div></th>
|
||||||
|
<th style='width:8%;'><div class="skeleton h-4 w-20"></div></th>
|
||||||
|
<th style='width:15%;'><div class="skeleton h-4 w-32"></div></th>
|
||||||
|
<th style='width:5%;'><div class="skeleton h-4 w-12"></div></th>
|
||||||
|
<th style='width:5%;'><div class="skeleton h-4 w-16"></div></th>
|
||||||
|
<th style='width:4%;'><div class="skeleton h-4 w-12"></div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="i in 5" :key="i">
|
||||||
|
<tr>
|
||||||
|
<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'>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php echo $this->include('admin/dialog_sample'); ?>
|
||||||
|
<?php echo $this->include('admin/dialog_val'); ?>
|
||||||
|
<?php echo $this->include('admin/dialog_unval'); ?>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
|
<?= $this->section('script') ?>
|
||||||
|
<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'],
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
|
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'},
|
||||||
|
}).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++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.list.sort((a, b) => {
|
||||||
|
let codeA = statusOrder[a.STATS] ?? 0;
|
||||||
|
let codeB = statusOrder[b.STATS] ?? 0;
|
||||||
|
return codeA - codeB;
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.filter.date1 = this.today;
|
||||||
|
this.filter.date2 = this.today;
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = [];
|
||||||
|
}).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'); ?>'})
|
||||||
|
})
|
||||||
|
.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'); ?>'})
|
||||||
|
})
|
||||||
|
.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'); ?>'})
|
||||||
|
})
|
||||||
|
.then(res => res.json()).then(data => {
|
||||||
|
this.fetchItem(accessnumber);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
validate 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
closeValDialog () {
|
||||||
|
this.isDialogValOpen = false;
|
||||||
|
},
|
||||||
|
validate(accessnumber, userid) {
|
||||||
|
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({ userid:`${userid}` })
|
||||||
|
}).then(response => {
|
||||||
|
this.closeValDialog();
|
||||||
|
this.fetchList();
|
||||||
|
console.log('Validate clicked for', this.valAccessnumber, '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(); ?>
|
||||||
62
app/Views/cs/main.php
Normal file
62
app/Views/cs/main.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="corporate">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CMOD</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/js/all.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
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 {
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<?=$this->renderSection('content');?>
|
||||||
|
|
||||||
|
<footer class='bg-base-100 p-1'>© <?=date('Y');?> - 5Panda</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.BASEURL = "<?=base_url("lab");?>";
|
||||||
|
</script>
|
||||||
|
<?=$this->renderSection('script');?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -28,13 +28,13 @@
|
|||||||
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
|
||||||
<div class='flex-1'>
|
<div class='flex-1'>
|
||||||
<a class='text-xl text-primary font-bold tracking-wide flex items-center gap-2'>
|
<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">| Admin Dashboard</span>
|
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Lab Analyst Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="text-right hidden sm:block leading-tight">
|
<div class="text-right hidden sm:block leading-tight">
|
||||||
<div class="text-sm font-bold opacity-70">Hi, <?=session('userid'); ?></div>
|
<div class="text-sm font-bold opacity-70">Hi, <?=session('userid'); ?></div>
|
||||||
<div class="text-xs opacity-50">Lab User</div>
|
<div class="text-xs opacity-50"><?= session()->get('userrole') ?></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
|
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
|
||||||
|
|||||||
88
app/Views/phlebo/dialog_sample.php
Normal file
88
app/Views/phlebo/dialog_sample.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<dialog class="modal" :open="isDialogSampleOpen">
|
||||||
|
<div class="modal-box w-2/3 max-w-5xl">
|
||||||
|
<p class='text-right'><button class="btn btn-xs btn-neutral" @click="closeSampleDialog()">X</button></p>
|
||||||
|
|
||||||
|
<table class="table table-xs table-compact w-full mb-4">
|
||||||
|
<tr>
|
||||||
|
<td>MR# </td> <td x-text="': '+item.patnumber"></td>
|
||||||
|
<td>Patient Name </td> <td x-text="': '+item.patname"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>KTP# </td> <td x-text="': '+item.ktp"></td>
|
||||||
|
<td>Sex / Age </td> <td x-text="': '+item.placeofbirth+' '+item.gender+' / '+item.age"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Note</td>
|
||||||
|
<td colspan='3'>
|
||||||
|
<textarea x-text="item.comment" class="textarea textarea-bordered w-full"></textarea>
|
||||||
|
<button class="btn btn-sm btn-primary mt-2" @click="saveComment(item.accessnumber)">Save</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="table table-xs table-compact w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sample Code</th>
|
||||||
|
<th>Sample Name</th>
|
||||||
|
<th class='text-center'>Collected</th>
|
||||||
|
<th class='text-center'>Received</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td></td> <td>Collection</td> <td></td> <td></td>
|
||||||
|
<td><button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td> <td>All</td> <td></td> <td></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></i></button>
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1" onclick=""><h6 class="p-0 m-0">Coll.</h6></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<template x-for="sample in item.samples">
|
||||||
|
<tr>
|
||||||
|
<td x-text="sample.sampcode"></td>
|
||||||
|
<td x-text="sample.name"></td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.colstatus == 1" disabled>
|
||||||
|
</td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.tubestatus != 0" disabled>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
||||||
|
<template x-if="sample.colstatus == 0">
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1" @click="collect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.colstatus == 1">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1" @click="uncollect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.tubestatus != 0">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1" @click="unreceive(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Recv.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
10
app/Views/phlebo/dialog_unval.php
Normal file
10
app/Views/phlebo/dialog_unval.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<dialog class="modal" :open="isDialogUnvalOpen">
|
||||||
|
<div class="modal-box">
|
||||||
|
<textarea class="textarea textarea-bordered w-full" rows="5" x-model="unvalReason" placeholder="Enter reason for unvalidation..."></textarea>
|
||||||
|
<p class='text-right mt-2'>
|
||||||
|
<button class="btn btn-sm btn-neutral" @click="closeUnvalDialog()">Cancel</button>
|
||||||
|
<button id="unvalidate-btn" x-ref="unvalidateBtn" class="btn btn-sm btn-warning"
|
||||||
|
@click="unvalidate(unvalAccessnumber, '<?=session('userid');?>')" :disabled="!unvalReason.trim()">Unvalidate</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
13
app/Views/phlebo/dialog_val.php
Normal file
13
app/Views/phlebo/dialog_val.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<dialog class="modal" :open="isDialogValOpen">
|
||||||
|
<template x-if="valAccessnumber">
|
||||||
|
<div class="modal-box w-2/3 max-w-5xl">
|
||||||
|
<p class='text-right mx-3 mb-2'>
|
||||||
|
<button class="btn btn-sm btn-neutral" @click="closeValDialog()">Cancel</button>
|
||||||
|
<button id="validate-btn" x-ref="validateBtn" class="btn btn-sm btn-success"
|
||||||
|
@click="validate(valAccessnumber, '<?=session('userid');?>')" :disabled="!isValidateEnabled">Validate</button>
|
||||||
|
</p>
|
||||||
|
<!-- <iframe id="result-iframe" src="http://glenlis/spooler_db/main_dev.php?acc=" width="750px" height="600px"></iframe> -->
|
||||||
|
<iframe id="result-iframe" x-ref="resultIframe" src="<?=base_url('dummypage');?>" width="750px" height="600px"></iframe>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dialog>
|
||||||
413
app/Views/phlebo/index.php
Normal file
413
app/Views/phlebo/index.php
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
<?= $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>
|
||||||
|
</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>
|
||||||
|
</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:8%;'><div class="skeleton h-4 w-16"></div></th>
|
||||||
|
<th style='width:8%;'><div class="skeleton h-4 w-20"></div></th>
|
||||||
|
<th style='width:15%;'><div class="skeleton h-4 w-32"></div></th>
|
||||||
|
<th style='width:5%;'><div class="skeleton h-4 w-12"></div></th>
|
||||||
|
<th style='width:5%;'><div class="skeleton h-4 w-16"></div></th>
|
||||||
|
<th style='width:4%;'><div class="skeleton h-4 w-12"></div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="i in 5" :key="i">
|
||||||
|
<tr>
|
||||||
|
<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'>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php echo $this->include('admin/dialog_sample'); ?>
|
||||||
|
<?php echo $this->include('admin/dialog_val'); ?>
|
||||||
|
<?php echo $this->include('admin/dialog_unval'); ?>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
|
<?= $this->section('script') ?>
|
||||||
|
<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'],
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
|
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'},
|
||||||
|
}).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++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.list.sort((a, b) => {
|
||||||
|
let codeA = statusOrder[a.STATS] ?? 0;
|
||||||
|
let codeB = statusOrder[b.STATS] ?? 0;
|
||||||
|
return codeA - codeB;
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.filter.date1 = this.today;
|
||||||
|
this.filter.date2 = this.today;
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = [];
|
||||||
|
}).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'); ?>'})
|
||||||
|
})
|
||||||
|
.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'); ?>'})
|
||||||
|
})
|
||||||
|
.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'); ?>'})
|
||||||
|
})
|
||||||
|
.then(res => res.json()).then(data => {
|
||||||
|
this.fetchItem(accessnumber);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
validate 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
closeValDialog () {
|
||||||
|
this.isDialogValOpen = false;
|
||||||
|
},
|
||||||
|
validate(accessnumber, userid) {
|
||||||
|
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({ userid:`${userid}` })
|
||||||
|
}).then(response => {
|
||||||
|
this.closeValDialog();
|
||||||
|
this.fetchList();
|
||||||
|
console.log('Validate clicked for', this.valAccessnumber, '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(); ?>
|
||||||
62
app/Views/phlebo/main.php
Normal file
62
app/Views/phlebo/main.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="corporate">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CMOD</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/js/all.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
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 {
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<?=$this->renderSection('content');?>
|
||||||
|
|
||||||
|
<footer class='bg-base-100 p-1'>© <?=date('Y');?> - 5Panda</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.BASEURL = "<?=base_url("lab");?>";
|
||||||
|
</script>
|
||||||
|
<?=$this->renderSection('script');?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
app/Views/superuser/dialog_preview.php
Normal file
45
app/Views/superuser/dialog_preview.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<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">
|
||||||
|
<button @click="previewType = 'preview'" :class="previewType === 'preview' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">Preview</button>
|
||||||
|
<button @click="setPreviewType('ind')" :class="previewType === 'ind' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">IND</button>
|
||||||
|
<button @click="setPreviewType('eng')" :class="previewType === 'eng' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" class="btn btn-sm join-item">ENG</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>
|
||||||
99
app/Views/superuser/dialog_sample.php
Normal file
99
app/Views/superuser/dialog_sample.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<dialog class="modal" :open="isDialogSampleOpen">
|
||||||
|
<div class="modal-box w-2/3 max-w-5xl">
|
||||||
|
<p class='text-right'><button class="btn btn-xs btn-neutral" @click="closeSampleDialog()">X</button></p>
|
||||||
|
|
||||||
|
<template x-if="isSampleLoading">
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
<p class="mt-2">Loading data...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="!isSampleLoading">
|
||||||
|
<div>
|
||||||
|
<table class="table table-xs table-compact w-full mb-4">
|
||||||
|
<tr>
|
||||||
|
<td>MR# </td> <td x-text="': '+item.patnumber"></td>
|
||||||
|
<td>Patient Name </td> <td x-text="': '+item.patname"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>KTP# </td> <td x-text="': '+item.ktp"></td>
|
||||||
|
<td>Sex / Age </td> <td x-text="': '+item.placeofbirth+' '+item.gender+' / '+item.age"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Note</td>
|
||||||
|
<td colspan='3'>
|
||||||
|
<textarea x-text="item.comment" class="textarea textarea-bordered w-full"></textarea>
|
||||||
|
<button class="btn btn-sm btn-primary mt-2" @click="saveComment(item.accessnumber)">Save</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="table table-xs table-compact w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sample Code</th>
|
||||||
|
<th>Sample Name</th>
|
||||||
|
<th class='text-center'>Collected</th>
|
||||||
|
<th class='text-center'>Received</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td></td> <td>Collection</td> <td></td> <td></td>
|
||||||
|
<td><button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td> <td>All</td> <td></td> <td></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></i></button>
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1" onclick=""><h6 class="p-0 m-0">Coll.</h6></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<template x-for="sample in item.samples">
|
||||||
|
<tr>
|
||||||
|
<td x-text="sample.sampcode"></td>
|
||||||
|
<td x-text="sample.name"></td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.colstatus == 1" disabled>
|
||||||
|
</td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.tubestatus != 0" disabled>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
||||||
|
<template x-if="sample.colstatus == 0">
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1" @click="collect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.colstatus == 1">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1" @click="uncollect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.tubestatus != 0">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1" @click="unreceive(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Recv.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
28
app/Views/superuser/dialog_setPassword.php
Normal file
28
app/Views/superuser/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>
|
||||||
20
app/Views/superuser/dialog_unval.php
Normal file
20
app/Views/superuser/dialog_unval.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<dialog class="modal" :open="isDialogUnvalOpen">
|
||||||
|
<template x-if="unvalAccessnumber">
|
||||||
|
<div class="modal-box w-96">
|
||||||
|
<h3 class="font-bold text-lg mb-4 text-warning">
|
||||||
|
<i class="fa fa-exclamation-triangle mr-2"></i>Unvalidate Request
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm mb-3" x-text="'Access Number: ' + unvalAccessnumber"></p>
|
||||||
|
<textarea class="textarea textarea-bordered w-full" rows="4"
|
||||||
|
x-model="unvalReason" placeholder="Enter reason for unvalidation..."></textarea>
|
||||||
|
<p class='text-right mt-3'>
|
||||||
|
<button class="btn btn-sm btn-ghost" @click="closeUnvalDialog()">Cancel</button>
|
||||||
|
<button id="unvalidate-btn" class="btn btn-sm btn-warning"
|
||||||
|
@click="unvalidate(unvalAccessnumber, '<?=session('userid');?>')"
|
||||||
|
:disabled="!unvalReason.trim()">
|
||||||
|
<i class="fa fa-undo mr-1"></i>Unvalidate
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dialog>
|
||||||
401
app/Views/superuser/index.php
Normal file
401
app/Views/superuser/index.php
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
<?= $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>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</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:7%;'>No Register</th>
|
||||||
|
<th style='width:8%;'>Reff</th>
|
||||||
|
<th style='width:8%;'>Doctor</th>
|
||||||
|
<th style='width:15%;'>Tests</th>
|
||||||
|
<th style='width:3%;'>ResTo</th>
|
||||||
|
<th style='width:5%;'>Val</th>
|
||||||
|
<th style='width:5%;'>Result</th>
|
||||||
|
<th style='width:4%;'>Status</th>
|
||||||
|
</tr>
|
||||||
|
</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'>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php echo $this->include('superuser/dialog_sample'); ?>
|
||||||
|
<?php echo $this->include('superuser/dialog_unval'); ?>
|
||||||
|
<?php echo $this->include('superuser/dialog_preview'); ?>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
|
<?= $this->section('script') ?>
|
||||||
|
<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'],
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.today = new Date().toISOString().slice(0, 10);
|
||||||
|
this.filter.date1 = this.today;
|
||||||
|
this.filter.date2 = this.today;
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchList(){
|
||||||
|
this.isLoading = true;
|
||||||
|
this.list = [];
|
||||||
|
let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 };
|
||||||
|
let param = new URLSearchParams(this.filter).toString();
|
||||||
|
for (let k in this.counters) { this.counters[k] = 0; }
|
||||||
|
fetch(`${BASEURL}/api/requests?${param}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
this.list = data.data ?? [];
|
||||||
|
this.filterKey = 'Total';
|
||||||
|
this.list.forEach(item => {
|
||||||
|
if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; }
|
||||||
|
else {
|
||||||
|
if(item.STATS == 'PartColl') { this.counters.Coll++; }
|
||||||
|
else if(item.STATS == 'PartRecv') { this.counters.Recv++; }
|
||||||
|
this.counters.Total++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.list.sort((a, b) => {
|
||||||
|
let codeA = statusOrder[a.STATS] ?? 0;
|
||||||
|
let codeB = statusOrder[b.STATS] ?? 0;
|
||||||
|
return codeA - codeB;
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.filter.date1 = this.today;
|
||||||
|
this.filter.date2 = this.today;
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = [];
|
||||||
|
}).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'); ?>'})
|
||||||
|
})
|
||||||
|
.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'); ?>'})
|
||||||
|
})
|
||||||
|
.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'); ?>'})
|
||||||
|
})
|
||||||
|
.then(res => res.json()).then(data => {
|
||||||
|
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(); ?>
|
||||||
62
app/Views/superuser/main.php
Normal file
62
app/Views/superuser/main.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="corporate">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CMOD</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/js/all.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
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 {
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<?=$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");?>";
|
||||||
|
</script>
|
||||||
|
<?=$this->renderSection('script');?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
269
app/Views/superuser/users.php
Normal file
269
app/Views/superuser/users.php
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<?= $this->extend('superuser/main'); ?>
|
||||||
|
|
||||||
|
<?= $this->section('content') ?>
|
||||||
|
<div x-data="users" class="contents">
|
||||||
|
<main class="p-4 flex-1 flex flex-col gap-2 max-w-6xl w-full mx-auto">
|
||||||
|
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-2xl font-bold flex items-center gap-2 text-base-content">
|
||||||
|
<i class="fa fa-users text-primary"></i> User Management
|
||||||
|
</h2>
|
||||||
|
<button class="btn btn-primary btn-sm" @click="openUserModal('create')">
|
||||||
|
<i class="fa fa-plus"></i> Add User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<template x-if="isLoading">
|
||||||
|
<table class="table table-zebra w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><div class="skeleton h-4 w-24"></div></th>
|
||||||
|
<th><div class="skeleton h-4 w-24"></div></th>
|
||||||
|
<th class="text-right"><div class="skeleton h-4 w-16 ml-auto"></div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="i in 5" :key="i">
|
||||||
|
<tr>
|
||||||
|
<td><div class="skeleton h-4 w-20"></div></td>
|
||||||
|
<td><div class="skeleton h-4 w-24"></div></td>
|
||||||
|
<td class="text-right"><div class="skeleton h-4 w-16 ml-auto"></div></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
<template x-if="!isLoading && list.length">
|
||||||
|
<table class="table table-zebra w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User ID</th>
|
||||||
|
<th>Role/Level</th>
|
||||||
|
<th class="text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="user in list" :key="user.USERID">
|
||||||
|
<tr>
|
||||||
|
<td class="font-bold" x-text="user.USERID"></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge"
|
||||||
|
:class="getRoleClass(user.USERLEVEL)"
|
||||||
|
x-text="getRoleName(user.USERLEVEL)"></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<button class="btn btn-square btn-ghost btn-xs text-info" @click="openUserModal('edit', user)">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-square btn-ghost btn-xs text-error" @click="deleteUser(user.USERID)">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</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 users found</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- User Modal -->
|
||||||
|
<dialog id="user_modal" class="modal">
|
||||||
|
<div class="modal-box p-0 overflow-hidden w-11/12 max-w-lg bg-base-100 shadow-2xl">
|
||||||
|
<div class="p-6 flex flex-col gap-4">
|
||||||
|
<div class="alert alert-info shadow-sm py-2 text-sm" x-show="mode === 'edit'">
|
||||||
|
<i class="fa fa-info-circle"></i> Editing user: <span class="font-bold font-mono" x-text="form.userid"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User ID & Level -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium text-base-content/70">User ID</span>
|
||||||
|
</label>
|
||||||
|
<label class="input input-bordered flex items-center gap-2 focus-within:input-primary transition-all">
|
||||||
|
<i class="fa fa-id-badge text-base-content/40"></i>
|
||||||
|
<input type="text" class="grow font-mono" x-model="form.userid" :disabled="mode === 'edit'" placeholder="e.g. USER001" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium text-base-content/70">Role / Level</span>
|
||||||
|
</label>
|
||||||
|
<select class="select select-bordered w-full focus:select-primary transition-all" x-model="form.userlevel">
|
||||||
|
<option value="" disabled>Select Level</option>
|
||||||
|
<option value="1">Admin</option>
|
||||||
|
<option value="2">Lab</option>
|
||||||
|
<option value="3">Phlebotomist</option>
|
||||||
|
<option value="4">Customer Service</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider text-xs text-base-content/30 my-0">Security</div>
|
||||||
|
|
||||||
|
<!-- Passwords -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium text-base-content/70">Password</span>
|
||||||
|
<span class="label-text-alt text-xs opacity-50" x-show="mode === 'edit'">(Optional)</span>
|
||||||
|
</label>
|
||||||
|
<label class="input input-bordered flex items-center gap-2 focus-within:input-primary transition-all">
|
||||||
|
<i class="fa fa-lock text-base-content/40"></i>
|
||||||
|
<input type="password" class="grow" x-model="form.password" placeholder="••••••" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium text-base-content/70">Confirm</span>
|
||||||
|
</label>
|
||||||
|
<label class="input input-bordered flex items-center gap-2 focus-within:input-primary transition-all">
|
||||||
|
<i class="fa fa-lock text-base-content/40"></i>
|
||||||
|
<input type="password" class="grow" x-model="form.password_2" placeholder="••••••" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div x-show="errorMsg"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 transform scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 transform scale-100"
|
||||||
|
class="alert alert-error text-sm shadow-md">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
<span x-text="errorMsg"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="modal-action bg-base-200/50 p-6 m-0 flex justify-between items-center border-t border-base-200">
|
||||||
|
<button class="btn btn-ghost hover:bg-base-200 text-base-content/70" @click="closeModal()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary px-8 shadow-lg shadow-primary/30 min-w-[120px]" @click="saveUser()" :disabled="isLoading">
|
||||||
|
<span x-show="isLoading" class="loading loading-spinner loading-xs"></span>
|
||||||
|
<span x-show="!isLoading" x-text="mode === 'create' ? 'Create User' : 'Save Changes'"></span>
|
||||||
|
<i x-show="!isLoading" class="fa fa-save ml-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button @click="closeModal()">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
|
<?= $this->section('script') ?>
|
||||||
|
<script type="module">
|
||||||
|
import Alpine from '<?=base_url("js/app.js");?>';
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data("users", () => ({
|
||||||
|
list: [],
|
||||||
|
mode: 'create',
|
||||||
|
isLoading: false,
|
||||||
|
errorMsg: '',
|
||||||
|
form: {
|
||||||
|
userid: '',
|
||||||
|
userlevel: '',
|
||||||
|
password: '',
|
||||||
|
password_2: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchUsers() {
|
||||||
|
this.isLoading = true;
|
||||||
|
fetch(`${BASEURL}/api/users`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
this.list = data.data ?? [];
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRoleName(level) {
|
||||||
|
const map = { 1: 'Administrator', 2: 'Lab', 3: 'Phlebotomist', 4: 'Customer Service' };
|
||||||
|
return map[level] || 'Unknown (' + level + ')';
|
||||||
|
},
|
||||||
|
|
||||||
|
getRoleClass(level) {
|
||||||
|
const map = { 1: 'badge-primary', 2: 'badge-secondary', 3: 'badge-accent', 4: 'badge-neutral' };
|
||||||
|
return map[level] || 'badge-ghost';
|
||||||
|
},
|
||||||
|
|
||||||
|
openUserModal(targetMode, user = null) {
|
||||||
|
this.mode = targetMode;
|
||||||
|
this.errorMsg = '';
|
||||||
|
|
||||||
|
if (targetMode === 'edit' && user) {
|
||||||
|
this.form = {
|
||||||
|
userid: user.USERID,
|
||||||
|
userlevel: user.USERLEVEL,
|
||||||
|
password: '',
|
||||||
|
password_2: ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.form = { userid: '', userlevel: '', password: '', password_2: '' };
|
||||||
|
}
|
||||||
|
document.getElementById('user_modal').showModal();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
document.getElementById('user_modal').close();
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUser() {
|
||||||
|
this.errorMsg = '';
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res;
|
||||||
|
if(this.mode == 'create') {
|
||||||
|
res = await fetch(`${BASEURL}/api/users`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await fetch(`${BASEURL}/api/users/${this.form.userid}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(this.form)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.messages?.error || data.message || 'Error saving user');
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
this.errorMsg = err.message;
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.closeModal();
|
||||||
|
this.fetchUsers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
Alpine.start();
|
||||||
|
</script>
|
||||||
|
<?= $this->endSection(); ?>
|
||||||
Loading…
x
Reference in New Issue
Block a user