gdc_cmod/app/Views/v2/admin/users.php
2025-12-09 14:10:43 +07:00

246 lines
8.1 KiB
PHP

<?= $this->extend('v2/admin/main'); ?>
<?= $this->section('content') ?>
<div x-data="users" class="contents">
<main class="p-4 flex-1 flex flex-col gap-2 max-w-6xl w-full mx-auto">
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body p-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold flex items-center gap-2 text-base-content">
<i class="fa fa-users text-primary"></i> User Management
</h2>
<button class="btn btn-primary btn-sm" @click="openUserModal('create')">
<i class="fa fa-plus"></i> Add User
</button>
</div>
<div class="overflow-x-auto">
<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="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>
</main>
<!-- User Modal -->
<dialog id="user_modal" class="modal">
<div class="modal-box p-0 overflow-hidden w-11/12 max-w-lg bg-base-100 shadow-2xl">
<div class="p-6 flex flex-col gap-4">
<div class="alert alert-info shadow-sm py-2 text-sm" x-show="mode === 'edit'">
<i class="fa fa-info-circle"></i> Editing user: <span class="font-bold font-mono" x-text="form.userid"></span>
</div>
<!-- User ID & Level -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="form-control w-full">
<label class="label">
<span class="label-text font-medium text-base-content/70">User ID</span>
</label>
<label class="input input-bordered flex items-center gap-2 focus-within:input-primary transition-all">
<i class="fa fa-id-badge text-base-content/40"></i>
<input type="text" class="grow font-mono" x-model="form.userid" :disabled="mode === 'edit'" placeholder="e.g. USER001" />
</label>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text font-medium text-base-content/70">Role / Level</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-all" x-model="form.userlevel">
<option value="" disabled>Select Level</option>
<option value="1">Admin</option>
<option value="2">Lab</option>
<option value="3">Phlebotomist</option>
<option value="4">Customer Service</option>
</select>
</div>
</div>
<div class="divider text-xs text-base-content/30 my-0">Security</div>
<!-- Passwords -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="form-control w-full">
<label class="label">
<span class="label-text font-medium text-base-content/70">Password</span>
<span class="label-text-alt text-xs opacity-50" x-show="mode === 'edit'">(Optional)</span>
</label>
<label class="input input-bordered flex items-center gap-2 focus-within:input-primary transition-all">
<i class="fa fa-lock text-base-content/40"></i>
<input type="password" class="grow" x-model="form.password" placeholder="••••••" />
</label>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text font-medium text-base-content/70">Confirm</span>
</label>
<label class="input input-bordered flex items-center gap-2 focus-within:input-primary transition-all">
<i class="fa fa-lock text-base-content/40"></i>
<input type="password" class="grow" x-model="form.password_2" placeholder="••••••" />
</label>
</div>
</div>
<!-- Error Message -->
<div x-show="errorMsg"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
class="alert alert-error text-sm shadow-md">
<i class="fa fa-exclamation-triangle"></i>
<span x-text="errorMsg"></span>
</div>
</div>
<!-- Actions -->
<div class="modal-action bg-base-200/50 p-6 m-0 flex justify-between items-center border-t border-base-200">
<button class="btn btn-ghost hover:bg-base-200 text-base-content/70" @click="closeModal()">
Cancel
</button>
<button class="btn btn-primary px-8 shadow-lg shadow-primary/30 min-w-[120px]" @click="saveUser()" :disabled="isLoading">
<span x-show="isLoading" class="loading loading-spinner loading-xs"></span>
<span x-show="!isLoading" x-text="mode === 'create' ? 'Create User' : 'Save Changes'"></span>
<i x-show="!isLoading" class="fa fa-save ml-2"></i>
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button @click="closeModal()">close</button>
</form>
</dialog>
</div>
<?= $this->endSection(); ?>
<?= $this->section('script') ?>
<script type="module">
import Alpine from '<?=base_url("js/app.js");?>';
document.addEventListener('alpine:init', () => {
Alpine.data("users", () => ({
list: [],
mode: 'create', // create | edit
isLoading: false,
errorMsg: '',
form: {
userid: '',
userlevel: '',
password: '',
password_2: ''
},
init() {
this.fetchUsers();
},
fetchUsers() {
fetch(`${BASEURL}/api/users`)
.then(res => res.json())
.then(data => {
this.list = data.data ?? [];
});
},
getRoleName(level) {
const map = { 1: 'Administrator', 2: 'Lab', 3: 'Phlebotomist', 4: 'Customer Service' };
return map[level] || 'Unknown (' + level + ')';
},
getRoleClass(level) {
const map = { 1: 'badge-primary', 2: 'badge-secondary', 3: 'badge-accent', 4: 'badge-neutral' };
return map[level] || 'badge-ghost';
},
/*
User Modal
*/
openUserModal(targetMode, user = null) {
this.mode = targetMode;
this.errorMsg = '';
if (targetMode === 'edit' && user) {
this.form = {
userid: user.USERID,
userlevel: user.USERLEVEL,
password: '',
password_2: ''
};
} else {
this.form = { userid: '', userlevel: '', password: '', password_2: '' };
}
document.getElementById('user_modal').showModal();
},
closeModal() {
document.getElementById('user_modal').close();
},
async saveUser() {
this.errorMsg = '';
this.isLoading = true;
try {
let res;
if(this.mode == 'create') {
res = await fetch(`${BASEURL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
} else {
res = await fetch(`${BASEURL}/api/users/${this.form.userid}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
}
const data = await res.json();
if (!res.ok) throw new Error(data.messages?.error || data.message || 'Error saving user');
return data;
} catch (err) {
this.errorMsg = err.message;
} finally {
this.isLoading = false;
this.closeModal();
this.fetchUsers();
}
},
}));
});
Alpine.start();
</script>
<?= $this->endSection(); ?>