Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c230e79a62 | |||
| 4df29e877a | |||
| 743f31d6a0 | |||
| d7149ca322 | |||
| d2e84162cd | |||
| 2392b4ba25 | |||
| 7dbc288553 | |||
| 1440f01024 | |||
| 578b87b2d7 | |||
| d2ff21e9c4 | |||
| d787522519 | |||
| 448186bea7 | |||
| 01a3e5d5bf | |||
|
|
cf9ac38849 |
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
app/.htaccess
Normal file → Executable file
0
app/.htaccess
Normal file → Executable file
0
app/Common.php
Normal file → Executable file
0
app/Common.php
Normal file → Executable file
0
app/Config/App.php
Normal file → Executable file
0
app/Config/App.php
Normal file → Executable file
0
app/Config/Autoload.php
Normal file → Executable file
0
app/Config/Autoload.php
Normal file → Executable file
0
app/Config/Boot/development.php
Normal file → Executable file
0
app/Config/Boot/development.php
Normal file → Executable file
0
app/Config/Boot/production.php
Normal file → Executable file
0
app/Config/Boot/production.php
Normal file → Executable file
0
app/Config/Boot/testing.php
Normal file → Executable file
0
app/Config/Boot/testing.php
Normal file → Executable file
0
app/Config/CURLRequest.php
Normal file → Executable file
0
app/Config/CURLRequest.php
Normal file → Executable file
0
app/Config/Cache.php
Normal file → Executable file
0
app/Config/Cache.php
Normal file → Executable file
0
app/Config/Constants.php
Normal file → Executable file
0
app/Config/Constants.php
Normal file → Executable file
0
app/Config/ContentSecurityPolicy.php
Normal file → Executable file
0
app/Config/ContentSecurityPolicy.php
Normal file → Executable file
0
app/Config/Cookie.php
Normal file → Executable file
0
app/Config/Cookie.php
Normal file → Executable file
0
app/Config/Cors.php
Normal file → Executable file
0
app/Config/Cors.php
Normal file → Executable file
0
app/Config/Database.php
Normal file → Executable file
0
app/Config/Database.php
Normal file → Executable file
0
app/Config/DocTypes.php
Normal file → Executable file
0
app/Config/DocTypes.php
Normal file → Executable file
0
app/Config/Email.php
Normal file → Executable file
0
app/Config/Email.php
Normal file → Executable file
0
app/Config/Encryption.php
Normal file → Executable file
0
app/Config/Encryption.php
Normal file → Executable file
0
app/Config/Events.php
Normal file → Executable file
0
app/Config/Events.php
Normal file → Executable file
0
app/Config/Exceptions.php
Normal file → Executable file
0
app/Config/Exceptions.php
Normal file → Executable file
0
app/Config/Feature.php
Normal file → Executable file
0
app/Config/Feature.php
Normal file → Executable file
0
app/Config/Filters.php
Normal file → Executable file
0
app/Config/Filters.php
Normal file → Executable file
0
app/Config/ForeignCharacters.php
Normal file → Executable file
0
app/Config/ForeignCharacters.php
Normal file → Executable file
0
app/Config/Format.php
Normal file → Executable file
0
app/Config/Format.php
Normal file → Executable file
0
app/Config/Generators.php
Normal file → Executable file
0
app/Config/Generators.php
Normal file → Executable file
0
app/Config/Honeypot.php
Normal file → Executable file
0
app/Config/Honeypot.php
Normal file → Executable file
0
app/Config/Images.php
Normal file → Executable file
0
app/Config/Images.php
Normal file → Executable file
0
app/Config/Kint.php
Normal file → Executable file
0
app/Config/Kint.php
Normal file → Executable file
0
app/Config/Logger.php
Normal file → Executable file
0
app/Config/Logger.php
Normal file → Executable file
0
app/Config/Migrations.php
Normal file → Executable file
0
app/Config/Migrations.php
Normal file → Executable file
0
app/Config/Mimes.php
Normal file → Executable file
0
app/Config/Mimes.php
Normal file → Executable file
0
app/Config/Modules.php
Normal file → Executable file
0
app/Config/Modules.php
Normal file → Executable file
0
app/Config/Optimize.php
Normal file → Executable file
0
app/Config/Optimize.php
Normal file → Executable file
0
app/Config/Pager.php
Normal file → Executable file
0
app/Config/Pager.php
Normal file → Executable file
0
app/Config/Paths.php
Normal file → Executable file
0
app/Config/Paths.php
Normal file → Executable file
0
app/Config/Publisher.php
Normal file → Executable file
0
app/Config/Publisher.php
Normal file → Executable file
1
app/Config/Routes.php
Normal file → Executable file
1
app/Config/Routes.php
Normal file → Executable file
@ -24,6 +24,7 @@ $routes->get('/entry/daily', 'PageController::entryDaily', ['filter' => AuthFilt
|
|||||||
$routes->get('/entry/monthly', 'PageController::entryMonthly', ['filter' => AuthFilter::class]);
|
$routes->get('/entry/monthly', 'PageController::entryMonthly', ['filter' => AuthFilter::class]);
|
||||||
$routes->get('/report', 'PageController::report', ['filter' => AuthFilter::class]);
|
$routes->get('/report', 'PageController::report', ['filter' => AuthFilter::class]);
|
||||||
$routes->get('/report/merged', 'PageController::reportMerged', ['filter' => AuthFilter::class]);
|
$routes->get('/report/merged', 'PageController::reportMerged', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/report/custom1', 'PageController::reportCustom1', ['filter' => AuthFilter::class]);
|
||||||
|
|
||||||
$routes->group('api', ['filter' => AuthFilter::class], function ($routes) {
|
$routes->group('api', ['filter' => AuthFilter::class], function ($routes) {
|
||||||
$routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent');
|
$routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent');
|
||||||
|
|||||||
0
app/Config/Routing.php
Normal file → Executable file
0
app/Config/Routing.php
Normal file → Executable file
0
app/Config/Security.php
Normal file → Executable file
0
app/Config/Security.php
Normal file → Executable file
0
app/Config/Services.php
Normal file → Executable file
0
app/Config/Services.php
Normal file → Executable file
0
app/Config/Session.php
Normal file → Executable file
0
app/Config/Session.php
Normal file → Executable file
0
app/Config/Toolbar.php
Normal file → Executable file
0
app/Config/Toolbar.php
Normal file → Executable file
0
app/Config/UserAgents.php
Normal file → Executable file
0
app/Config/UserAgents.php
Normal file → Executable file
0
app/Config/Validation.php
Normal file → Executable file
0
app/Config/Validation.php
Normal file → Executable file
0
app/Config/View.php
Normal file → Executable file
0
app/Config/View.php
Normal file → Executable file
0
app/Controllers/Api/DashboardApiController.php
Normal file → Executable file
0
app/Controllers/Api/DashboardApiController.php
Normal file → Executable file
19
app/Controllers/Api/EntryApiController.php
Normal file → Executable file
19
app/Controllers/Api/EntryApiController.php
Normal file → Executable file
@ -195,11 +195,18 @@ class EntryApiController extends BaseController
|
|||||||
|
|
||||||
$date = $input['date'];
|
$date = $input['date'];
|
||||||
$results = $input['results'];
|
$results = $input['results'];
|
||||||
|
$deletedIds = $input['deletedIds'] ?? [];
|
||||||
$savedIds = [];
|
$savedIds = [];
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
$this->resultModel->db->transBegin();
|
$this->resultModel->db->transBegin();
|
||||||
|
|
||||||
|
// Handle deletions first
|
||||||
|
foreach ($deletedIds as $resultId) {
|
||||||
|
$this->resultModel->delete((int) $resultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save/update results
|
||||||
foreach ($results as $r) {
|
foreach ($results as $r) {
|
||||||
if (!isset($r['controlId']) || !isset($r['testId']) || !isset($r['value'])) {
|
if (!isset($r['controlId']) || !isset($r['testId']) || !isset($r['value'])) {
|
||||||
continue;
|
continue;
|
||||||
@ -228,7 +235,7 @@ class EntryApiController extends BaseController
|
|||||||
|
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Saved ' . count($savedIds) . ' results',
|
'message' => 'Saved ' . count($savedIds) . ' results' . (count($deletedIds) > 0 ? ', deleted ' . count($deletedIds) : ''),
|
||||||
'data' => ['savedIds' => $savedIds]
|
'data' => ['savedIds' => $savedIds]
|
||||||
], 200);
|
], 200);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@ -252,8 +259,8 @@ class EntryApiController extends BaseController
|
|||||||
return $this->failValidationErrors(['test_id' => 'Required', 'month' => 'Required']);
|
return $this->failValidationErrors(['test_id' => 'Required', 'month' => 'Required']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get test details
|
// Get test details with department name
|
||||||
$test = $this->testModel->find($testId);
|
$test = $this->testModel->findWithDept($testId);
|
||||||
if (!$test) {
|
if (!$test) {
|
||||||
return $this->failNotFound('Test not found');
|
return $this->failNotFound('Test not found');
|
||||||
}
|
}
|
||||||
@ -311,6 +318,7 @@ 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'],
|
||||||
'mean' => $c['mean'],
|
'mean' => $c['mean'],
|
||||||
'sd' => $c['sd'],
|
'sd' => $c['sd'],
|
||||||
'results' => $resultsArray
|
'results' => $resultsArray
|
||||||
@ -320,8 +328,11 @@ class EntryApiController extends BaseController
|
|||||||
$data = [
|
$data = [
|
||||||
'test' => [
|
'test' => [
|
||||||
'testId' => $test['testId'],
|
'testId' => $test['testId'],
|
||||||
|
'testCode' => $test['testCode'] ?? null,
|
||||||
'testName' => $test['testName'],
|
'testName' => $test['testName'],
|
||||||
'testUnit' => $test['testUnit']
|
'testUnit' => $test['testUnit'],
|
||||||
|
'testMethod' => $test['testMethod'] ?? null,
|
||||||
|
'deptName' => $test['deptName'] ?? null
|
||||||
],
|
],
|
||||||
'month' => $month,
|
'month' => $month,
|
||||||
'controls' => $controlsWithData
|
'controls' => $controlsWithData
|
||||||
|
|||||||
0
app/Controllers/Api/ReportApiController.php
Normal file → Executable file
0
app/Controllers/Api/ReportApiController.php
Normal file → Executable file
0
app/Controllers/Auth/AuthController.php
Normal file → Executable file
0
app/Controllers/Auth/AuthController.php
Normal file → Executable file
0
app/Controllers/BaseController.php
Normal file → Executable file
0
app/Controllers/BaseController.php
Normal file → Executable file
7
app/Controllers/Master/MasterControlsController.php
Normal file → Executable file
7
app/Controllers/Master/MasterControlsController.php
Normal file → Executable file
@ -19,7 +19,12 @@ class MasterControlsController extends BaseController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() { $keyword = $this->request->getGet('keyword'); $deptId = $this->request->getGet('dept_id'); try { $rows = $this->model->search($keyword, $deptId);
|
public function index() {
|
||||||
|
$keyword = $this->request->getGet('keyword');
|
||||||
|
$deptId = $this->request->getGet('dept_id');
|
||||||
|
$isActive = $this->request->getGet('is_active');
|
||||||
|
try {
|
||||||
|
$rows = $this->model->search($keyword, $deptId, $isActive);
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'fetch success',
|
'message' => 'fetch success',
|
||||||
|
|||||||
0
app/Controllers/Master/MasterDeptsController.php
Normal file → Executable file
0
app/Controllers/Master/MasterDeptsController.php
Normal file → Executable file
0
app/Controllers/Master/MasterTestsController.php
Normal file → Executable file
0
app/Controllers/Master/MasterTestsController.php
Normal file → Executable file
6
app/Controllers/PageController.php
Normal file → Executable file
6
app/Controllers/PageController.php
Normal file → Executable file
@ -47,7 +47,11 @@ class PageController extends BaseController {
|
|||||||
return view('report/view');
|
return view('report/view');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reportMerged() {
|
public function reportMerged() {
|
||||||
return view('report/merged');
|
return view('report/merged');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reportCustom1() {
|
||||||
|
return view('report/custom1');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
app/Controllers/Qc/ControlTestsController.php
Normal file → Executable file
0
app/Controllers/Qc/ControlTestsController.php
Normal file → Executable file
0
app/Controllers/Qc/ResultsController.php
Normal file → Executable file
0
app/Controllers/Qc/ResultsController.php
Normal file → Executable file
0
app/Controllers/Qc/TestCommentsController.php
Normal file → Executable file
0
app/Controllers/Qc/TestCommentsController.php
Normal file → Executable file
0
app/Database/Migrations/.gitkeep
Normal file → Executable file
0
app/Database/Migrations/.gitkeep
Normal file → Executable file
1
app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php
Normal file → Executable file
1
app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php
Normal file → Executable file
@ -27,6 +27,7 @@ class QualityControlSystem extends Migration
|
|||||||
'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||||
'producer' => ['type' => 'TEXT', 'null' => true],
|
'producer' => ['type' => 'TEXT', 'null' => true],
|
||||||
'exp_date' => ['type' => 'DATE', 'null' => true],
|
'exp_date' => ['type' => 'DATE', 'null' => true],
|
||||||
|
'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'null' => false],
|
||||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
|||||||
0
app/Database/Migrations/2026-02-09-000001_Users.php
Normal file → Executable file
0
app/Database/Migrations/2026-02-09-000001_Users.php
Normal file → Executable file
0
app/Database/Seeds/.gitkeep
Normal file → Executable file
0
app/Database/Seeds/.gitkeep
Normal file → Executable file
0
app/Database/Seeds/CmodQcSeeder.php
Normal file → Executable file
0
app/Database/Seeds/CmodQcSeeder.php
Normal file → Executable file
0
app/Filters/.gitkeep
Normal file → Executable file
0
app/Filters/.gitkeep
Normal file → Executable file
0
app/Filters/AuthFilter.php
Normal file → Executable file
0
app/Filters/AuthFilter.php
Normal file → Executable file
0
app/Helpers/.gitkeep
Normal file → Executable file
0
app/Helpers/.gitkeep
Normal file → Executable file
0
app/Helpers/stringcase_helper.php
Normal file → Executable file
0
app/Helpers/stringcase_helper.php
Normal file → Executable file
0
app/Language/.gitkeep
Normal file → Executable file
0
app/Language/.gitkeep
Normal file → Executable file
0
app/Language/en/Validation.php
Normal file → Executable file
0
app/Language/en/Validation.php
Normal file → Executable file
0
app/Libraries/.gitkeep
Normal file → Executable file
0
app/Libraries/.gitkeep
Normal file → Executable file
0
app/Models/.gitkeep
Normal file → Executable file
0
app/Models/.gitkeep
Normal file → Executable file
0
app/Models/Auth/UsersModel.php
Normal file → Executable file
0
app/Models/Auth/UsersModel.php
Normal file → Executable file
0
app/Models/BaseModel.php
Normal file → Executable file
0
app/Models/BaseModel.php
Normal file → Executable file
9
app/Models/Master/MasterControlsModel.php
Normal file → Executable file
9
app/Models/Master/MasterControlsModel.php
Normal file → Executable file
@ -12,6 +12,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
'lot',
|
'lot',
|
||||||
'producer',
|
'producer',
|
||||||
'exp_date',
|
'exp_date',
|
||||||
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
'deleted_at'
|
'deleted_at'
|
||||||
@ -19,7 +20,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $useSoftDeletes = true;
|
protected $useSoftDeletes = true;
|
||||||
|
|
||||||
public function search($keyword = null, $deptId = null) {
|
public function search($keyword = null, $deptId = null, $isActive = null) {
|
||||||
$builder = $this->builder();
|
$builder = $this->builder();
|
||||||
$builder->select('
|
$builder->select('
|
||||||
master_controls.control_id as controlId,
|
master_controls.control_id as controlId,
|
||||||
@ -27,6 +28,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
master_controls.lot,
|
master_controls.lot,
|
||||||
master_controls.producer,
|
master_controls.producer,
|
||||||
master_controls.exp_date as expDate,
|
master_controls.exp_date as expDate,
|
||||||
|
master_controls.is_active as isActive,
|
||||||
master_depts.dept_name as deptName
|
master_depts.dept_name as deptName
|
||||||
');
|
');
|
||||||
$builder->join('master_depts', 'master_depts.dept_id = master_controls.dept_id', 'left');
|
$builder->join('master_depts', 'master_depts.dept_id = master_controls.dept_id', 'left');
|
||||||
@ -36,6 +38,10 @@ class MasterControlsModel extends BaseModel {
|
|||||||
$builder->where('master_controls.dept_id', $deptId);
|
$builder->where('master_controls.dept_id', $deptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isActive !== null && $isActive !== '') {
|
||||||
|
$builder->where('master_controls.is_active', $isActive);
|
||||||
|
}
|
||||||
|
|
||||||
if ($keyword) {
|
if ($keyword) {
|
||||||
$builder->groupStart()
|
$builder->groupStart()
|
||||||
->like('master_controls.control_name', $keyword)
|
->like('master_controls.control_name', $keyword)
|
||||||
@ -48,7 +54,6 @@ class MasterControlsModel extends BaseModel {
|
|||||||
|
|
||||||
$results = $builder->get()->getResultArray();
|
$results = $builder->get()->getResultArray();
|
||||||
|
|
||||||
// Add deptName after camelCase conversion from BaseModel
|
|
||||||
foreach ($results as &$row) {
|
foreach ($results as &$row) {
|
||||||
$row['deptName'] = $row['deptName'] ?? null;
|
$row['deptName'] = $row['deptName'] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
0
app/Models/Master/MasterDeptsModel.php
Normal file → Executable file
0
app/Models/Master/MasterDeptsModel.php
Normal file → Executable file
22
app/Models/Master/MasterTestsModel.php
Normal file → Executable file
22
app/Models/Master/MasterTestsModel.php
Normal file → Executable file
@ -53,4 +53,26 @@ class MasterTestsModel extends BaseModel {
|
|||||||
|
|
||||||
return $builder->get()->getResultArray();
|
return $builder->get()->getResultArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function findWithDept($testId) {
|
||||||
|
$builder = $this->builder();
|
||||||
|
$builder->select('
|
||||||
|
master_tests.test_id as testId,
|
||||||
|
master_tests.test_code as testCode,
|
||||||
|
master_tests.test_name as testName,
|
||||||
|
master_tests.test_unit as testUnit,
|
||||||
|
master_tests.test_method as testMethod,
|
||||||
|
master_tests.cva,
|
||||||
|
master_tests.ba,
|
||||||
|
master_tests.tea,
|
||||||
|
master_depts.dept_name as deptName
|
||||||
|
');
|
||||||
|
$builder->join('master_depts', 'master_depts.dept_id = master_tests.dept_id', 'left');
|
||||||
|
$builder->where('master_tests.test_id', $testId);
|
||||||
|
$builder->where('master_tests.deleted_at', null);
|
||||||
|
$builder->groupBy('master_tests.test_id, master_depts.dept_name');
|
||||||
|
|
||||||
|
return $builder->get()->getRowArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
app/Models/Qc/ControlTestsModel.php
Normal file → Executable file
0
app/Models/Qc/ControlTestsModel.php
Normal file → Executable file
0
app/Models/Qc/ResultsModel.php
Normal file → Executable file
0
app/Models/Qc/ResultsModel.php
Normal file → Executable file
0
app/Models/Qc/TestCommentsModel.php
Normal file → Executable file
0
app/Models/Qc/TestCommentsModel.php
Normal file → Executable file
0
app/ThirdParty/.gitkeep
vendored
Normal file → Executable file
0
app/ThirdParty/.gitkeep
vendored
Normal file → Executable file
0
app/Views/auth/login.php
Normal file → Executable file
0
app/Views/auth/login.php
Normal file → Executable file
0
app/Views/dashboard.php
Normal file → Executable file
0
app/Views/dashboard.php
Normal file → Executable file
88
app/Views/entry/daily.php
Normal file → Executable file
88
app/Views/entry/daily.php
Normal file → Executable file
@ -100,8 +100,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Test</th>
|
<th>Test</th>
|
||||||
<th class="text-center">Mean ± 2SD</th>
|
<th class="text-center">Mean ± 2SD</th>
|
||||||
<th class="w-32">Result</th>
|
<th class="w-40">Result</th>
|
||||||
<th class="w-56">Comment</th>
|
<th class="w-56">Comment</th>
|
||||||
|
<th class="w-16">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -123,18 +124,30 @@
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
:placeholder="'...'"
|
:placeholder="'...'"
|
||||||
class="input input-bordered input-sm w-full font-mono"
|
class="input input-bordered input-sm w-full font-mono"
|
||||||
:class="getInputClass(test, $el.value)"
|
:class="getInputClass(test, $el.value) + ' ' + (deletedResults.includes(test.testId) ? 'input-disabled' : '')"
|
||||||
|
:disabled="deletedResults.includes(test.testId)"
|
||||||
@input.debounce.300ms="updateResult(test.testId, $el.value)"
|
@input.debounce.300ms="updateResult(test.testId, $el.value)"
|
||||||
:value="test.existingResult ? test.existingResult.resValue : ''">
|
:value="getResultValue(test.testId)">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<textarea
|
<textarea
|
||||||
:placeholder="'Optional comment...'"
|
:placeholder="'Optional comment...'"
|
||||||
rows="1"
|
rows="1"
|
||||||
class="textarea textarea-bordered textarea-xs w-full"
|
class="textarea textarea-bordered textarea-xs w-full"
|
||||||
|
:class="deletedResults.includes(test.testId) ? 'textarea-disabled' : ''"
|
||||||
|
:disabled="deletedResults.includes(test.testId)"
|
||||||
@input.debounce.300ms="updateComment(test.testId, $el.value)"
|
@input.debounce.300ms="updateComment(test.testId, $el.value)"
|
||||||
:value="getComment(test.testId)"></textarea>
|
:value="getComment(test.testId)"></textarea>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button"
|
||||||
|
@click="toggleDelete(test.testId)"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
:class="deletedResults.includes(test.testId) ? 'text-warning' : 'text-error'"
|
||||||
|
:title="deletedResults.includes(test.testId) ? 'Restore' : 'Delete'">
|
||||||
|
<i class="fa-solid" :class="deletedResults.includes(test.testId) ? 'fa-rotate-left' : 'fa-trash'"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -163,6 +176,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
saving: false,
|
saving: false,
|
||||||
resultsData: {},
|
resultsData: {},
|
||||||
commentsData: {},
|
commentsData: {},
|
||||||
|
deletedResults: [],
|
||||||
deptId: null,
|
deptId: null,
|
||||||
departments: null,
|
departments: null,
|
||||||
|
|
||||||
@ -236,9 +250,10 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
this.tests = json.data || [];
|
this.tests = json.data || [];
|
||||||
|
|
||||||
// Initialize resultsData and commentsData with existing values
|
// Initialize resultsData, commentsData, and deletedResults
|
||||||
this.resultsData = {};
|
this.resultsData = {};
|
||||||
this.commentsData = {};
|
this.commentsData = {};
|
||||||
|
this.deletedResults = [];
|
||||||
for (const test of this.tests) {
|
for (const test of this.tests) {
|
||||||
if (test.existingResult && test.existingResult.resValue !== null) {
|
if (test.existingResult && test.existingResult.resValue !== null) {
|
||||||
this.resultsData[test.testId] = test.existingResult.resValue;
|
this.resultsData[test.testId] = test.existingResult.resValue;
|
||||||
@ -263,7 +278,17 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.saving = true;
|
this.saving = true;
|
||||||
try {
|
try {
|
||||||
const results = [];
|
const results = [];
|
||||||
|
const deletedIds = [];
|
||||||
|
|
||||||
for (const test of this.tests) {
|
for (const test of this.tests) {
|
||||||
|
// Check if this test is marked for deletion
|
||||||
|
if (this.deletedResults.includes(test.testId)) {
|
||||||
|
if (test.existingResult && test.existingResult.resultId) {
|
||||||
|
deletedIds.push(test.existingResult.resultId);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const value = this.resultsData[test.testId];
|
const value = this.resultsData[test.testId];
|
||||||
if (value !== undefined && value !== '') {
|
if (value !== undefined && value !== '') {
|
||||||
results.push({
|
results.push({
|
||||||
@ -279,15 +304,20 @@ document.addEventListener('alpine:init', () => {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
date: this.date,
|
date: this.date,
|
||||||
results: results
|
results: results,
|
||||||
|
deletedIds: deletedIds
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (json.status === 'success') {
|
if (json.status === 'success') {
|
||||||
// Save comments using the returned result IDs
|
// Save comments for non-deleted tests
|
||||||
const savedIds = json.data.savedIds || [];
|
const savedIds = json.data.savedIds || [];
|
||||||
for (const item of savedIds) {
|
for (const item of savedIds) {
|
||||||
|
// Skip if this test is marked for deletion
|
||||||
|
if (this.deletedResults.includes(item.testId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const comment = this.commentsData[item.testId];
|
const comment = this.commentsData[item.testId];
|
||||||
if (comment) {
|
if (comment) {
|
||||||
await this.saveComment(item.testId, this.date, comment);
|
await this.saveComment(item.testId, this.date, comment);
|
||||||
@ -299,6 +329,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
// Refresh data
|
// Refresh data
|
||||||
this.resultsData = {};
|
this.resultsData = {};
|
||||||
this.commentsData = {};
|
this.commentsData = {};
|
||||||
|
this.deletedResults = [];
|
||||||
await this.fetchTests();
|
await this.fetchTests();
|
||||||
} else {
|
} else {
|
||||||
this.$dispatch('notify', { type: 'error', message: json.message || 'Failed to save' });
|
this.$dispatch('notify', { type: 'error', message: json.message || 'Failed to save' });
|
||||||
@ -329,12 +360,51 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getResultValue(testId) {
|
||||||
|
// If marked for deletion, show empty
|
||||||
|
if (this.deletedResults.includes(testId)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// Return from resultsData if changed
|
||||||
|
if (testId in this.resultsData) {
|
||||||
|
return this.resultsData[testId];
|
||||||
|
}
|
||||||
|
// Return existing result from database
|
||||||
|
const test = this.tests.find(t => t.testId === testId);
|
||||||
|
if (test && test.existingResult && test.existingResult.resValue !== null) {
|
||||||
|
return test.existingResult.resValue;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
updateResult(testId, value) {
|
updateResult(testId, value) {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
delete this.resultsData[testId];
|
delete this.resultsData[testId];
|
||||||
} else {
|
} else {
|
||||||
this.resultsData[testId] = value;
|
this.resultsData[testId] = value;
|
||||||
}
|
}
|
||||||
|
// Remove from deletedResults if user enters a value
|
||||||
|
if (value !== '' && this.deletedResults.includes(testId)) {
|
||||||
|
this.deletedResults = this.deletedResults.filter(id => id !== testId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDelete(testId) {
|
||||||
|
const test = this.tests.find(t => t.testId === testId);
|
||||||
|
const hasExistingResult = test && test.existingResult && test.existingResult.resValue !== null;
|
||||||
|
const hasChanges = testId in this.resultsData || testId in this.commentsData;
|
||||||
|
|
||||||
|
if (this.deletedResults.includes(testId)) {
|
||||||
|
// Restore - remove from deleted list
|
||||||
|
this.deletedResults = this.deletedResults.filter(id => id !== testId);
|
||||||
|
} else {
|
||||||
|
// Mark for deletion
|
||||||
|
if (hasExistingResult || hasChanges) {
|
||||||
|
this.deletedResults.push(testId);
|
||||||
|
// Clear any pending changes for this test
|
||||||
|
delete this.resultsData[testId];
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateComment(testId, value) {
|
updateComment(testId, value) {
|
||||||
@ -384,7 +454,11 @@ document.addEventListener('alpine:init', () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get canSave() {
|
get canSave() {
|
||||||
return this.selectedControl && (Object.keys(this.resultsData).length > 0 || Object.keys(this.commentsData).length > 0) && !this.saving;
|
return this.selectedControl && (
|
||||||
|
Object.keys(this.resultsData).length > 0 ||
|
||||||
|
Object.keys(this.commentsData).length > 0 ||
|
||||||
|
this.deletedResults.length > 0
|
||||||
|
) && !this.saving;
|
||||||
},
|
},
|
||||||
|
|
||||||
setToday() {
|
setToday() {
|
||||||
|
|||||||
0
app/Views/entry/index.php
Normal file → Executable file
0
app/Views/entry/index.php
Normal file → Executable file
0
app/Views/entry/monthly.php
Normal file → Executable file
0
app/Views/entry/monthly.php
Normal file → Executable file
0
app/Views/errors/cli/error_404.php
Normal file → Executable file
0
app/Views/errors/cli/error_404.php
Normal file → Executable file
0
app/Views/errors/cli/error_exception.php
Normal file → Executable file
0
app/Views/errors/cli/error_exception.php
Normal file → Executable file
0
app/Views/errors/cli/production.php
Normal file → Executable file
0
app/Views/errors/cli/production.php
Normal file → Executable file
0
app/Views/errors/html/debug.css
Normal file → Executable file
0
app/Views/errors/html/debug.css
Normal file → Executable file
0
app/Views/errors/html/debug.js
Normal file → Executable file
0
app/Views/errors/html/debug.js
Normal file → Executable file
0
app/Views/errors/html/error_400.php
Normal file → Executable file
0
app/Views/errors/html/error_400.php
Normal file → Executable file
0
app/Views/errors/html/error_404.php
Normal file → Executable file
0
app/Views/errors/html/error_404.php
Normal file → Executable file
0
app/Views/errors/html/error_exception.php
Normal file → Executable file
0
app/Views/errors/html/error_exception.php
Normal file → Executable file
0
app/Views/errors/html/production.php
Normal file → Executable file
0
app/Views/errors/html/production.php
Normal file → Executable file
9
app/Views/layout/main_layout.php
Normal file → Executable file
9
app/Views/layout/main_layout.php
Normal file → Executable file
@ -159,13 +159,20 @@
|
|||||||
Standard Report
|
Standard Report
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1 min-h-0">
|
<li class="mb-1 min-h-0">
|
||||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === 'report/merged' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === 'report/merged' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
||||||
href="<?= base_url('/report/merged') ?>">
|
href="<?= base_url('/report/merged') ?>">
|
||||||
<i class="fa-solid fa-chart-simple w-5"></i>
|
<i class="fa-solid fa-chart-simple w-5"></i>
|
||||||
Merged Report
|
Merged Report
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="mb-1 min-h-0">
|
||||||
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === 'report/custom1' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
||||||
|
href="<?= base_url('/report/custom1') ?>">
|
||||||
|
<i class="fa-solid fa-file-lines w-5"></i>
|
||||||
|
Custom 1
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
63
app/Views/master/control/dialog_control_form.php
Normal file → Executable file
63
app/Views/master/control/dialog_control_form.php
Normal file → Executable file
@ -34,19 +34,33 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="form-control">
|
<div class="form-control relative" x-data="{ showAutocomplete: false, filteredNames: [], highlightIndex: -1 }" @click.outside="showAutocomplete = false">
|
||||||
<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>
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="input input-bordered input-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 placeholder:opacity-50"
|
class="input input-bordered input-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 placeholder:opacity-50"
|
||||||
:class="{'border-error': errors.controlName}" x-model="form.controlName" list="control-names-list"
|
:class="{'border-error': errors.controlName}"
|
||||||
|
x-model="form.controlName"
|
||||||
|
@input="filteredNames = uniqueControlNames.filter(n => n.toLowerCase().includes(form.controlName.toLowerCase())).slice(0, 10); showAutocomplete = filteredNames.length > 0 && form.controlName.length > 0; highlightIndex = -1"
|
||||||
|
@focus="filteredNames = uniqueControlNames.filter(n => n.toLowerCase().includes(form.controlName.toLowerCase())).slice(0, 10); showAutocomplete = filteredNames.length > 0 && form.controlName.length > 0"
|
||||||
|
@keydown.down.prevent="highlightIndex = Math.min(highlightIndex + 1, filteredNames.length - 1)"
|
||||||
|
@keydown.up.prevent="highlightIndex = Math.max(highlightIndex - 1, -1)"
|
||||||
|
@keydown.enter.prevent="if (highlightIndex >= 0) { form.controlName = filteredNames[highlightIndex]; showAutocomplete = false; }"
|
||||||
|
@keydown.escape="showAutocomplete = false"
|
||||||
placeholder="Enter control name" />
|
placeholder="Enter control name" />
|
||||||
<datalist id="control-names-list">
|
<div x-show="showAutocomplete"
|
||||||
<template x-for="name in uniqueControlNames" :key="name">
|
x-transition:enter="transition ease-out duration-100"
|
||||||
<option :value="name" x-text="name"></option>
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
class="absolute top-full left-0 right-0 mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg z-50 max-h-48 overflow-y-auto">
|
||||||
|
<template x-for="(name, index) in filteredNames" :key="name">
|
||||||
|
<div @click="form.controlName = name; showAutocomplete = false"
|
||||||
|
:class="{'bg-primary text-primary-content': index === highlightIndex, 'hover:bg-base-200': index !== highlightIndex}"
|
||||||
|
class="px-3 py-2 cursor-pointer text-sm"
|
||||||
|
x-text="name"></div>
|
||||||
</template>
|
</template>
|
||||||
</datalist>
|
</div>
|
||||||
<template x-if="errors.controlName">
|
<template x-if="errors.controlName">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt text-error" x-text="errors.controlName"></span>
|
<span class="label-text-alt text-error" x-text="errors.controlName"></span>
|
||||||
@ -74,18 +88,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control relative" x-data="{ showProdAutocomplete: false, filteredProducers: [], prodHighlightIndex: -1 }" @click.outside="showProdAutocomplete = false">
|
||||||
<label class="label py-1">
|
<label class="label py-1">
|
||||||
<span class="label-text-alt font-semibold text-base-content/70">Producer</span>
|
<span class="label-text-alt font-semibold text-base-content/70">Producer</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="input input-bordered input-sm w-full focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50"
|
class="input input-bordered input-sm w-full focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50"
|
||||||
x-model="form.producer" list="producers-list" placeholder="Enter producer name" />
|
x-model="form.producer"
|
||||||
<datalist id="producers-list">
|
@input="filteredProducers = uniqueProducers.filter(p => p && p.toLowerCase().includes(form.producer.toLowerCase())).slice(0, 10); showProdAutocomplete = filteredProducers.length > 0 && form.producer.length > 0; prodHighlightIndex = -1"
|
||||||
<template x-for="producer in uniqueProducers" :key="producer">
|
@focus="filteredProducers = uniqueProducers.filter(p => p && p.toLowerCase().includes(form.producer.toLowerCase())).slice(0, 10); showProdAutocomplete = filteredProducers.length > 0 && form.producer.length > 0"
|
||||||
<option :value="producer" x-text="producer"></option>
|
@keydown.down.prevent="prodHighlightIndex = Math.min(prodHighlightIndex + 1, filteredProducers.length - 1)"
|
||||||
|
@keydown.up.prevent="prodHighlightIndex = Math.max(prodHighlightIndex - 1, -1)"
|
||||||
|
@keydown.enter.prevent="if (prodHighlightIndex >= 0) { form.producer = filteredProducers[prodHighlightIndex]; showProdAutocomplete = false; }"
|
||||||
|
@keydown.escape="showProdAutocomplete = false"
|
||||||
|
placeholder="Enter producer name" />
|
||||||
|
<div x-show="showProdAutocomplete"
|
||||||
|
x-transition:enter="transition ease-out duration-100"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
class="absolute top-full left-0 right-0 mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg z-50 max-h-48 overflow-y-auto">
|
||||||
|
<template x-for="(producer, index) in filteredProducers" :key="producer">
|
||||||
|
<div @click="form.producer = producer; showProdAutocomplete = false"
|
||||||
|
:class="{'bg-primary text-primary-content': index === prodHighlightIndex, 'hover:bg-base-200': index !== prodHighlightIndex}"
|
||||||
|
class="px-3 py-2 cursor-pointer text-sm"
|
||||||
|
x-text="producer"></div>
|
||||||
</template>
|
</template>
|
||||||
</datalist>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label cursor-pointer justify-start gap-3 py-1">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="checkbox checkbox-sm checkbox-primary"
|
||||||
|
x-model="form.isActive"
|
||||||
|
:checked="form.isActive == 1"
|
||||||
|
:value="1" />
|
||||||
|
<span class="label-text text-sm font-semibold text-base-content/70">Active</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
260
app/Views/master/control/index.php
Normal file → Executable file
260
app/Views/master/control/index.php
Normal file → Executable file
@ -15,13 +15,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-3 mb-4">
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-3 mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="relative flex-1 max-w-md">
|
<div class="flex items-center gap-2">
|
||||||
<input type="text" placeholder="Search by name..."
|
<div class="relative flex-1 max-w-md">
|
||||||
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"
|
<input type="text" placeholder="Search by name..."
|
||||||
x-model="keyword" @keyup.enter="fetchList()" />
|
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"
|
||||||
</div>
|
x-model="keyword" @keyup.enter="fetchList()" />
|
||||||
<select x-model="deptId" @change="setDeptId(deptId)" class="select select-bordered select-sm">
|
</div>
|
||||||
|
<select x-model="deptId" @change="setDeptId(deptId)" class="select select-bordered select-sm">
|
||||||
<option value="">All Departments</option>
|
<option value="">All Departments</option>
|
||||||
<template x-if="departments">
|
<template x-if="departments">
|
||||||
<template x-for="dept in departments" :key="dept.deptId">
|
<template x-for="dept in departments" :key="dept.deptId">
|
||||||
@ -32,29 +33,50 @@
|
|||||||
<option disabled>Loading...</option>
|
<option disabled>Loading...</option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
<template x-if="deptId">
|
<template x-if="deptId">
|
||||||
<button class="btn btn-sm gap-1" @click="setDeptId(null)">
|
<button class="btn btn-sm gap-1" @click="setDeptId(null)">
|
||||||
<i class="fa-solid fa-xmark text-xs"></i> Clear
|
<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>
|
</button>
|
||||||
</template>
|
</div>
|
||||||
<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 class="space-y-4">
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-1"
|
||||||
|
:class="statusFilter === '1' ? 'btn-primary' : 'btn-ghost'"
|
||||||
|
@click="setStatusFilter('1')">
|
||||||
|
<i class="fa-solid fa-check-circle text-xs"></i> Active
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-1"
|
||||||
|
:class="statusFilter === '0' ? 'btn-error' : 'btn-ghost'"
|
||||||
|
@click="setStatusFilter('0')">
|
||||||
|
<i class="fa-solid fa-circle-xmark text-xs"></i> Inactive
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-1"
|
||||||
|
:class="statusFilter === '' ? 'btn-neutral' : 'btn-ghost'"
|
||||||
|
@click="setStatusFilter('')">
|
||||||
|
<i class="fa-solid fa-list text-xs"></i> All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden">
|
||||||
<template x-if="loading && !list">
|
<template x-if="loading && !list">
|
||||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
<span class="loading loading-spinner loading-md text-primary"></span>
|
<span class="loading loading-spinner loading-md text-primary"></span>
|
||||||
<p class="mt-2 text-base-content/60 text-xs font-medium">Fetching controls...</p>
|
<p class="mt-2 text-base-content/60 text-xs font-medium">Fetching controls...</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="!loading && error">
|
<template x-if="!loading && error">
|
||||||
<div class="bg-base-100 rounded-xl border border-error/20 shadow-sm p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
<div
|
<div class="w-12 h-12 bg-error/10 text-error rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
class="w-12 h-12 bg-error/10 text-error rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation text-xl"></i>
|
<i class="fa-solid fa-triangle-exclamation text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold text-base-content">Something went wrong</h3>
|
<h3 class="font-bold text-base-content">Something went wrong</h3>
|
||||||
@ -64,103 +86,70 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="!loading && !error && list">
|
<template x-if="!loading && !error && list">
|
||||||
<div class="space-y-4">
|
<div class="overflow-x-auto">
|
||||||
<template x-for="(group, name) in groupedList" :key="name">
|
<table class="w-full text-sm">
|
||||||
<div
|
<thead>
|
||||||
class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden transition-all hover:shadow-md">
|
<tr class="bg-base-200/50 text-left border-b border-base-300">
|
||||||
<div
|
<th class="py-3 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Control Name</th>
|
||||||
class="p-3 bg-base-200/40 border-b border-base-300 flex flex-wrap justify-between items-center gap-3">
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Lot</th>
|
||||||
<div class="flex items-center gap-3">
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Producer</th>
|
||||||
<div
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Expiry</th>
|
||||||
class="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center">
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Status</th>
|
||||||
<i class="fa-solid fa-vial text-base"></i>
|
<th class="py-3 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider text-right">Actions</th>
|
||||||
</div>
|
</tr>
|
||||||
<div>
|
</thead>
|
||||||
<h3 class="font-bold text-sm text-base-content leading-tight" x-text="name"></h3>
|
<tbody class="divide-y divide-base-200">
|
||||||
<div class="flex items-center gap-2 mt-0.5">
|
<template x-for="item in list" :key="item.controlId">
|
||||||
<span class="text-[10px] flex items-center gap-1.5 text-base-content/60">
|
<tr class="hover:bg-base-200/30 transition-colors">
|
||||||
<i class="fa-solid fa-industry opacity-50"></i>
|
<td class="py-2.5 px-4">
|
||||||
<span x-text="group.producer || 'No producer info'"></span>
|
<div class="flex items-center gap-2">
|
||||||
</span>
|
<div class="w-6 h-6 rounded bg-primary/10 text-primary flex items-center justify-center">
|
||||||
<span class="w-1 h-1 rounded-full bg-base-300"></span>
|
<i class="fa-solid fa-vial text-[10px]"></i>
|
||||||
<span class="text-[10px] font-medium text-primary"
|
</div>
|
||||||
x-text="group.lots.length + ' Lot(s)'"></span>
|
<span class="font-medium text-sm" x-text="item.controlName"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<td class="py-2.5 px-3">
|
||||||
<button class="btn btn-sm btn-primary gap-1.5 px-3" @click="addLotToControl(group)">
|
<span class="font-mono text-[10px] bg-base-200 text-base-content/70 px-1.5 py-0.5 rounded border border-base-300" x-text="item.lot"></span>
|
||||||
<i class="fa-solid fa-plus text-[10px]"></i> Add Lot
|
</td>
|
||||||
</button>
|
<td class="py-2.5 px-3 text-xs text-base-content/70" x-text="item.producer || '-'">
|
||||||
</div>
|
</td>
|
||||||
|
<td class="py-2.5 px-3 text-xs text-base-content/70" x-text="item.expDate">
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
|
||||||
|
:class="getStatusBadgeClass(item.expDate)">
|
||||||
|
<span class="w-1 h-1 rounded-full" :class="getStatusDotClass(item.expDate)"></span>
|
||||||
|
<span x-text="getStatusLabel(item.expDate)"></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-4 text-right">
|
||||||
|
<div class="flex justify-end items-center gap-1">
|
||||||
|
<button
|
||||||
|
class="p-1.5 text-amber-600 hover:bg-amber-50 rounded transition-colors tooltip tooltip-left"
|
||||||
|
data-tip="Edit" @click="showForm(item.controlId)">
|
||||||
|
<i class="fa-solid fa-pencil text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1.5 text-error hover:bg-error/10 rounded transition-colors tooltip tooltip-left"
|
||||||
|
data-tip="Delete" @click="deleteData(item.controlId)">
|
||||||
|
<i class="fa-solid fa-trash text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr class="bg-base-100 text-left border-b border-base-300">
|
|
||||||
<th
|
|
||||||
class="py-2 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">
|
|
||||||
Lot Number</th>
|
|
||||||
<th
|
|
||||||
class="py-2 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">
|
|
||||||
Expiry Date</th>
|
|
||||||
<th
|
|
||||||
class="py-2 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">
|
|
||||||
Status</th>
|
|
||||||
<th
|
|
||||||
class="py-2 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider text-right">
|
|
||||||
Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-base-200">
|
|
||||||
<template x-for="lot in group.lots" :key="lot.controlId">
|
|
||||||
<tr class="hover:bg-base-200/30 transition-colors group">
|
|
||||||
<td class="py-2 px-4">
|
|
||||||
<span
|
|
||||||
class="font-mono text-[10px] bg-base-200 text-base-content/70 px-1.5 py-0.5 rounded border border-base-300"
|
|
||||||
x-text="lot.lot"></span>
|
|
||||||
</td>
|
|
||||||
<td class="py-2 px-3 text-base-content/80 text-xs font-medium"
|
|
||||||
x-text="lot.expDate">
|
|
||||||
</td>
|
|
||||||
<td class="py-2 px-3">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
|
|
||||||
:class="getStatusBadgeClass(lot.expDate)">
|
|
||||||
<span class="w-1 h-1 rounded-full"
|
|
||||||
:class="getStatusDotClass(lot.expDate)"></span>
|
|
||||||
<span x-text="getStatusLabel(lot.expDate)"></span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="py-2 px-4 text-right">
|
|
||||||
<div class="flex justify-end items-center gap-1">
|
|
||||||
<button
|
|
||||||
class="p-1.5 text-amber-600 hover:bg-amber-50 rounded transition-colors tooltip tooltip-left"
|
|
||||||
data-tip="Edit Lot" @click="showForm(lot.controlId)">
|
|
||||||
<i class="fa-solid fa-pencil text-xs"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1.5 text-error hover:bg-error/10 rounded transition-colors tooltip tooltip-left"
|
|
||||||
data-tip="Delete Lot" @click="deleteData(lot.controlId)">
|
|
||||||
<i class="fa-solid fa-trash text-xs"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="list.length === 0">
|
<template x-if="list.length === 0">
|
||||||
<div class="bg-base-100 rounded-xl border border-dashed border-base-300 p-8 text-center">
|
<div class="p-8 text-center border-t border-base-200">
|
||||||
<div
|
<div class="w-12 h-12 bg-base-200 text-base-content/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
class="w-12 h-12 bg-base-200 text-base-content/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<i class="fa-solid fa-box-open text-xl"></i>
|
<i class="fa-solid fa-box-open text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold text-base-content/70">No data found</h3>
|
<h3 class="font-bold text-base-content/70">No data found</h3>
|
||||||
<p class="text-base-content/50 mt-0.5 text-xs">Try adjusting your search keyword or add a new
|
<p class="text-base-content/50 mt-0.5 text-xs">Try adjusting your filters or add a new control.</p>
|
||||||
control.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -181,6 +170,7 @@
|
|||||||
error: null,
|
error: null,
|
||||||
keyword: "",
|
keyword: "",
|
||||||
deptId: null,
|
deptId: null,
|
||||||
|
statusFilter: "1",
|
||||||
departments: null,
|
departments: null,
|
||||||
loadingDepartments: false,
|
loadingDepartments: false,
|
||||||
list: null,
|
list: null,
|
||||||
@ -190,22 +180,7 @@
|
|||||||
lot: "",
|
lot: "",
|
||||||
producer: "",
|
producer: "",
|
||||||
expDate: "",
|
expDate: "",
|
||||||
},
|
isActive: 1,
|
||||||
|
|
||||||
get groupedList() {
|
|
||||||
if (!this.list) return {};
|
|
||||||
const groups = {};
|
|
||||||
this.list.forEach(item => {
|
|
||||||
if (!groups[item.controlName]) {
|
|
||||||
groups[item.controlName] = {
|
|
||||||
name: item.controlName,
|
|
||||||
producer: item.producer,
|
|
||||||
lots: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
groups[item.controlName].lots.push(item);
|
|
||||||
});
|
|
||||||
return groups;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get uniqueControlNames() {
|
get uniqueControlNames() {
|
||||||
@ -279,6 +254,9 @@
|
|||||||
if (this.deptId) {
|
if (this.deptId) {
|
||||||
params.set('dept_id', this.deptId);
|
params.set('dept_id', this.deptId);
|
||||||
}
|
}
|
||||||
|
if (this.statusFilter !== '') {
|
||||||
|
params.set('is_active', this.statusFilter);
|
||||||
|
}
|
||||||
const response = await fetch(`${BASEURL}api/master/controls?${params}`, {
|
const response = await fetch(`${BASEURL}api/master/controls?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
@ -293,14 +271,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
setDeptId(id) {
|
setDeptId(id) {
|
||||||
this.deptId = id;
|
this.deptId = id;
|
||||||
this.list = null;
|
this.list = null;
|
||||||
this.fetchList();
|
this.fetchList();
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadData(id) {
|
setStatusFilter(status) {
|
||||||
|
this.statusFilter = status;
|
||||||
|
this.list = null;
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadData(id) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BASEURL}api/master/controls/${id}`, {
|
const response = await fetch(`${BASEURL}api/master/controls/${id}`, {
|
||||||
@ -324,30 +307,19 @@ async loadData(id) {
|
|||||||
if (id) {
|
if (id) {
|
||||||
await this.loadData(id);
|
await this.loadData(id);
|
||||||
} else {
|
} else {
|
||||||
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" };
|
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "", isActive: 1 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async addLotToControl(group) {
|
|
||||||
this.showModal = true;
|
|
||||||
this.errors = {};
|
|
||||||
this.form = {
|
|
||||||
controlId: null,
|
|
||||||
controlName: group.name,
|
|
||||||
lot: "",
|
|
||||||
producer: group.producer,
|
|
||||||
expDate: ""
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" };
|
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "", isActive: 1 };
|
||||||
},
|
},
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
this.errors = {};
|
this.errors = {};
|
||||||
if (!this.form.controlName) this.errors.controlName = "Name is required.";
|
if (!this.form.controlName) this.errors.controlName = "Name is required.";
|
||||||
|
if (!this.form.lot) this.errors.lot = "Lot is required.";
|
||||||
return Object.keys(this.errors).length === 0;
|
return Object.keys(this.errors).length === 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
0
app/Views/master/control_test/dialog_control_test_form.php
Normal file → Executable file
0
app/Views/master/control_test/dialog_control_test_form.php
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user