This commit is contained in:
mahdahar 2025-12-08 19:49:35 +07:00
parent 315e2ce400
commit d3703ab653
5 changed files with 414 additions and 11 deletions

View File

@ -73,6 +73,13 @@ $routes->group('v2', function($routes) {
$routes->post('login', 'V2::login'); $routes->post('login', 'V2::login');
$routes->group('admin', ['filter' => 'role:1'], function($routes) { $routes->group('admin', ['filter' => 'role:1'], function($routes) {
$routes->get('', 'V2\Admin::index'); $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');
}); });
}); });

View File

@ -18,9 +18,9 @@ class V2 extends BaseController {
case 1: case 1:
return redirect()->to('v2/admin'); return redirect()->to('v2/admin');
case 2: case 2:
return redirect()->to('v2/dokter'); return redirect()->to('v2/analyst');
case 3: case 3:
return redirect()->to('v2/analis'); return redirect()->to('v2/phlebotomist');
case 4: case 4:
return redirect()->to('v2/cs'); return redirect()->to('v2/cs');
default: default:
@ -52,10 +52,10 @@ class V2 extends BaseController {
$role = 'admin'; $role = 'admin';
break; break;
case 2: case 2:
$role = 'doctor'; $role = 'analyst';
break; break;
case 3: case 3:
$role = 'analyst'; $role = 'phlebotomist';
break; break;
case 4: case 4:
$role = 'cs'; $role = 'cs';
@ -78,9 +78,9 @@ class V2 extends BaseController {
case 1: case 1:
return redirect()->to('v2/admin'); return redirect()->to('v2/admin');
case 2: case 2:
return redirect()->to('v2/doctor');
case 3:
return redirect()->to('v2/analyst'); return redirect()->to('v2/analyst');
case 3:
return redirect()->to('v2/phlebotomist');
case 4: case 4:
return redirect()->to('v2/cs'); return redirect()->to('v2/cs');
default: default:

View File

@ -2,9 +2,140 @@
namespace App\Controllers\V2; namespace App\Controllers\V2;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
class Admin extends BaseController { class Admin extends BaseController {
public function index() { use ResponseTrait;
return view('v2/admin/index');
} protected $db;
public function __construct() {
$this->db = \Config\Database::connect();
helper(['url', 'form', 'text']);
}
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']);
}
} }

View File

@ -42,8 +42,9 @@
</div> </div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-xl border border-base-200 mt-2"> <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 class="menu-title px-4 py-2">Account</li>
<li><a class="active:bg-primary"><i class="fa fa-user-circle mr-2"></i> Profile</a></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"><i class="fa fa-cog mr-2"></i> Settings</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 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> <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> </ul>

View File

@ -0,0 +1,264 @@
<!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">
<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">
<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>
</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>
</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>
</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>
<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>
<script type="module">
import Alpine from '<?=base_url("js/app.js");?>';
document.addEventListener('alpine:init', () => {
Alpine.data("users", () => ({
list: [],
mode: 'create', // create | edit
isLoading: false,
errorMsg: '',
form: {
userid: '',
userlevel: '',
password: '',
password_2: ''
},
init() {
this.fetchUsers();
},
fetchUsers() {
fetch(`${BASEURL}/v2/admin/api/users`)
.then(res => res.json())
.then(data => {
this.list = data.data ?? [];
});
},
getRoleName(level) {
const map = { 1: 'Administrator', 2: 'Analyst', 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';
},
openModal(mode, user = null) {
this.mode = mode;
this.errorMsg = '';
if (mode === '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();
},
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 => {
const data = await res.json();
if (!res.ok) throw new Error(data.messages?.error || data.message || 'Error saving user');
return data;
})
.then(() => {
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>