feat: implement View Controls - add department filter and active status indicator

This commit is contained in:
mahdahar 2026-01-16 16:37:13 +07:00
parent a5a3857eff
commit 44795310e1
3 changed files with 36 additions and 3 deletions

View File

@ -24,8 +24,10 @@ class ControlApiController extends BaseController
public function index() public function index()
{ {
$keyword = $this->request->getGet('keyword');
$deptId = $this->request->getGet('deptId');
try { try {
$rows = $this->dictControlModel->getWithDept(); $rows = $this->dictControlModel->getWithDept($keyword, $deptId);
return $this->respond([ return $this->respond([
'status' => 'success', 'status' => 'success',
'message' => 'fetch success', 'message' => 'fetch success',

View File

@ -19,11 +19,23 @@ class DictControlModel extends BaseModel
return $this->where('dept_ref_id', $deptId)->findAll(); return $this->where('dept_ref_id', $deptId)->findAll();
} }
public function getWithDept() public function getWithDept($keyword = null, $deptId = null)
{ {
$builder = $this->db->table('dict_controls c'); $builder = $this->db->table('dict_controls c');
$builder->select('c.*, d.name as dept_name'); $builder->select('c.*, d.name as dept_name');
$builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left'); $builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left');
if ($keyword) {
$builder->groupStart();
$builder->like('c.name', $keyword);
$builder->orLike('c.lot', $keyword);
$builder->groupEnd();
}
if ($deptId) {
$builder->where('c.dept_ref_id', $deptId);
}
return $builder->get()->getResultArray(); return $builder->get()->getResultArray();
} }

View File

@ -27,6 +27,12 @@
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i> <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..."> <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> </div>
<select x-model="deptId" @change="fetchList()" class="select select-bordered select-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">
<option value="">All Departments</option>
<template x-for="dept in depts" :key="dept.dept_id">
<option :value="dept.dept_id" x-text="dept.name"></option>
</template>
</select>
<button @click="fetchList()" class="btn btn-primary"> <button @click="fetchList()" class="btn btn-primary">
<i class="fa-solid fa-magnifying-glass mr-2"></i>Search <i class="fa-solid fa-magnifying-glass mr-2"></i>Search
</button> </button>
@ -66,6 +72,7 @@
<th class="py-3 px-5 font-semibold">Name</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">Lot</th>
<th class="py-3 px-5 font-semibold">Department</th> <th class="py-3 px-5 font-semibold">Department</th>
<th class="py-3 px-5 font-semibold">Status</th>
<th class="py-3 px-5 font-semibold">Expiry Date</th> <th class="py-3 px-5 font-semibold">Expiry Date</th>
<th class="py-3 px-5 font-semibold text-right">Actions</th> <th class="py-3 px-5 font-semibold text-right">Actions</th>
</tr> </tr>
@ -79,6 +86,9 @@
<span class="font-mono text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded" x-text="item.lot || '-'"></span> <span class="font-mono text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded" x-text="item.lot || '-'"></span>
</td> </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.dept_name || '-'"></td>
<td class="py-3 px-5">
<span :class="isExpired(item.expdate) ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'" class="px-2 py-1 text-xs font-medium rounded-full" x-text="isExpired(item.expdate) ? 'Expired' : 'Active'"></span>
</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-slate-600" x-text="item.expdate ? new Date(item.expdate).toLocaleDateString() : '-'"></td>
<td class="py-3 px-5 text-right"> <td class="py-3 px-5 text-right">
<button @click="showForm(item.control_id)" class="text-blue-600 hover:text-blue-800 mr-3"> <button @click="showForm(item.control_id)" class="text-blue-600 hover:text-blue-800 mr-3">
@ -111,6 +121,7 @@ document.addEventListener('alpine:init', () => {
errors: {}, errors: {},
error: '', error: '',
keyword: '', keyword: '',
deptId: '',
depts: <?= json_encode($depts ?? []) ?>, depts: <?= json_encode($depts ?? []) ?>,
tests: <?= json_encode($tests ?? []) ?>, tests: <?= json_encode($tests ?? []) ?>,
@ -122,7 +133,10 @@ document.addEventListener('alpine:init', () => {
this.loading = true; this.loading = true;
this.error = ''; this.error = '';
try { try {
const res = await fetch(`${window.BASEURL}/api/control`); const params = new URLSearchParams();
if (this.keyword) params.append('keyword', this.keyword);
if (this.deptId) params.append('deptId', this.deptId);
const res = await fetch(`${window.BASEURL}/api/control?${params}`);
const data = await res.json(); const data = await res.json();
if (data.status === 'success') { if (data.status === 'success') {
this.list = data.data || []; this.list = data.data || [];
@ -137,6 +151,11 @@ document.addEventListener('alpine:init', () => {
} }
}, },
isExpired(expdate) {
if (!expdate) return false;
return new Date(expdate) < new Date();
},
async showForm(id = null) { async showForm(id = null) {
this.errors = {}; this.errors = {};
if (id) { if (id) {