gdc_cmod/app/Views/v2/admin/users.php
2025-12-08 19:49:35 +07:00

265 lines
8.6 KiB
PHP

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