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()
|
public function getControls()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$keyword = $this->request->getGet('keyword');
|
||||||
$deptId = $this->request->getGet('dept_id');
|
$deptId = $this->request->getGet('dept_id');
|
||||||
$date = $this->request->getGet('date');
|
$date = $this->request->getGet('date');
|
||||||
|
|
||||||
if ($deptId) {
|
$controls = $this->controlModel->search($keyword, $deptId);
|
||||||
$controls = $this->controlModel->where('dept_id', $deptId)->where('deleted_at', null)->findAll();
|
|
||||||
} else {
|
// Debug logging
|
||||||
$controls = $this->controlModel->where('deleted_at', null)->findAll();
|
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
|
// Filter expired controls if date provided
|
||||||
if ($date) {
|
if ($date) {
|
||||||
@ -61,16 +61,20 @@ class EntryApiController extends BaseController
|
|||||||
'controlName' => $c['controlName'],
|
'controlName' => $c['controlName'],
|
||||||
'lot' => $c['lot'],
|
'lot' => $c['lot'],
|
||||||
'producer' => $c['producer'],
|
'producer' => $c['producer'],
|
||||||
'expDate' => $c['expDate']
|
'expDate' => $c['expDate'],
|
||||||
|
'deptName' => $c['deptName'] ?? null
|
||||||
];
|
];
|
||||||
}, $controls);
|
}, $controls);
|
||||||
|
|
||||||
|
log_message('debug', 'getControls: returning ' . count($data) . ' controls');
|
||||||
|
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'fetch success',
|
'message' => 'fetch success',
|
||||||
'data' => $data
|
'data' => $data
|
||||||
], 200);
|
], 200);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
log_message('error', 'getControls error: ' . $e->getMessage());
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,7 @@ class MasterControlsController extends BaseController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index() { $keyword = $this->request->getGet('keyword'); $deptId = $this->request->getGet('dept_id'); try { $rows = $this->model->search($keyword, $deptId);
|
||||||
$keyword = $this->request->getGet('keyword');
|
|
||||||
try {
|
|
||||||
$rows = $this->model->search($keyword);
|
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'fetch success',
|
'message' => 'fetch success',
|
||||||
|
|||||||
@ -18,10 +18,7 @@ class MasterTestsController extends BaseController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index() { $keyword = $this->request->getGet('keyword'); $deptId = $this->request->getGet('dept_id'); try { $rows = $this->model->search($keyword, $deptId);
|
||||||
$keyword = $this->request->getGet('keyword');
|
|
||||||
try {
|
|
||||||
$rows = $this->model->search($keyword);
|
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'fetch success',
|
'message' => 'fetch success',
|
||||||
|
|||||||
@ -19,14 +19,40 @@ class MasterControlsModel extends BaseModel {
|
|||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $useSoftDeletes = true;
|
protected $useSoftDeletes = true;
|
||||||
|
|
||||||
public function search($keyword = null) {
|
public function search($keyword = null, $deptId = null) {
|
||||||
if ($keyword) {
|
$builder = $this->builder();
|
||||||
return $this->groupStart()
|
$builder->select('
|
||||||
->like('control_name', $keyword)
|
master_controls.control_id as controlId,
|
||||||
->orLike('lot', $keyword)
|
master_controls.control_name as controlName,
|
||||||
->groupEnd()
|
master_controls.lot,
|
||||||
->findAll();
|
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 $useTimestamps = true;
|
||||||
protected $useSoftDeletes = true;
|
protected $useSoftDeletes = true;
|
||||||
|
|
||||||
public function search($keyword = null) {
|
public function search($keyword = null, $deptId = null) {
|
||||||
$builder = $this->builder();
|
$builder = $this->builder();
|
||||||
$builder->select('
|
$builder->select('
|
||||||
master_tests.test_id as testId,
|
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->join('master_depts', 'master_depts.dept_id = master_tests.dept_id', 'left');
|
||||||
$builder->where('master_tests.deleted_at', null);
|
$builder->where('master_tests.deleted_at', null);
|
||||||
|
|
||||||
|
if ($deptId) {
|
||||||
|
$builder->where('master_tests.dept_id', $deptId);
|
||||||
|
}
|
||||||
|
|
||||||
if ($keyword) {
|
if ($keyword) {
|
||||||
$builder->groupStart()
|
$builder->groupStart()
|
||||||
->like('master_tests.test_name', $keyword)
|
->like('master_tests.test_name', $keyword)
|
||||||
|
|||||||
@ -39,6 +39,44 @@
|
|||||||
<button @click="setYesterday()" class="btn btn-sm btn-outline">Yesterday</button>
|
<button @click="setYesterday()" class="btn btn-sm btn-outline">Yesterday</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text font-medium">Control</span>
|
<span class="label-text font-medium">Control</span>
|
||||||
@ -147,19 +185,53 @@ document.addEventListener('alpine:init', () => {
|
|||||||
saving: false,
|
saving: false,
|
||||||
resultsData: {},
|
resultsData: {},
|
||||||
commentsData: {},
|
commentsData: {},
|
||||||
|
deptId: null,
|
||||||
|
departments: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.fetchDepartments();
|
||||||
this.fetchControls();
|
this.fetchControls();
|
||||||
this.setupKeyboard();
|
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() {
|
get today() {
|
||||||
return new Date().toISOString().split('T')[0];
|
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() {
|
async fetchControls() {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ date: this.date });
|
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 response = await fetch(`${BASEURL}api/entry/controls?${params}`);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
this.controls = json.data || [];
|
this.controls = json.data || [];
|
||||||
|
|||||||
@ -49,6 +49,47 @@
|
|||||||
|
|
||||||
<div class="divider divider-horizontal mx-0 hidden sm:flex"></div>
|
<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">
|
<div class="form-control flex-1 max-w-xs">
|
||||||
<label class="label pt-0">
|
<label class="label pt-0">
|
||||||
<span class="label-text font-semibold opacity-60 uppercase text-[10px]">Select Test</span>
|
<span class="label-text font-semibold opacity-60 uppercase text-[10px]">Select Test</span>
|
||||||
@ -267,6 +308,8 @@ document.addEventListener('alpine:init', () => {
|
|||||||
commentsData: {},
|
commentsData: {},
|
||||||
originalComments: {},
|
originalComments: {},
|
||||||
month: '',
|
month: '',
|
||||||
|
deptId: null,
|
||||||
|
departments: null,
|
||||||
commentModal: {
|
commentModal: {
|
||||||
show: false,
|
show: false,
|
||||||
controlId: null,
|
controlId: null,
|
||||||
@ -278,13 +321,33 @@ document.addEventListener('alpine:init', () => {
|
|||||||
init() {
|
init() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
this.month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
|
this.month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
this.fetchDepartments();
|
||||||
this.fetchTests();
|
this.fetchTests();
|
||||||
this.setupKeyboard();
|
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() {
|
async fetchTests() {
|
||||||
try {
|
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();
|
const json = await response.json();
|
||||||
this.tests = json.data || [];
|
this.tests = json.data || [];
|
||||||
} catch (e) {
|
} 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() {
|
onMonthChange() {
|
||||||
if (this.selectedTest) {
|
if (this.selectedTest) {
|
||||||
this.fetchMonthlyData();
|
this.fetchMonthlyData();
|
||||||
|
|||||||
@ -6,6 +6,34 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<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">
|
<div class="form-control">
|
||||||
<label class="label py-1">
|
<label class="label py-1">
|
||||||
<span class="label-text-alt font-semibold text-base-content/70">Control Name</span>
|
<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"
|
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()" />
|
x-model="keyword" @keyup.enter="fetchList()" />
|
||||||
</div>
|
</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()">
|
<button class="btn btn-sm btn-neutral gap-2" @click="fetchList()">
|
||||||
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<template x-if="loading && !list">
|
<template x-if="loading && !list">
|
||||||
@ -164,6 +202,8 @@
|
|||||||
errors: {},
|
errors: {},
|
||||||
error: null,
|
error: null,
|
||||||
keyword: "",
|
keyword: "",
|
||||||
|
deptId: null,
|
||||||
|
departments: null,
|
||||||
list: null,
|
list: null,
|
||||||
form: {
|
form: {
|
||||||
controlId: null,
|
controlId: null,
|
||||||
@ -229,9 +269,62 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.fetchDepartments();
|
||||||
this.fetchList();
|
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() {
|
async fetchList() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|||||||
@ -27,6 +27,44 @@
|
|||||||
@keyup.enter="fetchList()"
|
@keyup.enter="fetchList()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<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"
|
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()"
|
@click="fetchList()"
|
||||||
@ -110,6 +148,8 @@
|
|||||||
errors: {},
|
errors: {},
|
||||||
error: null,
|
error: null,
|
||||||
keyword: "",
|
keyword: "",
|
||||||
|
deptId: null,
|
||||||
|
departments: null,
|
||||||
list: null,
|
list: null,
|
||||||
form: {
|
form: {
|
||||||
testId: null,
|
testId: null,
|
||||||
@ -123,15 +163,34 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.fetchDepartments();
|
||||||
this.fetchList();
|
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() {
|
async fetchList() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.list = null;
|
this.list = null;
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ keyword: this.keyword });
|
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}`, {
|
const response = await fetch(`${BASEURL}api/master/tests?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
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) {
|
async loadData(id) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user