tinyqc/app/Views/control/index.php

243 lines
9.8 KiB
PHP
Raw Normal View History

<?= $this->extend("layout/main_layout"); ?>
<?= $this->section("content") ?>
<main x-data="controlIndex()">
<!-- Page Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-slate-800">Control Dictionary</h1>
<p class="text-sm text-slate-500 mt-1">Manage control materials and lot numbers</p>
</div>
<button @click="showForm()" class="btn btn-primary">
<i class="fa-solid fa-plus mr-2"></i>Add Control
</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>
<!-- Search Card -->
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-4 mb-6">
<div class="flex gap-3">
<div class="flex-1 relative">
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input type="text" x-model="keyword" @keyup.enter="fetchList()" class="w-full pl-10 pr-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Search controls...">
</div>
<button @click="fetchList()" class="btn btn-primary">
<i class="fa-solid fa-magnifying-glass mr-2"></i>Search
</button>
</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 controls...</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-sliders text-slate-400 text-xl"></i>
</div>
<p class="text-slate-500 text-sm">No controls 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">Lot</th>
<th class="py-3 px-5 font-semibold">Department</th>
<th class="py-3 px-5 font-semibold">Expiry Date</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.control_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-slate-600">
<span class="font-mono text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded" x-text="item.lot || '-'"></span>
</td>
<td class="py-3 px-5 text-slate-600" x-text="item.dept_name || '-'"></td>
<td class="py-3 px-5 text-slate-600" x-text="item.expdate ? new Date(item.expdate).toLocaleDateString() : '-'"></td>
<td class="py-3 px-5 text-right">
<button @click="showForm(item.control_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.control_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('control/dialog_form'); ?>
</main>
<?= $this->endSection(); ?>
<?= $this->section("script") ?>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data("controlIndex", () => ({
loading: false,
showModal: false,
list: [],
form: {},
errors: {},
error: '',
keyword: '',
depts: <?= json_encode($depts ?? []) ?>,
tests: <?= json_encode($tests ?? []) ?>,
init() {
this.fetchList();
},
async fetchList() {
this.loading = true;
this.error = '';
try {
const res = await fetch(`${window.BASEURL}/api/control`);
const data = await res.json();
if (data.status === 'success') {
this.list = data.data || [];
} else {
this.error = data.message || 'Failed to load controls';
}
} catch (err) {
console.error(err);
this.error = 'Network error. Please try again.';
} finally {
this.loading = false;
}
},
async showForm(id = null) {
this.errors = {};
if (id) {
const item = this.list.find(x => x.control_id === id);
if (item) {
this.form = {
control_id: item.control_id,
dept_ref_id: item.dept_ref_id,
name: item.name,
lot: item.lot || '',
producer: item.producer || '',
expdate: item.expdate || '',
test_ids: []
};
// Fetch assigned tests
try {
const res = await fetch(`${window.BASEURL}/api/control/${id}/tests`);
const data = await res.json();
if (data.status === 'success') {
this.form.test_ids = data.data || [];
}
} catch (err) {
console.error('Failed to fetch assigned tests', err);
}
}
} else {
this.form = {
dept_ref_id: '',
name: '',
lot: '',
producer: '',
expdate: '',
test_ids: []
};
}
this.showModal = true;
},
closeModal() {
this.showModal = false;
this.errors = {};
this.form = {};
},
validate() {
this.errors = {};
if (!this.form.dept_ref_id) this.errors.dept_ref_id = 'Department is required';
if (!this.form.name) this.errors.name = 'Control name is required';
return Object.keys(this.errors).length === 0;
},
async save() {
if (!this.validate()) return;
this.loading = true;
try {
const url = this.form.control_id
? `${window.BASEURL}/api/control/${this.form.control_id}`
: `${window.BASEURL}/api/control`;
const res = await fetch(url, {
method: this.form.control_id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
const data = await res.json();
if (data.status === 'success') {
this.closeModal();
this.fetchList();
} else {
this.error = data.message || 'Failed to save control';
}
} 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 control?')) return;
this.loading = true;
try {
const res = await fetch(`${window.BASEURL}/api/control/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.status === 'success') {
this.fetchList();
} else {
this.error = data.message || 'Failed to delete control';
}
} catch (err) {
console.error(err);
this.error = 'Network error. Please try again.';
} finally {
this.loading = false;
}
}
}));
});
</script>
<?= $this->endSection(); ?>