post to prod

This commit is contained in:
mahdahar 2025-12-09 14:10:43 +07:00
parent d3703ab653
commit 43c8823803
17 changed files with 1285 additions and 454 deletions

View File

@ -68,18 +68,40 @@ $routes->delete('api/specimen/receive/(:any)', 'Specimen::unreceive/$1');
/*- lets go alpine -*/
$routes->group('v2', function($routes) {
$routes->get('', 'V2::index');
$routes->get('login', 'V2::loginPage');
$routes->post('login', 'V2::login');
$routes->group('admin', ['filter' => 'role:1'], function($routes) {
$routes->get('', 'V2\Admin::index');
$routes->get('users', 'V2\Admin::users');
// internal api for this module
$routes->get('api/users', 'V2\Admin::usersList');
$routes->post('api/users', 'V2\Admin::userCreate');
$routes->post('api/users/update', 'V2\Admin::userUpdate');
$routes->post('api/users/delete', 'V2\Admin::userDelete');
});
});
$routes->get('', 'V2::index');
$routes->get('login', 'V2::loginPage');
$routes->post('login', 'V2::login');
$routes->get('logout', 'V2::logout');
// Admin
$routes->group('admin', ['filter' => 'role:1'], function($routes) {
$routes->get('', 'V2\Admin::index');
$routes->get('users', 'V2\Admin::users');
$routes->post('setPassword', 'V2\Users::setPassword');
// Users
$routes->get('api/users', 'V2\Users::index');
$routes->post('api/users', 'V2\Users::create');
$routes->patch('api/users/(:any)', 'V2\Users::update/$1');
$routes->delete('api/users/(:any)', 'V2\Users::delete/$1');
// Request
$routes->get('api/requests', 'V2\Requests::index');
$routes->post('api/requests/validate/(:any)', 'V2\Requests::validate/$1');
$routes->delete('api/requests/validate/(:any)', 'V2\Requests::unvalidate/$1');
// Samples
$routes->post('api/samples/collect/(:any)', 'V2\Samples::collect/$1');
$routes->delete('api/samples/collect/(:any)', 'V2\Samples::uncollect/$1');
$routes->delete('api/samples/receive/(:any)', 'V2\Samples::unreceive/$1');
$routes->get('api/samples/(:any)', 'V2\Samples::show/$1');
});
// Analyst and doctor
$routes->group('lab', ['filter' => 'role:2'], function($routes) {
$routes->get('', 'V2\Lab::index');
$routes->post('setPassword', 'V2\Lab::setPassword');
// Request
$routes->get('api/requests', 'V2\Requests::index');
$routes->post('api/requests/validate/(:any)', 'V2\Requests::validate/$1');
$routes->delete('api/requests/validate/(:any)', 'V2\Requests::unvalidate/$1');
// Samples
$routes->post('api/samples/collect/(:any)', 'V2\Samples::collect/$1');
$routes->get('api/samples/(:any)', 'V2\Samples::show/$1');
});
});

View File

@ -3,59 +3,59 @@
namespace App\Controllers;
class Auth extends BaseController {
public function login() {
if ($this->request->getMethod() === 'GET') {
return view('login');
} else if ($this->request->getMethod() === 'POST') {
helper(['form', 'url']);
$session = session();
$db = \Config\Database::connect();
public function login() {
if ($this->request->getMethod() === 'GET') {
return view('login');
} else if ($this->request->getMethod() === 'POST') {
helper(['form', 'url']);
$session = session();
$db = \Config\Database::connect();
$userid = strtoupper(trim($this->request->getPost('userid')));
$password = $this->request->getPost('password');
$userid = strtoupper(trim($this->request->getPost('userid')));
$password = $this->request->getPost('password');
// Gunakan raw SQL sesuai kolom di tabel kamu
$query = $db->query("SELECT * FROM gdc_cmod.dbo.USERS WHERE USERID = ?", [$userid]);
$user = $query->getRowArray();
// Gunakan raw SQL sesuai kolom di tabel kamu
$query = $db->query("SELECT * FROM gdc_cmod.dbo.USERS WHERE USERID = ?", [$userid]);
$user = $query->getRowArray();
if ($user && !empty($user['PASSWORD']) && password_verify($password, $user['PASSWORD'])) {
if ($user && !empty($user['PASSWORD']) && password_verify($password, $user['PASSWORD'])) {
// Role untuk url
switch ((int)$user['USERLEVEL']) {
case 1: $role = 'admin'; break;
case 2: $role = 'doctor'; break;
case 3: $role = 'analyst'; break;
case 4: $role = 'cs'; break;
default: $role = ''; break;
}
// Role untuk url
switch ((int)$user['USERLEVEL']) {
case 1: $role = 'admin'; break;
case 2: $role = 'doctor'; break;
case 3: $role = 'analyst'; break;
case 4: $role = 'cs'; break;
default: $role = ''; break;
}
// Simpan session
$session->set([
'isLoggedIn' => true,
'userid' => (string) $user['USERID'],
'userlevel' => (int) $user['USERLEVEL'],
'userrole' => (string) $role,
]);
// Simpan session
$session->set([
'isLoggedIn' => true,
'userid' => (string) $user['USERID'],
'userlevel' => (int) $user['USERLEVEL'],
'userrole' => (string) $role,
]);
// Redirect sesuai level dari data didatabase
switch ((int)$user['USERLEVEL']) {
case 1: return redirect()->to('/admin');
case 2: return redirect()->to('/doctor');
case 3: return redirect()->to('/analyst');
case 4: return redirect()->to('/cs');
default: return redirect()->to('/login');
}
} else {
$session->setFlashdata('error', 'USERID atau PASSWORD salah.');
return redirect()->back();
}
// Redirect sesuai level dari data didatabase
switch ((int)$user['USERLEVEL']) {
case 1: return redirect()->to('/admin');
case 2: return redirect()->to('/doctor');
case 3: return redirect()->to('/analyst');
case 4: return redirect()->to('/cs');
default: return redirect()->to('/login');
}
} else {
$session->setFlashdata('error', 'USERID atau PASSWORD salah.');
return redirect()->back();
}
}
}
}
}
public function logout() {
session()->destroy();
return redirect()->to('/login');
}
public function logout() {
session()->destroy();
return redirect()->to('/login');
}
}

