prework
This commit is contained in:
parent
315e2ce400
commit
d3703ab653
@ -73,6 +73,13 @@ $routes->group('v2', function($routes) {
|
||||
$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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -18,9 +18,9 @@ class V2 extends BaseController {
|
||||
case 1:
|
||||
return redirect()->to('v2/admin');
|
||||
case 2:
|
||||
return redirect()->to('v2/dokter');
|
||||
return redirect()->to('v2/analyst');
|
||||
case 3:
|
||||
return redirect()->to('v2/analis');
|
||||
return redirect()->to('v2/phlebotomist');
|
||||
case 4:
|
||||
return redirect()->to('v2/cs');
|
||||
default:
|
||||
@ -52,10 +52,10 @@ class V2 extends BaseController {
|
||||
$role = 'admin';
|
||||
break;
|
||||
case 2:
|
||||
$role = 'doctor';
|
||||
$role = 'analyst';
|
||||
break;
|
||||
case 3:
|
||||
$role = 'analyst';
|
||||
$role = 'phlebotomist';
|
||||
break;
|
||||
case 4:
|
||||
$role = 'cs';
|
||||
@ -78,9 +78,9 @@ class V2 extends BaseController {
|
||||
case 1:
|
||||
return redirect()->to('v2/admin');
|
||||
case 2:
|
||||
return redirect()->to('v2/doctor');
|
||||
case 3:
|
||||
return redirect()->to('v2/analyst');
|
||||
case 3:
|
||||
return redirect()->to('v2/phlebotomist');
|
||||
case 4:
|
||||
return redirect()->to('v2/cs');
|
||||
default:
|
||||
|
||||
@ -2,9 +2,140 @@
|
||||
namespace App\Controllers\V2;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
class Admin extends BaseController {
|
||||
public function index() {
|
||||
return view('v2/admin/index');
|
||||
}
|
||||
use ResponseTrait;
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,8 +42,9 @@
|
||||
</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"><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/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>
|
||||
|
||||
264
app/Views/v2/admin/users.php
Normal file
264
app/Views/v2/admin/users.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user