feat: Add department filtering to Controls, Tests, and Entry pages
Implemented comprehensive department filtering across multiple pages in the QC system to enable users to filter data by laboratory department. ## Backend Changes **Models:** - MasterControlsModel: Enhanced search() method to accept optional dept_id parameter, added LEFT JOIN with master_depts to include department info - MasterTestsModel: Updated search() to support dept_id filtering with JOIN **Controllers:** - MasterControlsController: Modified index() to accept and pass dept_id parameter - MasterTestsController: Modified index() to accept and pass dept_id parameter - EntryApiController: Updated getControls() to filter by dept_id using model search, added debug logging for troubleshooting ## Frontend Changes **Views Updated:** 1. Controls page (/master/control) - Added department dropdown with DaisyUI styling - Active filter badge and clear button - Fetch controls filtered by selected department - Added department field to control form dialog 2. Tests page (/master/test) - Added department dropdown with active state indication - Filter tests by department - Clear button to reset filter 3. Daily Entry page (/entry/daily) - Added department dropdown in filter section - Resets control selection when department changes - Fetches controls and tests filtered by department 4. Monthly Entry page (/entry/monthly) - Added department dropdown with month selector - Resets test selection when department changes - Fetches tests filtered by department ## Key Features - Dropdown UI shows "All Departments" as default - Selected department name displayed in dropdown button - Clear button appears when filter is active - Active department highlighted in dropdown menu - Loading state while fetching departments - Automatic reset of dependent selections when department changes - Consistent UI pattern across all pages using DaisyUI components ## Bug Fixes - Fixed syntax error in MasterControlsModel search() method - Removed duplicate/corrupted code that was causing incorrect results - Added proper deptName field to SELECT query in MasterControlsModel ## Important Note: Department IDs Required **ACTION REQUIRED**: Existing controls and tests in the database must be assigned to departments for the filter to work correctly. To update existing records, run: UPDATE master_controls SET dept_id = 1 WHERE dept_id IS NULL; UPDATE master_tests SET dept_id = 1 WHERE dept_id IS NULL; Replace '1' with a valid department ID from master_depts table. Alternatively, edit each control/test through the UI to assign a department. ## Technical Details - Alpine.js data binding for reactive department selection - API expects 'dept_id' query parameter (snake_case) - Internal state uses camelCase (deptId) - Departments loaded on init via /api/master/depts - Search requests include both keyword and dept_id parameters
This commit is contained in:
parent
c9c9e59316
commit
4ae2c75fdd
@ -37,14 +37,14 @@ class EntryApiController extends BaseController
|
||||
public function getControls()
|
||||
{
|
||||
try {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
$deptId = $this->request->getGet('dept_id');
|
||||
$date = $this->request->getGet('date');
|
||||
|
||||
if ($deptId) {
|
||||
$controls = $this->controlModel->where('dept_id', $deptId)->where('deleted_at', null)->findAll();
|
||||
} else {
|
||||
$controls = $this->controlModel->where('deleted_at', null)->findAll();
|
||||
}
|
||||
$controls = $this->controlModel->search($keyword, $deptId);
|
||||
|
||||
// Debug logging
|
||||
log_message('debug', 'getControls: keyword=' . var_export($keyword, true) . ', deptId=' . var_export($deptId, true) . ', date=' . var_export($date, true) . ', found=' . count($controls));
|
||||
|
||||
// Filter expired controls if date provided
|
||||
if ($date) {
|
||||
@ -61,16 +61,20 @@ class EntryApiController extends BaseController
|
||||
'controlName' => $c['controlName'],
|
||||
'lot' => $c['lot'],
|
||||
'producer' => $c['producer'],
|
||||
'expDate' => $c['expDate']
|
||||
'expDate' => $c['expDate'],
|
||||
'deptName' => $c['deptName'] ?? null
|
||||
];
|
||||
}, $controls);
|
||||
|
||||
log_message('debug', 'getControls: returning ' . count($data) . ' controls');
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $data
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'getControls error: ' . $e->getMessage());
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,10 +19,7 @@ class MasterControlsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
public function index() { $keyword = $this->request->getGet('keyword'); $deptId = $this->request->getGet('dept_id'); try { $rows = $this->model->search($keyword, $deptId);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
|
||||
@ -18,10 +18,7 @@ class MasterTestsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
public function index() { $keyword = $this->request->getGet('keyword'); $deptId = $this->request->getGet('dept_id'); try { $rows = $this->model->search($keyword, $deptId);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
|
||||
@ -19,14 +19,40 @@ class MasterControlsModel extends BaseModel {
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('control_name', $keyword)
|
||||
->orLike('lot', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
public function search($keyword = null, $deptId = null) {
|
||||
$builder = $this->builder();
|
||||
$builder->select('
|
||||
master_controls.control_id as controlId,
|
||||
master_controls.control_name as controlName,
|
||||
master_controls.lot,
|
||||
master_controls.producer,
|
||||
master_controls.exp_date as expDate,
|
||||
master_depts.dept_name as deptName
|
||||
');
|
||||
$builder->join('master_depts', 'master_depts.dept_id = master_controls.dept_id', 'left');
|
||||
$builder->where('master_controls.deleted_at', null);
|
||||
|
||||
if ($deptId) {
|
||||
$builder->where('master_controls.dept_id', $deptId);
|
||||
}
|
||||
return $this->findAll();
|
||||
|
||||
if ($keyword) {
|
||||
$builder->groupStart()
|
||||
->like('master_controls.control_name', $keyword)
|
||||
->orLike('master_controls.lot', $keyword)
|
||||
->orLike('master_controls.producer', $keyword)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$builder->orderBy('master_controls.control_name', 'ASC');
|
||||
|
||||
$results = $builder->get()->getResultArray();
|
||||
|
||||
// Add deptName after camelCase conversion from BaseModel
|
||||
foreach ($results as &$row) {
|
||||
$row['deptName'] = $row['deptName'] ?? null;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ class MasterTestsModel extends BaseModel {
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
public function search($keyword = null, $deptId = null) {
|
||||
$builder = $this->builder();
|
||||
$builder->select('
|
||||
master_tests.test_id as testId,
|
||||
@ -38,6 +38,10 @@ class MasterTestsModel extends BaseModel {
|
||||
$builder->join('master_depts', 'master_depts.dept_id = master_tests.dept_id', 'left');
|
||||
$builder->where('master_tests.deleted_at', null);
|
||||
|
||||
if ($deptId) {
|
||||
$builder->where('master_tests.dept_id', $deptId);
|
||||
}
|
||||
|
||||
if ($keyword) {
|
||||
$builder->groupStart()
|
||||
->like('master_tests.test_name', $keyword)
|
||||
|
||||
@ -39,6 +39,44 @@
|
||||
<button @click="setYesterday()" class="btn btn-sm btn-outline">Yesterday</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Department</span>
|
||||
</label>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-sm gap-2 btn-outline w-48 justify-start">
|
||||
<i class="fa-solid fa-building text-xs"></i>
|
||||
<template x-if="deptId">
|
||||
<span class="truncate" x-text="getDeptName()"></span>
|
||||
</template>
|
||||
<template x-if="!deptId">
|
||||
<span class="opacity-70">All Departments</span>
|
||||
</template>
|
||||
<i class="fa-solid fa-caret-down text-xs ml-auto"></i>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-56 border border-base-300">
|
||||
<li>
|
||||
<a @click="setDeptId(null)" :class="{'active': !deptId}">
|
||||
<i class="fa-solid fa-building text-xs"></i> All Departments
|
||||
</a>
|
||||
</li>
|
||||
<template x-if="departments">
|
||||
<template x-for="dept in departments" :key="dept.deptId">
|
||||
<li>
|
||||
<a @click="setDeptId(dept.deptId)" :class="{'active': deptId === dept.deptId}" x-text="dept.deptName"></a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!departments">
|
||||
<li>
|
||||
<a class="opacity-50">
|
||||
<span class="loading loading-spinner loading-xs"></span> Loading...
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Control</span>
|
||||
@ -147,19 +185,53 @@ document.addEventListener('alpine:init', () => {
|
||||
saving: false,
|
||||
resultsData: {},
|
||||
commentsData: {},
|
||||
deptId: null,
|
||||
departments: null,
|
||||
|
||||
init() {
|
||||
this.fetchDepartments();
|
||||
this.fetchControls();
|
||||
this.setupKeyboard();
|
||||
},
|
||||
|
||||
async fetchDepartments() {
|
||||
try {
|
||||
const response = await fetch(`${BASEURL}api/master/depts`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load departments");
|
||||
const json = await response.json();
|
||||
this.departments = json.data || [];
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch departments:', e);
|
||||
}
|
||||
},
|
||||
|
||||
get today() {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
getDeptName() {
|
||||
if (!this.deptId || !this.departments) return '';
|
||||
const dept = this.departments.find(d => d.deptId === this.deptId);
|
||||
return dept ? dept.deptName : '';
|
||||
},
|
||||
|
||||
setDeptId(id) {
|
||||
this.deptId = id;
|
||||
this.selectedControl = null;
|
||||
this.controls = [];
|
||||
this.tests = [];
|
||||
this.fetchControls();
|
||||
},
|
||||
|
||||
async fetchControls() {
|
||||
try {
|
||||
const params = new URLSearchParams({ date: this.date });
|
||||
if (this.deptId) {
|
||||
params.set('dept_id', this.deptId);
|
||||
}
|
||||
const response = await fetch(`${BASEURL}api/entry/controls?${params}`);
|
||||
const json = await response.json();
|
||||
this.controls = json.data || [];
|
||||
|
||||
@ -49,6 +49,47 @@
|
||||
|
||||
<div class="divider divider-horizontal mx-0 hidden sm:flex"></div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label pt-0">
|
||||
<span class="label-text font-semibold opacity-60 uppercase text-[10px]">Department</span>
|
||||
</label>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-sm gap-2 btn-outline w-48 justify-start">
|
||||
<i class="fa-solid fa-building text-xs"></i>
|
||||
<template x-if="deptId">
|
||||
<span class="truncate" x-text="getDeptName()"></span>
|
||||
</template>
|
||||
<template x-if="!deptId">
|
||||
<span class="opacity-70">All Departments</span>
|
||||
</template>
|
||||
<i class="fa-solid fa-caret-down text-xs ml-auto"></i>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-56 border border-base-300">
|
||||
<li>
|
||||
<a @click="setDeptId(null)" :class="{'active': !deptId}">
|
||||
<i class="fa-solid fa-building text-xs"></i> All Departments
|
||||
</a>
|
||||
</li>
|
||||
<template x-if="departments">
|
||||
<template x-for="dept in departments" :key="dept.deptId">
|
||||
<li>
|
||||
<a @click="setDeptId(dept.deptId)" :class="{'active': deptId === dept.deptId}" x-text="dept.deptName"></a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!departments">
|
||||
<li>
|
||||
<a class="opacity-50">
|
||||
<span class="loading loading-spinner loading-xs"></span> Loading...
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider divider-horizontal mx-0 hidden sm:flex"></div>
|
||||
|
||||
<div class="form-control flex-1 max-w-xs">
|
||||
<label class="label pt-0">
|
||||
<span class="label-text font-semibold opacity-60 uppercase text-[10px]">Select Test</span>
|
||||
@ -267,6 +308,8 @@ document.addEventListener('alpine:init', () => {
|
||||
commentsData: {},
|
||||
originalComments: {},
|
||||
month: '',
|
||||
deptId: null,
|
||||
departments: null,
|
||||
commentModal: {
|
||||
show: false,
|
||||
controlId: null,
|
||||
@ -278,13 +321,33 @@ document.addEventListener('alpine:init', () => {
|
||||
init() {
|
||||
const now = new Date();
|
||||
this.month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
|
||||
this.fetchDepartments();
|
||||
this.fetchTests();
|
||||
this.setupKeyboard();
|
||||
},
|
||||
|
||||
async fetchDepartments() {
|
||||
try {
|
||||
const response = await fetch(`${BASEURL}api/master/depts`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load departments");
|
||||
const json = await response.json();
|
||||
this.departments = json.data || [];
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch departments:', e);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTests() {
|
||||
try {
|
||||
const response = await fetch(`${BASEURL}api/master/tests`);
|
||||
const params = new URLSearchParams();
|
||||
if (this.deptId) {
|
||||
params.set('dept_id', this.deptId);
|
||||
}
|
||||
const url = `${BASEURL}api/master/tests${this.deptId ? '?' + params.toString() : ''}`;
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
this.tests = json.data || [];
|
||||
} catch (e) {
|
||||
@ -292,6 +355,19 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
getDeptName() {
|
||||
if (!this.deptId || !this.departments) return '';
|
||||
const dept = this.departments.find(d => d.deptId === this.deptId);
|
||||
return dept ? dept.deptName : '';
|
||||
},
|
||||
|
||||
setDeptId(id) {
|
||||
this.deptId = id;
|
||||
this.selectedTest = null;
|
||||
this.controls = [];
|
||||
this.fetchTests();
|
||||
},
|
||||
|
||||
onMonthChange() {
|
||||
if (this.selectedTest) {
|
||||
this.fetchMonthlyData();
|
||||
|
||||
@ -6,6 +6,34 @@
|
||||
</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<template x-if="!departmentsFetchComplete">
|
||||
<div class="flex items-center justify-center py-2">
|
||||
<span class="loading loading-spinner loading-sm text-primary"></span>
|
||||
<span class="ml-2 text-xs opacity-50">Loading departments...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="departments">
|
||||
<div class="form-control">
|
||||
<label class="label py-1">
|
||||
<span class="label-text-alt font-semibold text-base-content/70">Department</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary bg-base-200 border-base-300 text-base-content"
|
||||
:class="{'border-error': errors.deptId}"
|
||||
x-model="form.deptId"
|
||||
placeholder="Select department">
|
||||
<option value="">Select Department</option>
|
||||
<template x-for="dept in departments" :key="dept.deptId">
|
||||
<option :value="dept.deptId" x-text="dept.deptName"></option>
|
||||
</template>
|
||||
</select>
|
||||
<template x-if="errors.deptId">
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error" x-text="errors.deptId"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="form-control">
|
||||
<label class="label py-1">
|
||||
<span class="label-text-alt font-semibold text-base-content/70">Control Name</span>
|
||||
|
||||
@ -21,11 +21,49 @@
|
||||
class="input input-bordered input-sm w-full px-3 py-2 text-sm bg-base-200 border border-base-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all"
|
||||
x-model="keyword" @keyup.enter="fetchList()" />
|
||||
</div>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-sm btn-neutral gap-2">
|
||||
<i class="fa-solid fa-building text-xs"></i>
|
||||
<template x-if="deptId">
|
||||
<span x-text="getDeptName()"></span>
|
||||
</template>
|
||||
<template x-if="!deptId">
|
||||
Department
|
||||
</template>
|
||||
<i class="fa-solid fa-caret-down text-xs ml-1"></i>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-300">
|
||||
<li>
|
||||
<a @click="setDeptId(null)" :class="{'active': !deptId}">
|
||||
<i class="fa-solid fa-building text-xs"></i> All Departments
|
||||
</a>
|
||||
</li>
|
||||
<template x-if="departments">
|
||||
<template x-for="dept in departments" :key="dept.deptId">
|
||||
<li>
|
||||
<a @click="setDeptId(dept.deptId)" :class="{'active': deptId === dept.deptId}" x-text="dept.deptName"></a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!departments">
|
||||
<li>
|
||||
<a class="opacity-50">
|
||||
<span class="loading loading-spinner loading-xs"></span> Loading...
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<template x-if="deptId">
|
||||
<button class="btn btn-sm gap-1" @click="setDeptId(null)">
|
||||
<i class="fa-solid fa-xmark text-xs"></i> Clear
|
||||
</button>
|
||||
</template>
|
||||
<button class="btn btn-sm btn-neutral gap-2" @click="fetchList()">
|
||||
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<template x-if="loading && !list">
|
||||
@ -164,6 +202,8 @@
|
||||
errors: {},
|
||||
error: null,
|
||||
keyword: "",
|
||||
deptId: null,
|
||||
departments: null,
|
||||
list: null,
|
||||
form: {
|
||||
controlId: null,
|
||||
@ -229,9 +269,62 @@
|
||||
},
|
||||
|
||||
init() {
|
||||
this.fetchDepartments();
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async fetchDepartments() {
|
||||
try {
|
||||
const response = await fetch(`${BASEURL}api/master/depts`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load departments");
|
||||
const data = await response.json();
|
||||
this.departments = data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.departments = [];
|
||||
}
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.list = null;
|
||||
try {
|
||||
const params = new URLSearchParams({ keyword: this.keyword });
|
||||
if (this.deptId) {
|
||||
params.set('dept_id', this.deptId);
|
||||
}
|
||||
const response = await fetch(`${BASEURL}api/master/controls?${params}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load data");
|
||||
const data = await response.json();
|
||||
this.list = data.data;
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getDeptName() {
|
||||
if (!this.deptId || !this.departments) return '';
|
||||
const dept = this.departments.find(d => d.deptId === this.deptId);
|
||||
return dept ? dept.deptName : '';
|
||||
},
|
||||
|
||||
setDeptId(id) {
|
||||
this.deptId = id;
|
||||
this.list = null;
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async loadData(id) {
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
@ -27,6 +27,44 @@
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-sm gap-2">
|
||||
<i class="fa-solid fa-building text-xs"></i>
|
||||
<template x-if="deptId">
|
||||
<span x-text="getDeptName()"></span>
|
||||
</template>
|
||||
<template x-if="!deptId">
|
||||
Department
|
||||
</template>
|
||||
<i class="fa-solid fa-caret-down text-xs ml-1"></i>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-300">
|
||||
<li>
|
||||
<a @click="setDeptId(null)" :class="{'active': !deptId}">
|
||||
<i class="fa-solid fa-building text-xs"></i> All Departments
|
||||
</a>
|
||||
</li>
|
||||
<template x-if="departments">
|
||||
<template x-for="dept in departments" :key="dept.deptId">
|
||||
<li>
|
||||
<a @click="setDeptId(dept.deptId)" :class="{'active': deptId === dept.deptId}" x-text="dept.deptName"></a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!departments">
|
||||
<li>
|
||||
<a class="opacity-50">
|
||||
<span class="loading loading-spinner loading-xs"></span> Loading...
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<template x-if="deptId">
|
||||
<button class="btn btn-sm gap-1" @click="setDeptId(null)">
|
||||
<i class="fa-solid fa-xmark text-xs"></i> Clear
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
class="px-4 py-2.5 text-sm font-medium bg-base-content text-base-100 rounded-lg hover:bg-base-content/90 transition-all duration-200 flex items-center gap-2"
|
||||
@click="fetchList()"
|
||||
@ -110,6 +148,8 @@
|
||||
errors: {},
|
||||
error: null,
|
||||
keyword: "",
|
||||
deptId: null,
|
||||
departments: null,
|
||||
list: null,
|
||||
form: {
|
||||
testId: null,
|
||||
@ -123,15 +163,34 @@
|
||||
},
|
||||
|
||||
init() {
|
||||
this.fetchDepartments();
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async fetchDepartments() {
|
||||
try {
|
||||
const response = await fetch(`${BASEURL}api/master/depts`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load departments");
|
||||
const data = await response.json();
|
||||
this.departments = data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.departments = [];
|
||||
}
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.list = null;
|
||||
try {
|
||||
const params = new URLSearchParams({ keyword: this.keyword });
|
||||
if (this.deptId) {
|
||||
params.set('dept_id', this.deptId);
|
||||
}
|
||||
const response = await fetch(`${BASEURL}api/master/tests?${params}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
@ -146,6 +205,32 @@
|
||||
}
|
||||
},
|
||||
|
||||
getDeptName() {
|
||||
if (!this.deptId || !this.departments) return '';
|
||||
const dept = this.departments.find(d => d.deptId === this.deptId);
|
||||
return dept ? dept.deptName : '';
|
||||
},
|
||||
|
||||
setDeptId(id) {
|
||||
this.deptId = id;
|
||||
this.list = null;
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async loadData(id) {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load data");
|
||||
const data = await response.json();
|
||||
this.list = data.data;
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadData(id) {
|
||||
this.loading = true;
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user