View File

@ -78,7 +78,7 @@ class V2 extends BaseController {
case 1:
return redirect()->to('v2/admin');
case 2:
return redirect()->to('v2/analyst');
return redirect()->to('v2/lab');
case 3:
return redirect()->to('v2/phlebotomist');
case 4:
@ -91,4 +91,10 @@ class V2 extends BaseController {
return redirect()->back();
}
}
public function logout() {
$session = session();
$session->destroy();
return redirect()->to('v2/login');
}
}

View File

@ -5,137 +5,20 @@ use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
class Admin extends BaseController {
use ResponseTrait;
use ResponseTrait;
protected $db;
protected $db;
public function __construct() {
$this->db = \Config\Database::connect();
helper(['url', 'form', 'text']);
}
public function __construct() {
$this->db = \Config\Database::connect();
helper(['url', 'form', 'text']);
}
public function index() {
return view('v2/admin/index');
}
public function index() {
return view('v2/admin/index');
}
public function users() {
return view('v2/admin/users');
}
public function profile() {
return view('v2/admin/profile');
}
public function settings() {
return view('v2/admin/settings');
}
// API Methods
public function usersList() {
$sql = "select u.USERID, u.USERLEVEL from GDC_CMOD.dbo.USERS u
left join glendb.dbo.USERS u1 on u1.USERID=u.USERID
where u1.LOCKEDACCOUNT is null";
$query = $this->db->query($sql);
$results = $query->getResultArray();
return $this->respond(['data' => $results]);
}
public function userCreate() {
$json = $this->request->getJSON();
$userid = strtoupper(trim($json->userid ?? ''));
$userlevel = trim($json->userlevel ?? '');
$password = trim($json->password ?? '');
$password_2 = trim($json->password_2 ?? '');
if (empty($userid) || empty($userlevel) || empty($password)) {
return $this->fail('All fields are required', 400);
}
if ($password != $password_2) {
return $this->fail('Passwords do not match', 400);
}
if (strlen($password) < 3) {
return $this->fail('Password must be at least 3 characters', 400);
}
// Check exists
$sql = $this->db->query("SELECT USERID FROM gdc_cmod.dbo.USERS WHERE USERID = ?", [$userid]);
if ($sql->getRowArray()) {
return $this->fail('User ID already exists', 400);
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$this->db->transBegin();
try {
$sqlInsert = "INSERT INTO gdc_cmod.dbo.USERS (USERID, USERLEVEL, PASSWORD) VALUES (?, ?, ?)";
$this->db->query($sqlInsert, [$userid, $userlevel, $hashedPassword]);
$this->db->transCommit();
} catch (\Throwable $e) {
$this->db->transRollback();
return $this->failServerError($e->getMessage());
}
return $this->respondCreated(['message' => 'User created']);
}
public function userUpdate() {
$json = $this->request->getJSON();
$userid = strtoupper(trim($json->userid ?? ''));
$userlevel = trim($json->userlevel ?? '');
$password = trim($json->password ?? '');
$password_2 = trim($json->password_2 ?? '');
if (empty($userid)) {
return $this->fail('User ID is required', 400);
}
$fullUpdate = false;
$hashedPassword = '';
if (!empty($password) || !empty($password_2)) {
if ($password != $password_2) {
return $this->fail('Passwords do not match', 400);
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$fullUpdate = true;
}
$this->db->transBegin();
try {
if ($fullUpdate) {
$sql = "UPDATE gdc_cmod.dbo.USERS SET USERLEVEL = ?, PASSWORD = ? WHERE USERID = ?";
$this->db->query($sql, [$userlevel, $hashedPassword, $userid]);
} else {
$sql = "UPDATE gdc_cmod.dbo.USERS SET USERLEVEL = ? WHERE USERID = ?";
$this->db->query($sql, [$userlevel, $userid]);
}
$this->db->transCommit();
} catch (\Throwable $e) {
$this->db->transRollback();
return $this->failServerError();
}
return $this->respond(['message' => 'User updated']);
}
public function userDelete() {
$json = $this->request->getJSON();
$userid = strtoupper(trim($json->userid ?? ''));
if (empty($userid)) {
return $this->fail('User ID is required', 400);
}
$this->db->transBegin();
try {
$sqlDelete = "DELETE FROM gdc_cmod.dbo.USERS WHERE USERID = ?";
$this->db->query($sqlDelete, [$userid]);
$this->db->transCommit();
} catch (\Throwable $e) {
$this->db->transRollback();
return $this->failServerError();
}
return $this->respondDeleted(['message' => 'User deleted']);
}
public function users() {
return view('v2/admin/users');
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Controllers\V2;
use App\Controllers\BaseController;
class Lab extends BaseController {
public function __construct() {
helper(['url', 'form', 'text']);
}
public function index() {
return view('v2/lab/index');
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Controllers\V2;
use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController;
class Requests extends BaseController {
use ResponseTrait;
public function index() {
$db = db_connect();
$date1 = $this->request->getGet('date1');
$date2 = $this->request->getGet('date2');
$db = \Config\Database::connect();
$sql = "SELECT * from GDC_CMOD.dbo.V_DASHBOARD_DEV where
COLLECTIONDATE between '$date1 00:00' and '$date2 23:59'
and ODR_DDATE between '$date1 00:00' and '$date2 23:59'";
$rows = $db->query($sql)->getResultArray();
foreach ($rows as &$row) {
$row['COLLECTIONDATE'] = date('Y-m-d H:i', strtotime($row['COLLECTIONDATE']));
$row['ODR_DDATE'] = date('Y-m-d H:i', strtotime($row['ODR_DDATE']));
$row['REQDATE'] = date('Y-m-d H:i', strtotime($row['REQDATE']));
}
$data['data'] = $rows;
return $this->response->setJSON($data);
}
public function show($accessnumber) {
$db = db_connect();
$data['accessnumber'] = $accessnumber;
$sql = "SELECT d.STATS, r.* FROM GDC_CMOD.dbo.V_DASHBOARD_DEV d
left join GDC_CMOD.dbo.CM_REQUESTS r ON r.ACCESSNUMBER=d.SP_ACCESSNUMBER
WHERE d.SP_ACCESSNUMBER='$accessnumber'";
$result = $db
->query($sql)
->getResultArray();
$data['val1'] = $result[0]['ISVAL1'];
$data['val1user'] = $result[0]['VAL1USER'];
$data['val2'] = $result[0]['ISVAL2'];
$data['val2user'] = $result[0]['VAL2USER'];
return view('admin/modal_request',$data);
}
public function showUnval($accessnumber) {
$data['accessnumber'] = $accessnumber;
return view('admin/modal_unvalidate',$data);
}
public function unval($accessnumber) {
$input = $this->request->getJSON(true);
$userid = $input['userid'];
$comment = $input['comment'];
$db = db_connect();
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL1=null, VAL1USER=null, VAL1DATE=null, ISVAL2=null, VAL2USER=null, VAL2DATE=null,
ISPENDING=1, PENDINGTEXT='$comment', PENDINGUSER='$userid', PENDINGDATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
$db->query($sql);
$data = ['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber" ];
return $this->response->setJSON($data);
}
public function val($accessnumber) {
$input = $this->request->getJSON(true);
$userid = $input['userid'];
$db = db_connect();
//cek val
$sql = "select * from GDC_CMOD.dbo.CM_REQUESTS where ACCESSNUMBER='$accessnumber'";
$result = $db->query($sql)->getResultArray();
//$data['data'] = $result;
if(!isset($result[0])) {
$sql = "insert into GDC_CMOD.dbo.CM_REQUESTS(ACCESSNUMBER, ISVAL1, VAL1USER, VAL1DATE) VALUES ('$accessnumber', 1, '$userid', GETDATE())";
$db->query($sql);
$data['val'] = 1;
$data['userid'] = $userid;
} else {
$row = $result[0];
$isval1 = $row['ISVAL1'];
$isval2 = $row['ISVAL2'];
$val1user = $row['VAL1USER'];
if( $isval1 == 1 ) {
// val done
if ( $isval2 == 1 ) { return $this->response->setJSON(['message'=> 'validation done, not updating anything']); }
else {
// val2 if user val1 != userid
if($val1user != $userid) {
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL2=1, VAL2USER='$userid', VAL2DATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
$data['val'] = 2;
$data['userid'] = $userid;
} else {
$this->response->setStatusCode(500);
return $this->response->setJSON([ 'message'=> 'user already validate this request' ]);
}
}
} else {
// val1
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL1=1, VAL1USER='$userid', VAL1DATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
$data['val'] = 1;
$data['userid'] = $userid;
}
$db->query($sql);
}
return $this->response->setJSON($data);
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Controllers\V2;
use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController;
class Samples extends BaseController {
use ResponseTrait;
public function show($accessnumber) {
$db = \Config\Database::connect();
$sql = "SELECT right(p.PATNUMBER,16) as [patnumber], ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') as [Name],
case when format(p.BIRTHDATE,'MMdd')=format(spr.COLLECTIONDATE,'MMdd') then DATEDIFF(YEAR,p.BIRTHDATE, spr.COLLECTIONDATE)
else FLOOR(DATEDIFF(DAY, p.BIRTHDATE, spr.COLLECTIONDATE) / 365.25) end ,
[Gender] = case
when p.SEX = 1 then 'M'
when p.SEX = 2 then 'F'
else ''
end, spr.REQDATE, spo.COMMENTTEXT, dmg.DMG_CKTPNO, dmg.DMG_CPLACEOFBIRTH
from SP_REQUESTS spr
left join PATIENTS p on p.PATID=spr.PATID
left join SP_REQUESTS_OCOM spo on spr.SP_ACCESSNUMBER=spo.SP_ACCESSNUMBER
left join GDC_CMOD.dbo.TDL_DEMOGRAPHIC dmg on right(dmg.DMG_CPATNUMBER,15)=right(p.PATNUMBER,15)
where spr.PATID=p.PATID and spr.SP_ACCESSNUMBER='$accessnumber'";
$query = $db->query($sql);
$results = $query->getRowArray();
$data = [
'patnumber' => $results["patnumber"],
'age' => $results[""],
'patname' => $results['Name'] ?? '',
'reqdate' => $results['REQDATE'] ?? '',
'gender' => $results['Gender'] ?? '',
'placeofbirth' => $results['DMG_CPLACEOFBIRTH'] ?? '',
'ktp' => $results['DMG_CKTPNO'] ?? '',
'comment' => $results['COMMENTTEXT'] ?? '',
'accessnumber' => $accessnumber,
];
$samples = [];
$sql = "SELECT req.SAMPTYPEID, req.SAMPCODE, req.SHORTTEXT, tu.STATUS, st.TUBESTATUS
from GDC_CMOD.dbo.v_sp_reqtube req
left join GDC_CMOD.dbo.TUBES tu on req.SP_ACCESSNUMBER=tu.ACCESSNUMBER and req.SAMPCODE=tu.TUBENUMBER
left join glendb.dbo.SP_TUBES st on st.SP_ACCESSNUMBER=req.SP_ACCESSNUMBER and req.SAMPCODE=st.SAMPLETYPE
where req.SP_ACCESSNUMBER='$accessnumber'";
$query = $db->query($sql);
$results = $query->getResultArray();
foreach ($results as $row) {
$samples[] = [
'samptypeid' => $row['SAMPTYPEID'] ?? null,
'sampcode' => $row['SAMPCODE'] ?? null,
'name' => $row['SHORTTEXT'] ?? '',
'colstatus' => $row['STATUS'] ?? '',
'tubestatus' => $row['TUBESTATUS'] ?? '',
];
}
$data['samples'] = $samples;
$resp = [ 'data' => $data ];
return $this->response->setJSON($resp);
}
public function collect($accessnumber) {
$db = \Config\Database::connect();
$input = $this->request->getJSON(true);
$samplenumber = $input['samplenumber'];
$userid = $input['userid'];
$sql = "update GDC_CMOD.dbo.TUBES set USERID='$userid',STATUS='1', COLLECTIONDATE=getdate() where ACCESSNUMBER='$accessnumber' and TUBENUMBER='$samplenumber'";
$db->query($sql);
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
VALUES ('$accessnumber', '$samplenumber', '$userid', '1', getdate())";
$db->query($sql);
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber" ], 201);
}
public function uncollect($accessnumber) {
$db = \Config\Database::connect();
$input = $this->request->getJSON(true);
$samplenumber = $input['samplenumber'];
$userid = $input['userid'];
$sql = "update GDC_CMOD.dbo.TUBES set USERID='$userid',STATUS='0', COLLECTIONDATE=getdate() where ACCESSNUMBER='$accessnumber' and TUBENUMBER='$samplenumber'";
$db->query($sql);
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
VALUES ('$accessnumber', '$samplenumber', '$userid', '0', getdate())";
$db->query($sql);
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber" ], 201);
}
public function unreceive($accessnumber) {
$db = \Config\Database::connect();
$input = $this->request->getJSON(true);
$samplenumber = $input['samplenumber'];
// update firebird
$sql = "select r.EXTERNALORDERNUMBER, dt.TESTCODE, do.HISCODE from glendb.dbo.TESTS t
left join glendb.dbo.DICT_TESTS dt on dt.TESTID=t.TESTID
left join glendb.dbo.REQUESTS r on r.REQUESTID=t.REQUESTID
left join glendb.dbo.DICT_TEST_SAMPLES dts on dts.TESTID=t.TESTID
left join glendb.dbo.DICT_SAMPLES_TYPES ds on ds.SAMPTYPEID=dts.SAMPTYPEID
left join GDC_CMOD.dbo.DICT_TESTS_ORDER do on do.TESTCODE=dt.TESTCODE
where t.DEPTH=0
and r.ACCESSNUMBER='$accessnumber' and ds.SAMPCODE='$samplenumber'";
$rows = $db->query($sql)->getResultArray();
$his_test = '';
foreach( $rows as $row ) {
$hon = $row['EXTERNALORDERNUMBER'];
$testcode = $row['TESTCODE'];
$hiscode = $row['HISCODE'];
$his_test .= "'$hiscode',";
$lis_test .= "'$testcode',";
}
$his_test = rtrim($his_test,',');
$lis_test = rtrim($lis_test,',');
$conn = odbc_connect('GLENEAGLES','','');
$sql = "UPDATE TDL_ORDERDT SET ODD_NRECEIVED=NULL , ODD_DTRECEIVE=NULL WHERE ODR_CNOLAB='$hon' and ODD_CPRODUCTCODE IN ($his_test)";
$rs = odbc_exec($conn,$sql);
if (!$rs) {exit("Error in Update FB");}
$sql = "update SP_TUBES set TUBESTATUS=0 where SP_ACCESSNUMBER='$accessnumber' and SAMPLETYPE='$samplenumber' ";
$db->query($sql);
$sql = "update SP_TESTS set SP_TESTSTATUS=NULL where SP_ACCESSNUMBER='$accessnumber' and SP_TESTCODE in ($lis_test)";
$db->query($sql);
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber" ], 201);
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace App\Controllers\V2;
use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController;
// Users Management
class Users extends BaseController {
use ResponseTrait;
protected $db;
public function __construct() {
// Koneksi database dan validation service
$this->db = \Config\Database::connect();
}
public function index() {
$sql = "select u.USERID, u.USERLEVEL from GDC_CMOD.dbo.USERS u
left join glendb.dbo.USERS u1 on u1.USERID=u.USERID
where u1.LOCKEDACCOUNT is null";
$query = $this->db->query($sql);
$results = $query->getResultArray();
$data['data'] = $results;
return $this->respond(['data' => $results]);
}
public function create() {
$input = $this->request->getJSON(true);
// ambil input
$userid = $input['userid'];
$userlevel = $input['userlevel'];
$password = $input['password'];
$password_2 = $input['password_2'];
// Cek Password Apakah Sama
if ($password != $password_2) {
return $this->response->setJSON(['message'=> 'Password not the same']);
}
if ( strlen($password) < 3 ) {
return $this->response->setJSON(['message'=> 'Password must be more than 2 characters']);
}
// Cek Apakah USERID Sama
$sql = $this->db->query("SELECT USERID FROM gdc_cmod.dbo.USERS WHERE USERID = ?", [$userid]);
$query = $sql->getRowArray();
if ($query != null) {
return $this->response->setJSON(['message'=> 'Userid already exists']);
}
// Hash Password
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// Insert
$this->db->transBegin();
try {
$sqlInsert = "
INSERT INTO gdc_cmod.dbo.USERS (USERID, USERLEVEL, PASSWORD)
VALUES (?, ?, ?)
";
$this->db->query($sqlInsert, [$userid, $userlevel, $hashedPassword]);
$this->db->transCommit();
} catch (\Throwable $e) {
// Kalau ada error, rollback semua perubahan
$this->db->transRollback();
// (Opsional) tampilkan atau log error
return $this->response->setJSON(['message'=> 'Server error']);
}
return $this->response->setJSON(['message'=> 'User '.$userid.' Berhasil ditambahkan!']);
}
public function update() {
$input = $this->request->getJSON(true);
$userid = $input['userid'];
$userlevel = $input['userlevel'];
$password = $input['password'];
$password_2 = $input['password_2'];
// Jika password tidak kosong - Lakukan Full Update
if ( $password != '' || $password_2 != '') {
// Cek Password Apakah Sama
if ($password != $password_2) {
return $this->response->setJSON(['message'=> 'Password not the same']);
}
// Hash Password
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$sqlUpdate ="
UPDATE gdc_cmod.dbo.USERS
SET USERLEVEL = ?,
PASSWORD = ?
WHERE USERID = ?
";
$fullUpdate = true;
// Jika password kosong - Lakukan Partial Update Saja
} else {
$sqlUpdate ="
UPDATE gdc_cmod.dbo.USERS
SET USERLEVEL = ?
WHERE USERID = ?
";
$fullUpdate = false;
}
// Insert
$this->db->transBegin();
try {
if ($fullUpdate) {
$this->db->query($sqlUpdate, [$userlevel, $hashedPassword, $userid]);
} else {
$this->db->query($sqlUpdate, [$userlevel, $userid]);
}
$this->db->transCommit();
} catch (\Throwable $e) {
// Kalau ada error, rollback semua perubahan
$this->db->transRollback();
// (Opsional) tampilkan atau log error
return $this->response->setJSON(['message'=> 'Terjadi kesalahan pada server.']);
}
return $this->response->setJSON(['message'=> 'User '.$userid.' Berhasil Diupdate!']);
}
}

View File

@ -42,8 +42,6 @@
<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>
<button class="btn btn-sm btn-warning px-2 py-1" onclick=""><h6 class="p-0 m-0">Un-Coll.</h6></button>
<button class="btn btn-sm btn-warning px-2 py-1" onclick=""><h6 class="p-0 m-0">Un-Recv.</h6></button>
</td>
</tr>
@ -60,18 +58,18 @@
</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 == 1">
<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 == 0">
<button class="btn btn-sm btn-warning px-2 py-1" @click="uncollect(sample.sampcode, item.accessnumber)">
<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-warning px-2 py-1" @click="unreceive(sample.sampcode, item.accessnumber)">
<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>

View File

@ -1,57 +1,6 @@
<!DOCTYPE html>
<html lang="en" data-theme="corporate">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">| Admin 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">Administrator</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div>
<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 class="menu-title px-4 py-2">Account</li>
<li><a class="active:bg-primary" href="<?=base_url('v2/admin/profile') ?>"><i class="fa fa-user-circle mr-2"></i> Profile</a></li>
<li><a class="active:bg-primary" href="<?=base_url('v2/admin/settings') ?>"><i class="fa fa-cog mr-2"></i> Settings</a></li>
<li><a class="active:bg-primary" href="<?=base_url('v2/admin/user') ?>"><i class="fa fa-users mr-2"></i> User Management</a></li>
<li class="divider my-1"></li>
<li><a href="<?=base_url('v2/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->extend('v2/admin/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">
@ -104,7 +53,7 @@
<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-ghost text-base-content/70" @click='reset()'><i class='fa fa-sync-alt'></i> Reset</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>
@ -180,12 +129,9 @@
<?php echo $this->include('v2/admin/dialog_unval'); ?>
</main>
<?= $this->endSection(); ?>
<footer class='bg-base-100 p-1'>&copy; 2025 - 5Panda</footer>
<script>
window.BASEURL = "<?=base_url();?>";
</script>
<?= $this->section('script') ?>
<script type="module">
import Alpine from '<?=base_url("js/app.js");?>';
@ -231,7 +177,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/request?${param}`, {
fetch(`${BASEURL}/api/requests?${param}`, {
method: 'GET',
headers: {'Content-Type': 'application/json'},
}).then(res => res.json()).then(data => {
@ -304,7 +250,7 @@
fetchItem(accessnumber){
this.item = [];
fetch(`${BASEURL}/api/sample/${accessnumber}`, { method: 'GET', headers: {'Content-Type': 'application/json'}})
fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: {'Content-Type': 'application/json'}})
.then(res => res.json()).then(data => {
this.item = data.data ?? {};
if (!Array.isArray(this.item.samples)) this.item.samples = [];
@ -312,7 +258,7 @@
},
collect(sampcode, accessnumber) {
fetch(`${BASEURL}/api/sample/collect/${accessnumber}`, {
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({samplenumber: sampcode, userid: '<?= session('userid'); ?>'})
})
@ -322,7 +268,8 @@
},
uncollect(sampcode, accessnumber) {
fetch(`${BASEURL}/api/sample/collect/${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'); ?>'})
})
@ -332,7 +279,8 @@
},
unreceive(sampcode, accessnumber) {
fetch(`${BASEURL}/api/sample/unreceive/${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'); ?>'})
})
@ -385,7 +333,7 @@
this.isDialogValOpen = false;
},
validate(accessnumber, userid) {
fetch(`${BASEURL}/api/request/validate/${accessnumber}`, {
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ userid:`${userid}` })
@ -409,7 +357,7 @@
},
unvalidate(accessnumber, userid) {
if(!confirm(`Unvalidate request ${accessnumber}?`)) { return ;}
fetch(`${BASEURL}/api/request/validate/${accessnumber}`, {
fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, {
method: "DELETE",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ userid:`${userid}`, comment: this.unvalReason.trim() })
@ -428,5 +376,4 @@
Alpine.start();
</script>
</body>
</html>
<?= $this->endSection(); ?>

View File

@ -0,0 +1,63 @@
<!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">| Admin 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">Administrator</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div>
<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 class="menu-title px-4 py-2">Account</li>
<li><a class="active:bg-primary" href="<?=base_url('v2/admin') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
<li><a class="active:bg-primary" href="<?=base_url('v2/admin/users') ?>"><i class="fa fa-users mr-2"></i> Users </a></li>
<li class="divider my-1"></li>
<li><a href="<?=base_url('v2/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'>&copy; <?=date('Y');?> - 5Panda</footer>
<script>
window.BASEURL = "<?=base_url("v2/admin");?>";
</script>
<?=$this->renderSection('script');?>
</body>
</html>

View File

@ -1,157 +1,149 @@
<!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 - Users</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;
}
</style>
</head>
<body class="bg-base-200 min-h-screen flex flex-col" x-data="users">
<?= $this->extend('v2/admin/main'); ?>
<nav class="navbar bg-base-100 shadow-md px-6 z-20">
<div class='flex-1'>
<a href="<?=base_url('v2/admin');?>" class='text-xl text-primary font-bold tracking-wide flex items-center gap-2 hover:opacity-80 transition-opacity'>
<i class="fa fa-cube"></i> CMOD <span class="text-base-content/40 font-light text-sm hidden sm:inline-block">| Admin 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">Administrator</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div>
<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 class="menu-title px-4 py-2">Account</li>
<li><a href="<?=base_url('v2/admin');?>"><i class="fa fa-home mr-2"></i> Dashboard</a></li>
<li><a class="active"><i class="fa fa-users mr-2"></i> Users</a></li>
<li class="divider my-1"></li>
<li><a href="<?=base_url('v2/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>
<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="openModal('create')">
<i class="fa fa-plus"></i> Add User
</button>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra w-full" x-show="list.length > 0">
<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">
<?= $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">
<table class="table table-zebra w-full" x-show="list.length > 0">
<thead>
<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="openModal('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>
<th>User ID</th>
<th>Role/Level</th>
<th class="text-right">Actions</th>
</tr>
</template>
</tbody>
</table>
<div x-show="list.length === 0" class="text-center py-10 opacity-50">
<i class="fa fa-spinner fa-spin text-4xl mb-2"></i>
<p>Loading users...</p>
</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>
<div x-show="list.length === 0" class="text-center py-10 opacity-50">
<i class="fa fa-spinner fa-spin text-4xl mb-2"></i>
<p>Loading users...</p>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- User Modal -->
<dialog id="user_modal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" @click="closeModal()"></button>
</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>
<h3 class="font-bold text-lg mb-4" x-text="mode === 'create' ? 'Add New User' : 'Edit User ' + form.userid"></h3>
<div class="flex flex-col gap-3">
<div class="form-control">
<label class="label">User ID (Username)</label>
<input type="text" class="input input-bordered" x-model="form.userid" :disabled="mode === 'edit'" placeholder="Enter User ID" />
</div>
<div class="form-control">
<label class="label">Level</label>
<select class="select select-bordered" x-model="form.userlevel">
<option value="" disabled>Select Level</option>
<option value="1">1 - Administrator</option>
<option value="2">2 - Analyst</option>
<option value="3">3 - Phlebotomist</option>
<option value="4">4 - Customer Service</option>
</select>
</div>
</dialog>
</div>
<?= $this->endSection(); ?>
<div class="form-control">
<label class="label">Password <span class="text-xs opacity-50" x-show="mode === 'edit'">(Leave blank to keep current)</span></label>
<input type="password" class="input input-bordered" x-model="form.password" placeholder="******" />
</div>
<div class="form-control">
<label class="label">Confirm Password</label>
<input type="password" class="input input-bordered" x-model="form.password_2" placeholder="******" />
</div>
<div x-show="errorMsg" class="alert alert-error text-sm py-2 rounded mt-2">
<i class="fa fa-exclamation-circle"></i> <span x-text="errorMsg"></span>
</div>
</div>
<div class="modal-action">
<button class="btn" @click="closeModal()">Cancel</button>
<button class="btn btn-primary" @click="saveUser()" :disabled="isLoading">
<span x-show="isLoading" class="loading loading-spinner loading-xs"></span>
Save
</button>
</div>
</div>
</dialog>
<script>
window.BASEURL = "<?=base_url();?>";
</script>
<?= $this->section('script') ?>
<script type="module">
import Alpine from '<?=base_url("js/app.js");?>';
@ -173,7 +165,7 @@
},
fetchUsers() {
fetch(`${BASEURL}/v2/admin/api/users`)
fetch(`${BASEURL}/api/users`)
.then(res => res.json())
.then(data => {
this.list = data.data ?? [];
@ -181,7 +173,7 @@
},
getRoleName(level) {
const map = { 1: 'Administrator', 2: 'Analyst', 3: 'Phlebotomist', 4: 'Customer Service' };
const map = { 1: 'Administrator', 2: 'Lab', 3: 'Phlebotomist', 4: 'Customer Service' };
return map[level] || 'Unknown (' + level + ')';
},
@ -190,10 +182,14 @@
return map[level] || 'badge-ghost';
},
openModal(mode, user = null) {
this.mode = mode;
/*
User Modal
*/
openUserModal(targetMode, user = null) {
this.mode = targetMode;
this.errorMsg = '';
if (mode === 'edit' && user) {
if (targetMode === 'edit' && user) {
this.form = {
userid: user.USERID,
userlevel: user.USERLEVEL,
@ -210,55 +206,40 @@
document.getElementById('user_modal').close();
},
saveUser() {
async saveUser() {
this.errorMsg = '';
this.isLoading = true;
const endpoint = this.mode === 'create'
? `${BASEURL}/v2/admin/api/users`
: `${BASEURL}/v2/admin/api/users/update`;
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
})
.then(async res => {
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;
})
.then(() => {
} catch (err) {
this.errorMsg = err.message;
} finally {
this.isLoading = false;
this.closeModal();
this.fetchUsers();
// Optional: Show success toast
})
.catch(err => {
this.errorMsg = err.message;
})
.finally(() => {
this.isLoading = false;
});
}
},
deleteUser(userid) {
if (!confirm(`Are you sure you want to delete user ${userid}?`)) return;
fetch(`${BASEURL}/v2/admin/api/users/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid })
})
.then(async res => {
if (!res.ok) throw new Error('Error deleting user');
this.fetchUsers();
})
.catch(err => alert(err.message));
}
}));
});
Alpine.start();
</script>
</body>
</html>
<?= $this->endSection(); ?>

