- Add BaseModel with automatic camel/snake case conversion - Add stringcase_helper with camel_to_snake(), snake_to_camel() functions - Update all models to extend BaseModel for consistent data handling - Update API controllers with standardized JSON response format - Remove legacy v1 PHP application directory - Consolidate documentation into AGENTS.md, delete VIEWS_RULES.md
188 lines
7.0 KiB
PHP
188 lines
7.0 KiB
PHP
<?= $this->extend("layout/main_layout"); ?>
|
|
<?= $this->section("content") ?>
|
|
<main x-data="deptIndex()">
|
|
<!-- Page Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-slate-800">Department Dictionary</h1>
|
|
<p class="text-sm text-slate-500 mt-1">Manage department entries</p>
|
|
</div>
|
|
<button @click="showForm()" class="btn btn-primary">
|
|
<i class="fa-solid fa-plus mr-2"></i>Add Department
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error Alert -->
|
|
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fa-solid fa-circle-exclamation"></i>
|
|
<span x-text="error"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Table Card -->
|
|
<div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden">
|
|
<!-- Loading State -->
|
|
<template x-if="loading">
|
|
<div class="p-12 text-center">
|
|
<div class="w-16 h-16 rounded-full bg-blue-50 flex items-center justify-center mx-auto mb-4">
|
|
<i class="fa-solid fa-spinner fa-spin text-blue-500 text-xl"></i>
|
|
</div>
|
|
<p class="text-slate-500 text-sm">Loading departments...</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Empty State -->
|
|
<template x-if="!loading && (!list || list.length === 0)">
|
|
<div class="flex-1 flex items-center justify-center p-8">
|
|
<div class="text-center">
|
|
<div class="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
|
|
<i class="fa-solid fa-inbox text-slate-400"></i>
|
|
</div>
|
|
<p class="text-slate-500 text-sm">No departments found</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Data Table -->
|
|
<template x-if="!loading && list && list.length > 0">
|
|
<table class="w-full text-sm text-left">
|
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
|
|
<tr>
|
|
<th class="py-3 px-5 font-semibold">#</th>
|
|
<th class="py-3 px-5 font-semibold">Name</th>
|
|
<th class="py-3 px-5 font-semibold text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
<template x-for="(item, index) in list" :key="item.dept_id">
|
|
<tr class="hover:bg-slate-50/50 transition-colors">
|
|
<td class="py-3 px-5" x-text="index + 1"></td>
|
|
<td class="py-3 px-5 font-medium text-slate-800" x-text="item.name"></td>
|
|
<td class="py-3 px-5 text-right">
|
|
<button @click="showForm(item.dept_id)" class="text-blue-600 hover:text-blue-800 mr-3">
|
|
<i class="fa-solid fa-pen-to-square"></i>
|
|
</button>
|
|
<button @click="deleteItem(item.dept_id)" class="text-red-600 hover:text-red-800">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Dialog Include -->
|
|
<?= $this->include('dept/dialog_form'); ?>
|
|
</main>
|
|
<?= $this->endSection(); ?>
|
|
|
|
<?= $this->section("script") ?>
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data("deptIndex", () => ({
|
|
loading: false,
|
|
showModal: false,
|
|
list: [],
|
|
form: {},
|
|
errors: {},
|
|
error: '',
|
|
|
|
init() {
|
|
this.fetchList();
|
|
},
|
|
|
|
async fetchList() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
try {
|
|
const res = await fetch(`${window.BASEURL}/api/dept`);
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
this.list = data.data || [];
|
|
} else {
|
|
this.error = data.message || 'Failed to load departments';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
this.error = 'Network error. Please try again.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
showForm(id = null) {
|
|
this.form = id ? JSON.parse(JSON.stringify(this.list.find(x => x.dept_id === id))) : {};
|
|
this.errors = {};
|
|
this.showModal = true;
|
|
},
|
|
|
|
closeModal() {
|
|
this.showModal = false;
|
|
this.errors = {};
|
|
this.form = {};
|
|
},
|
|
|
|
validate() {
|
|
this.errors = {};
|
|
if (!this.form.name) {
|
|
this.errors.name = 'Department name is required';
|
|
}
|
|
return Object.keys(this.errors).length === 0;
|
|
},
|
|
|
|
async save() {
|
|
if (!this.validate()) return;
|
|
this.loading = true;
|
|
try {
|
|
const url = this.form.dept_id
|
|
? `${window.BASEURL}/api/dept/${this.form.dept_id}`
|
|
: `${window.BASEURL}/api/dept`;
|
|
|
|
const res = await fetch(url, {
|
|
method: this.form.dept_id ? 'PATCH' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: this.form.name })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.closeModal();
|
|
this.fetchList();
|
|
} else {
|
|
this.error = data.message || 'Failed to save department';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
this.error = 'Network error. Please try again.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async deleteItem(id) {
|
|
if (!confirm('Are you sure you want to delete this department?')) return;
|
|
this.loading = true;
|
|
try {
|
|
const res = await fetch(`${window.BASEURL}/api/dept/${id}`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
this.fetchList();
|
|
} else {
|
|
this.error = data.message || 'Failed to delete department';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
this.error = 'Network error. Please try again.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
<?= $this->endSection(); ?>
|
|
|