tinyqc/app/Views/dept/index.php
mahdahar 18b85815ce refactor: standardize codebase with BaseModel and new conventions
- 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
2026-01-15 10:44:09 +07:00

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(); ?>