View 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>

View 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>

View 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>

379
app/Views/v2/lab/index.php Normal file
View File

@ -0,0 +1,379 @@
<?= $this->extend('v2/lab/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="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: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.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>
<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('v2/admin/dialog_sample'); ?>
<?php echo $this->include('v2/admin/dialog_val'); ?>
<?php echo $this->include('v2/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: [],
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 = "2025-03-03";
this.filter.date2 = "2025-03-03";
//this.filter.date1 = this.today;
//this.filter.date2 = this.today;
//this.fetchList();
},
fetchList(){
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;
});
});
},
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,
openSampleDialog (accessnumber) {
this.isDialogSampleOpen = true;
this.fetchItem(accessnumber)
},
closeSampleDialog () {
this.isDialogSampleOpen = false;
},
fetchItem(accessnumber){
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 = [];
});
},
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/v2/lab/main.php Normal file
View 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">| Admin 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">Lab User</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost avatar placeholder px-2">
<span class="text-xl"><i class="fa fa-user"></i></span>
</div>
<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 class="menu-title px-4 py-2">Account</li>
<li><a class="active:bg-primary" href="<?=base_url('v2/lab') ?>"><i class="fa fa-chart-bar mr-2"></i> Dashboard</a></li>
<li class="divider my-1"></li>
<li><a href="<?=base_url('v2/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'>&copy; <?=date('Y');?> - 5Panda</footer>
<script>
window.BASEURL = "<?=base_url("v2/lab");?>";
</script>
<?=$this->renderSection('script');?>
</body>
</html>