312 lines
9.9 KiB
PHP
312 lines
9.9 KiB
PHP
<?= $this->extend('shared/layout_dashboard'); ?>
|
|
|
|
<?= $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">
|
|
<template x-if="isLoading">
|
|
<table class="table table-zebra w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>
|
|
<div class="skeleton h-4 w-24"></div>
|
|
</th>
|
|
<th>
|
|
<div class="skeleton h-4 w-24"></div>
|
|
</th>
|
|
<th class="text-right">
|
|
<div class="skeleton h-4 w-16 ml-auto"></div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="i in 5" :key="i">
|
|
<tr>
|
|
<td>
|
|
<div class="skeleton h-4 w-20"></div>
|
|
</td>
|
|
<td>
|
|
<div class="skeleton h-4 w-24"></div>
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="skeleton h-4 w-16 ml-auto"></div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
<template x-if="!isLoading && list.length">
|
|
<table class="table table-zebra w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>User ID</th>
|
|
<th>Username</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 class="font-bold" x-text="user.USERNAME"></td>
|
|
<td>
|
|
<span class="badge" :class="getRoleClass(user.USERROLEID)"
|
|
x-text="getRoleName(user.USERROLEID)"></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>
|
|
</template>
|
|
<template x-if="!isLoading && !list.length">
|
|
<div class="text-center py-10">
|
|
<i class="fa fa-inbox text-4xl mb-2 opacity-50"></i>
|
|
<p>No users found</p>
|
|
</div>
|
|
</template>
|
|
</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.userroleid">
|
|
<option value="" disabled>Select Level</option>
|
|
<?php foreach (ROLE_NAMES as $key => $role): ?>
|
|
<option value="<?= $key ?>"><?= $role ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Username -->
|
|
<div class="form-control w-full">
|
|
<label class="label">
|
|
<span class="label-text font-medium text-base-content/70">Username</span>
|
|
</label>
|
|
<label class="input input-bordered w-full flex items-center gap-2 focus-within:input-primary transition-all">
|
|
<i class="fa fa-user text-base-content/40"></i>
|
|
<input type="text" class="grow font-mono" x-model="form.username" placeholder="e.g. john.doe" />
|
|
</label>
|
|
</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',
|
|
isLoading: false,
|
|
errorMsg: '',
|
|
form: {
|
|
userid: '',
|
|
username: '',
|
|
userroleid: '',
|
|
password: '',
|
|
password_2: ''
|
|
},
|
|
|
|
init() {
|
|
this.fetchUsers();
|
|
},
|
|
|
|
fetchUsers() {
|
|
this.isLoading = true;
|
|
fetch(`${BASEURL}/api/users`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
this.list = data.data ?? [];
|
|
}).finally(() => {
|
|
this.isLoading = false;
|
|
});
|
|
},
|
|
|
|
getRoleName(level) {
|
|
const map = <?= json_encode(ROLE_NAMES) ?>;
|
|
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';
|
|
},
|
|
|
|
openUserModal(targetMode, user = null) {
|
|
this.mode = targetMode;
|
|
this.errorMsg = '';
|
|
|
|
if (targetMode === 'edit' && user) {
|
|
this.form = {
|
|
userid: user.USERID,
|
|
username: user.USERNAME,
|
|
userroleid: user.USERROLEID,
|
|
password: '',
|
|
password_2: ''
|
|
};
|
|
} else {
|
|
this.form = { userid: '', username: '', userroleid: '', 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();
|
|
}
|
|
},
|
|
|
|
async deleteUser(userid) {
|
|
if (!confirm(`Are you sure you want to delete user ${userid}?`)) return;
|
|
|
|
this.isLoading = true;
|
|
try {
|
|
const res = await fetch(`${BASEURL}/api/users/${userid}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.message || 'Error deleting user');
|
|
} catch (err) {
|
|
alert(err.message);
|
|
} finally {
|
|
this.isLoading = false;
|
|
this.fetchUsers();
|
|
}
|
|
},
|
|
|
|
}));
|
|
});
|
|
|
|
Alpine.start();
|
|
</script>
|
|
<?= $this->endSection(); ?>
|