Refactor application structure, unify entry management, and overhaul UI with DaisyUI
- Reorganized Architecture: Moved Master data management (Controls, Departments, Tests) to a dedicated Master namespace/directory for better modularity.
- Unified Data Entry: Consolidated daily and monthly QC entry views and logic into a single streamlined system.
- UI/UX Modernization:
- Integrated DaisyUI and Alpine.js for a responsive, themeable, and interactive experience.
- Replaced legacy layouts with a new DaisyUI-based template.
- Updated branding with new logo and favicon assets.
- Cleaned up deprecated JavaScript assets (app.js, charts.js, tables.js).
- Backend Enhancements:
- Added `ControlEntryModel` and `ResultCommentsController` for improved data handling and auditing.
- Updated routing to support the new controller structure and consolidated API endpoints.
- Documentation & Assets:
- Added [docs/PRD.md](cci:7://file:///c:/www/tinyqc/docs/PRD.md:0:0-0:0) and [docs/llms.txt](cci:7://file:///c:/www/tinyqc/docs/llms.txt:0:0-0:0) for project context and requirements.
- Included database schema scripts and backups in the `backup/` directory.
- Cleanup: Removed legacy TUI progress files and unused commands.
This commit is contained in:
parent
2e23fc38c3
commit
5cae572916
@ -7,6 +7,9 @@ use CodeIgniter\Router\RouteCollection;
|
||||
*/
|
||||
$routes->get('/', 'PageController::dashboard');
|
||||
|
||||
$routes->get('/master/dept', 'PageController::masterDept');
|
||||
$routes->get('/master/test', 'PageController::masterTest');
|
||||
$routes->get('/master/control', 'PageController::masterControl');
|
||||
$routes->get('/dept', 'PageController::dept');
|
||||
$routes->get('/test', 'PageController::test');
|
||||
$routes->get('/control', 'PageController::control');
|
||||
@ -42,3 +45,43 @@ $routes->group('api', function ($routes) {
|
||||
$routes->post('entry/monthly', 'Api\EntryApiController::saveMonthly');
|
||||
$routes->post('entry/comment', 'Api\EntryApiController::saveComment');
|
||||
});
|
||||
|
||||
$routes->group('api/master', function ($routes) {
|
||||
$routes->get('depts', 'Master\MasterDeptsController::index');
|
||||
$routes->get('depts/(:num)', 'Master\MasterDeptsController::show/$1');
|
||||
$routes->post('depts', 'Master\MasterDeptsController::create');
|
||||
$routes->patch('depts/(:num)', 'Master\MasterDeptsController::update/$1');
|
||||
$routes->delete('depts/(:num)', 'Master\MasterDeptsController::delete/$1');
|
||||
|
||||
$routes->get('controls', 'Master\MasterControlsController::index');
|
||||
$routes->get('controls/(:num)', 'Master\MasterControlsController::show/$1');
|
||||
$routes->post('controls', 'Master\MasterControlsController::create');
|
||||
$routes->patch('controls/(:num)', 'Master\MasterControlsController::update/$1');
|
||||
$routes->delete('controls/(:num)', 'Master\MasterControlsController::delete/$1');
|
||||
|
||||
$routes->get('tests', 'Master\MasterTestsController::index');
|
||||
$routes->get('tests/(:num)', 'Master\MasterTestsController::show/$1');
|
||||
$routes->post('tests', 'Master\MasterTestsController::create');
|
||||
$routes->patch('tests/(:num)', 'Master\MasterTestsController::update/$1');
|
||||
$routes->delete('tests/(:num)', 'Master\MasterTestsController::delete/$1');
|
||||
});
|
||||
|
||||
$routes->group('api/qc', function ($routes) {
|
||||
$routes->get('control-tests', 'Qc\ControlTestsController::index');
|
||||
$routes->get('control-tests/(:num)', 'Qc\ControlTestsController::show/$1');
|
||||
$routes->post('control-tests', 'Qc\ControlTestsController::create');
|
||||
$routes->patch('control-tests/(:num)', 'Qc\ControlTestsController::update/$1');
|
||||
$routes->delete('control-tests/(:num)', 'Qc\ControlTestsController::delete/$1');
|
||||
|
||||
$routes->get('results', 'Qc\ResultsController::index');
|
||||
$routes->get('results/(:num)', 'Qc\ResultsController::show/$1');
|
||||
$routes->post('results', 'Qc\ResultsController::create');
|
||||
$routes->patch('results/(:num)', 'Qc\ResultsController::update/$1');
|
||||
$routes->delete('results/(:num)', 'Qc\ResultsController::delete/$1');
|
||||
|
||||
$routes->get('result-comments', 'Qc\ResultCommentsController::index');
|
||||
$routes->get('result-comments/(:num)', 'Qc\ResultCommentsController::show/$1');
|
||||
$routes->post('result-comments', 'Qc\ResultCommentsController::create');
|
||||
$routes->patch('result-comments/(:num)', 'Qc\ResultCommentsController::update/$1');
|
||||
$routes->delete('result-comments/(:num)', 'Qc\ResultCommentsController::delete/$1');
|
||||
});
|
||||
|
||||
@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\DictControlModel;
|
||||
use App\Models\ControlTestModel;
|
||||
|
||||
class ControlApiController extends BaseController
|
||||
{
|
||||
protected $dictControlModel;
|
||||
protected $controlTestModel;
|
||||
protected $rules;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictControlModel = new DictControlModel();
|
||||
$this->controlTestModel = new ControlTestModel();
|
||||
$this->rules = [
|
||||
'name' => 'required|min_length[1]',
|
||||
'dept_ref_id' => 'required',
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
$deptId = $this->request->getGet('deptId');
|
||||
try {
|
||||
$rows = $this->dictControlModel->getWithDept($keyword, $deptId);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null)
|
||||
{
|
||||
try {
|
||||
$rows = $this->dictControlModel->where('control_id', $id)->findAll();
|
||||
if (empty($rows)) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
], 200);
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function getTests($id = null)
|
||||
{
|
||||
try {
|
||||
$rows = $this->controlTestModel->where('control_ref_id', $id)->findAll();
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$controlId = $this->dictControlModel->insert($input, true);
|
||||
|
||||
if (!empty($input['test_ids'])) {
|
||||
foreach ($input['test_ids'] as $testId) {
|
||||
$this->controlTestModel->insert([
|
||||
'control_ref_id' => $controlId,
|
||||
'test_ref_id' => $testId,
|
||||
'mean' => 0,
|
||||
'sd' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $controlId
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->dictControlModel->update($id, $input);
|
||||
|
||||
if (!empty($input['test_ids'])) {
|
||||
$this->controlTestModel->where('control_ref_id', $id)->delete();
|
||||
foreach ($input['test_ids'] as $testId) {
|
||||
$this->controlTestModel->insert([
|
||||
'control_ref_id' => $id,
|
||||
'test_ref_id' => $testId,
|
||||
'mean' => 0,
|
||||
'sd' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null)
|
||||
{
|
||||
try {
|
||||
$this->controlTestModel->where('control_ref_id', $id)->delete();
|
||||
$this->dictControlModel->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,252 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\DictControlModel;
|
||||
use App\Models\ControlTestModel;
|
||||
use App\Models\DailyResultModel;
|
||||
use App\Models\ResultModel;
|
||||
use App\Models\ResultCommentModel;
|
||||
|
||||
class EntryApiController extends BaseController
|
||||
{
|
||||
protected $dictControlModel;
|
||||
protected $controlTestModel;
|
||||
protected $dailyResultModel;
|
||||
protected $resultModel;
|
||||
protected $commentModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictControlModel = new \App\Models\DictControlModel();
|
||||
$this->controlTestModel = new \App\Models\ControlTestModel();
|
||||
$this->dailyResultModel = new \App\Models\DailyResultModel();
|
||||
$this->resultModel = new \App\Models\ResultModel();
|
||||
$this->commentModel = new \App\Models\ResultCommentModel();
|
||||
}
|
||||
|
||||
public function getControls()
|
||||
{
|
||||
try {
|
||||
$date = $this->request->getGet('date');
|
||||
$deptId = $this->request->getGet('deptId');
|
||||
|
||||
$rows = $this->dictControlModel->getActiveByDate($date, $deptId);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function getTests()
|
||||
{
|
||||
try {
|
||||
$controlId = $this->request->getGet('controlId');
|
||||
|
||||
$rows = $this->controlTestModel->getByControl($controlId);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function saveDaily()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
try {
|
||||
$resultData = [
|
||||
'control_ref_id' => $input['controlId'] ?? 0,
|
||||
'test_ref_id' => $input['testId'] ?? 0,
|
||||
'resdate' => $input['resdate'] ?? date('Y-m-d'),
|
||||
'resvalue' => $input['resvalue'] ?? '',
|
||||
'rescomment' => $input['rescomment'] ?? '',
|
||||
];
|
||||
|
||||
$this->dailyResultModel->saveResult($resultData);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'save success'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function saveMonthly()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
try {
|
||||
$controlId = $input['controlId'] ?? 0;
|
||||
$yearMonth = $input['yearMonth'] ?? '';
|
||||
$operation = $input['operation'] ?? 'replace';
|
||||
$tests = $input['tests'] ?? [];
|
||||
$results = [];
|
||||
$statistics = [];
|
||||
$validations = [];
|
||||
|
||||
foreach ($tests as $testData) {
|
||||
$testId = $testData['testId'] ?? 0;
|
||||
$resvalues = $testData['resvalue'] ?? [];
|
||||
$rescomments = $testData['rescomment'] ?? [];
|
||||
|
||||
$controlTest = $this->controlTestModel->getByControlAndTest($controlId, $testId);
|
||||
$mean = $controlTest['mean'] ?? 0;
|
||||
$sd = $controlTest['sd'] ?? 0;
|
||||
$sdLimit = $sd > 0 ? $sd * 2 : 0;
|
||||
|
||||
$testValues = [];
|
||||
$validCount = 0;
|
||||
$validSum = 0;
|
||||
$validSqSum = 0;
|
||||
|
||||
foreach ($resvalues as $day => $value) {
|
||||
if (!empty($value)) {
|
||||
$resdate = $yearMonth . '-' . str_pad($day, 2, '0', STR_PAD_LEFT);
|
||||
$rescomment = $rescomments[$day] ?? '';
|
||||
|
||||
$resultData = [
|
||||
'control_ref_id' => $controlId,
|
||||
'test_ref_id' => $testId,
|
||||
'resdate' => $resdate,
|
||||
'resvalue' => $value,
|
||||
'rescomment' => $rescomment,
|
||||
];
|
||||
|
||||
if ($operation === 'replace') {
|
||||
$this->resultModel->saveResult($resultData);
|
||||
} else {
|
||||
$existing = $this->resultModel->checkExisting($controlId, $testId, $resultData['resdate']);
|
||||
if (!$existing) {
|
||||
$this->resultModel->saveResult($resultData);
|
||||
}
|
||||
}
|
||||
|
||||
$numValue = (float) $value;
|
||||
$testValues[] = $numValue;
|
||||
$validCount++;
|
||||
$validSum += $numValue;
|
||||
$validSqSum += $numValue * $numValue;
|
||||
|
||||
$withinLimit = true;
|
||||
if ($sdLimit > 0) {
|
||||
$withinLimit = abs($numValue - $mean) <= $sdLimit;
|
||||
}
|
||||
|
||||
if (!$withinLimit) {
|
||||
$validations[] = [
|
||||
'testId' => $testId,
|
||||
'day' => $day,
|
||||
'value' => $value,
|
||||
'mean' => $mean,
|
||||
'sd' => $sd,
|
||||
'status' => 'out_of_range'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($validCount > 0) {
|
||||
$calcMean = $validSum / $validCount;
|
||||
$calcSd = 0;
|
||||
if ($validCount > 1) {
|
||||
$variance = ($validSqSum - ($validSum * $validSum) / $validCount) / ($validCount - 1);
|
||||
$calcSd = $variance > 0 ? sqrt($variance) : 0;
|
||||
}
|
||||
$cv = $calcMean > 0 ? ($calcSd / $calcMean) * 100 : 0;
|
||||
|
||||
$statistics[] = [
|
||||
'testId' => $testId,
|
||||
'n' => $validCount,
|
||||
'mean' => round($calcMean, 3),
|
||||
'sd' => round($calcSd, 3),
|
||||
'cv' => round($cv, 2)
|
||||
];
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'testId' => $testId,
|
||||
'saved' => count($resvalues)
|
||||
];
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'save success',
|
||||
'data' => [
|
||||
'results' => $results,
|
||||
'statistics' => $statistics,
|
||||
'validations' => $validations
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function saveComment()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
try {
|
||||
$commentData = [
|
||||
'control_ref_id' => $input['controlId'] ?? 0,
|
||||
'test_ref_id' => $input['testId'] ?? 0,
|
||||
'commonth' => $input['commonth'] ?? '',
|
||||
'comtext' => $input['comtext'] ?? '',
|
||||
];
|
||||
|
||||
$this->commentModel->saveComment($commentData);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'save success'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function getMonthlyData()
|
||||
{
|
||||
$controlId = $this->request->getGet('controlId');
|
||||
$testId = $this->request->getGet('testId');
|
||||
$yearMonth = $this->request->getGet('yearMonth');
|
||||
|
||||
try {
|
||||
$results = $this->resultModel->getByMonth($controlId, $testId, $yearMonth);
|
||||
$comment = $this->commentModel->getByControlTestMonth($controlId, $testId, $yearMonth);
|
||||
|
||||
$formValues = [];
|
||||
$comments = [];
|
||||
foreach ($results as $row) {
|
||||
$day = (int)date('j', strtotime($row['resdate']));
|
||||
$formValues[$day] = $row['resvalue'];
|
||||
if (!empty($row['rescomment'])) {
|
||||
$comments[$day] = $row['rescomment'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => [
|
||||
'formValues' => $formValues,
|
||||
'comments' => $comments,
|
||||
'comment' => $comment ? $comment['comtext'] : ''
|
||||
]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\DictControlModel;
|
||||
use App\Models\ControlTestModel;
|
||||
use App\Models\DictDeptModel;
|
||||
use App\Models\DictTestModel;
|
||||
|
||||
class Control extends BaseController
|
||||
{
|
||||
protected $dictControlModel;
|
||||
protected $controlTestModel;
|
||||
protected $dictDeptModel;
|
||||
protected $dictTestModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictControlModel = new DictControlModel();
|
||||
$this->controlTestModel = new ControlTestModel();
|
||||
$this->dictDeptModel = new DictDeptModel();
|
||||
$this->dictTestModel = new DictTestModel();
|
||||
}
|
||||
|
||||
public function index(): string
|
||||
{
|
||||
return view('control/index', [
|
||||
'title' => 'Control Dictionary',
|
||||
'controls' => $this->dictControlModel->getWithDept(),
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'tests' => $this->dictTestModel->getWithDept(),
|
||||
'active_menu' => 'control',
|
||||
'page_title' => 'Control Dictionary'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\DictDeptModel;
|
||||
use App\Models\DictTestModel;
|
||||
use App\Models\DictControlModel;
|
||||
use App\Models\ResultModel;
|
||||
|
||||
class Dashboard extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$dictDeptModel = new DictDeptModel();
|
||||
$dictTestModel = new DictTestModel();
|
||||
$dictControlModel = new DictControlModel();
|
||||
$resultModel = new ResultModel();
|
||||
|
||||
return view('dashboard', [
|
||||
'depts' => $dictDeptModel->findAll(),
|
||||
'tests' => $dictTestModel->getWithDept(),
|
||||
'controls' => $dictControlModel->getWithDept(),
|
||||
'recent_results' => $resultModel->findAll(20),
|
||||
'page_title' => 'Dashboard',
|
||||
'active_menu' => 'dashboard'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\DictDeptModel;
|
||||
|
||||
class Dept extends BaseController
|
||||
{
|
||||
protected $dictDeptModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictDeptModel = new DictDeptModel();
|
||||
}
|
||||
|
||||
public function index(): string
|
||||
{
|
||||
return view('dept/index', [
|
||||
'title' => 'Department Dictionary',
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'dept',
|
||||
'page_title' => 'Department Dictionary'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\DictDeptModel;
|
||||
|
||||
class Entry extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$dictDeptModel = new \App\Models\DictDeptModel();
|
||||
|
||||
return view('entry/monthly', [
|
||||
'title' => 'Monthly Entry',
|
||||
'depts' => $dictDeptModel->findAll(),
|
||||
'active_menu' => 'entry',
|
||||
'page_title' => 'Monthly Entry'
|
||||
]);
|
||||
}
|
||||
|
||||
public function daily()
|
||||
{
|
||||
$dictDeptModel = new \App\Models\DictDeptModel();
|
||||
|
||||
return view('entry/daily', [
|
||||
'title' => 'Daily Entry',
|
||||
'depts' => $dictDeptModel->findAll(),
|
||||
'active_menu' => 'entry_daily',
|
||||
'page_title' => 'Daily Entry'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,46 +1,42 @@
|
||||
<?php
|
||||
namespace App\Controllers\Master;
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\DictTestModel;
|
||||
use App\Models\DictDeptModel;
|
||||
use App\Models\Master\MasterControlsModel;
|
||||
|
||||
class TestApiController extends BaseController
|
||||
{
|
||||
protected $dictTestModel;
|
||||
protected $dictDeptModel;
|
||||
class MasterControlsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictTestModel = new DictTestModel();
|
||||
$this->dictDeptModel = new DictDeptModel();
|
||||
public function __construct() {
|
||||
$this->model = new MasterControlsModel();
|
||||
$this->rules = [
|
||||
'name' => 'required|min_length[1]',
|
||||
'dept_ref_id' => 'required',
|
||||
'lot' => 'required|min_length[1]',
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->dictTestModel->getWithDept();
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null)
|
||||
{
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$rows = $this->dictTestModel->where('test_id', $id)->findAll();
|
||||
if (empty($rows)) {
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
@ -49,58 +45,58 @@ class TestApiController extends BaseController
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
'data' => [$row]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$id = $this->dictTestModel->insert($input, true);
|
||||
$id = $this->model->insert($input, true);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null)
|
||||
{
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->dictTestModel->update($id, $input);
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => $id
|
||||
]);
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null)
|
||||
{
|
||||
public function delete($id = null) {
|
||||
try {
|
||||
$this->dictTestModel->delete($id);
|
||||
$this->model->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success'
|
||||
]);
|
||||
'message' => 'delete success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +1,41 @@
|
||||
<?php
|
||||
namespace App\Controllers\Master;
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\DictDeptModel;
|
||||
use App\Models\Master\MasterDeptsModel;
|
||||
|
||||
class DeptApiController extends BaseController
|
||||
{
|
||||
protected $dictDeptModel;
|
||||
class MasterDeptsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictDeptModel = new DictDeptModel();
|
||||
public function __construct() {
|
||||
$this->model = new MasterDeptsModel();
|
||||
$this->rules = [
|
||||
'name' => 'required|min_length[1]',
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->dictDeptModel->findAll();
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null)
|
||||
{
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$rows = $this->dictDeptModel->where('dept_id', $id)->findAll();
|
||||
if (empty($rows)) {
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
@ -45,58 +44,58 @@ class DeptApiController extends BaseController
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
'data' => [$row]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$id = $this->dictDeptModel->insert($input, true);
|
||||
$id = $this->model->insert($input, true);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null)
|
||||
{
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->dictDeptModel->update($id, $input);
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => $id
|
||||
]);
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null)
|
||||
{
|
||||
public function delete($id = null) {
|
||||
try {
|
||||
$this->dictDeptModel->delete($id);
|
||||
$this->model->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success'
|
||||
]);
|
||||
'message' => 'delete success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
101
app/Controllers/Master/MasterTestsController.php
Normal file
101
app/Controllers/Master/MasterTestsController.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace App\Controllers\Master;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Master\MasterTestsModel;
|
||||
|
||||
class MasterTestsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->model = new MasterTestsModel();
|
||||
$this->rules = [
|
||||
'name' => 'required|min_length[1]',
|
||||
];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
], 200);
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => [$row]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$id = $this->model->insert($input, true);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null) {
|
||||
try {
|
||||
$this->model->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,157 +2,40 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class PageController extends BaseController
|
||||
{
|
||||
protected $dictDeptModel;
|
||||
protected $dictTestModel;
|
||||
protected $dictControlModel;
|
||||
protected $resultModel;
|
||||
protected $controlTestModel;
|
||||
protected $commentModel;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictDeptModel = new \App\Models\DictDeptModel();
|
||||
$this->dictTestModel = new \App\Models\DictTestModel();
|
||||
$this->dictControlModel = new \App\Models\DictControlModel();
|
||||
$this->resultModel = new \App\Models\ResultModel();
|
||||
$this->controlTestModel = new \App\Models\ControlTestModel();
|
||||
$this->commentModel = new \App\Models\ResultCommentModel();
|
||||
class PageController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
public function dashboard() {
|
||||
return view('dashboard');
|
||||
}
|
||||
|
||||
public function dashboard(): string
|
||||
{
|
||||
return view('dashboard', [
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'tests' => $this->dictTestModel->getWithDept(),
|
||||
'controls' => $this->dictControlModel->getWithDept(),
|
||||
'recent_results' => $this->resultModel->findAll(20),
|
||||
'page_title' => 'Dashboard',
|
||||
'active_menu' => 'dashboard'
|
||||
]);
|
||||
public function masterDept() {
|
||||
return view('master/dept/index');
|
||||
}
|
||||
|
||||
public function dept(): string
|
||||
{
|
||||
return view('dept/index', [
|
||||
'title' => 'Department Dictionary',
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'dept',
|
||||
'page_title' => 'Department Dictionary'
|
||||
]);
|
||||
public function masterTest() {
|
||||
return view('master/test/index');
|
||||
}
|
||||
|
||||
public function test(): string
|
||||
{
|
||||
return view('test/index', [
|
||||
'title' => 'Test Dictionary',
|
||||
'tests' => $this->dictTestModel->getWithDept(),
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'test',
|
||||
'page_title' => 'Test Dictionary'
|
||||
]);
|
||||
public function masterControl() {
|
||||
return view('master/control/index');
|
||||
}
|
||||
|
||||
public function control(): string
|
||||
{
|
||||
return view('control/index', [
|
||||
'title' => 'Control Dictionary',
|
||||
'controls' => $this->dictControlModel->getWithDept(),
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'tests' => $this->dictTestModel->getWithDept(),
|
||||
'active_menu' => 'control',
|
||||
'page_title' => 'Control Dictionary'
|
||||
]);
|
||||
public function entry() {
|
||||
return view('entry/index');
|
||||
}
|
||||
|
||||
public function entry(): string
|
||||
{
|
||||
return view('entry/monthly', [
|
||||
'title' => 'Monthly Entry',
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'entry',
|
||||
'page_title' => 'Monthly Entry'
|
||||
]);
|
||||
public function entryDaily() {
|
||||
return view('entry/daily');
|
||||
}
|
||||
|
||||
public function entryDaily(): string
|
||||
{
|
||||
return view('entry/daily', [
|
||||
'title' => 'Daily Entry',
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'entry_daily',
|
||||
'page_title' => 'Daily Entry'
|
||||
]);
|
||||
public function report() {
|
||||
return view('report/index');
|
||||
}
|
||||
|
||||
public function report(): string
|
||||
{
|
||||
return view('report/index', [
|
||||
'title' => 'Reports',
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'tests' => $this->dictTestModel->findAll(),
|
||||
'controls' => $this->dictControlModel->findAll(),
|
||||
'active_menu' => 'report',
|
||||
'page_title' => 'Reports'
|
||||
]);
|
||||
}
|
||||
|
||||
public function reportView(): string
|
||||
{
|
||||
$control1 = $this->request->getGet('control1') ?? 0;
|
||||
$control2 = $this->request->getGet('control2') ?? 0;
|
||||
$control3 = $this->request->getGet('control3') ?? 0;
|
||||
$dates = $this->request->getGet('dates') ?? date('Y-m');
|
||||
$test = $this->request->getGet('test') ?? 0;
|
||||
|
||||
$controls = [];
|
||||
if ($control1) {
|
||||
$c1 = $this->dictControlModel->find($control1);
|
||||
if ($c1) $controls[] = $c1;
|
||||
}
|
||||
if ($control2) {
|
||||
$c2 = $this->dictControlModel->find($control2);
|
||||
if ($c2) $controls[] = $c2;
|
||||
}
|
||||
if ($control3) {
|
||||
$c3 = $this->dictControlModel->find($control3);
|
||||
if ($c3) $controls[] = $c3;
|
||||
}
|
||||
|
||||
$reportData = [];
|
||||
foreach ($controls as $control) {
|
||||
$controlTest = $this->controlTestModel->getByControlAndTest($control['control_id'], $test);
|
||||
$results = $this->resultModel->getByMonth($control['control_id'], $test, $dates);
|
||||
$comment = $this->commentModel->getByControlTestMonth($control['control_id'], $test, $dates);
|
||||
|
||||
$outOfRangeCount = 0;
|
||||
if ($controlTest && $controlTest['sd'] > 0) {
|
||||
foreach ($results as $res) {
|
||||
if (abs($res['resvalue'] - $controlTest['mean']) > 2 * $controlTest['sd']) {
|
||||
$outOfRangeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$reportData[] = [
|
||||
'control' => $control,
|
||||
'controlTest' => $controlTest,
|
||||
'results' => $results,
|
||||
'comment' => $comment,
|
||||
'test' => $this->dictTestModel->find($test),
|
||||
'outOfRange' => $outOfRangeCount
|
||||
];
|
||||
}
|
||||
|
||||
return view('report/view', [
|
||||
'title' => 'QC Report',
|
||||
'reportData' => $reportData,
|
||||
'dates' => $dates,
|
||||
'test' => $test,
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'report',
|
||||
'page_title' => 'QC Report'
|
||||
]);
|
||||
public function reportView() {
|
||||
return view('report/view');
|
||||
}
|
||||
}
|
||||
|
||||
99
app/Controllers/Qc/ControlTestsController.php
Normal file
99
app/Controllers/Qc/ControlTestsController.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace App\Controllers\Qc;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Qc\ControlTestsModel;
|
||||
|
||||
class ControlTestsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->model = new ControlTestsModel();
|
||||
$this->rules = [];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
], 200);
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => [$row]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$id = $this->model->insert($input, true);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null) {
|
||||
try {
|
||||
$this->model->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
99
app/Controllers/Qc/ResultCommentsController.php
Normal file
99
app/Controllers/Qc/ResultCommentsController.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace App\Controllers\Qc;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Qc\ResultCommentsModel;
|
||||
|
||||
class ResultCommentsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->model = new ResultCommentsModel();
|
||||
$this->rules = [];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
], 200);
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => [$row]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$id = $this->model->insert($input, true);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null) {
|
||||
try {
|
||||
$this->model->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
99
app/Controllers/Qc/ResultsController.php
Normal file
99
app/Controllers/Qc/ResultsController.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace App\Controllers\Qc;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Qc\ResultsModel;
|
||||
|
||||
class ResultsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->model = new ResultsModel();
|
||||
$this->rules = [];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'data not found.'
|
||||
], 200);
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => [$row]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$id = $this->model->insert($input, true);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => $id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
if (!$this->validate($this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
try {
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'update success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null) {
|
||||
try {
|
||||
$this->model->delete($id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'delete success',
|
||||
'data' => [$id]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\DictDeptModel;
|
||||
use App\Models\DictTestModel;
|
||||
use App\Models\DictControlModel;
|
||||
|
||||
class Report extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$dictDeptModel = new DictDeptModel();
|
||||
$dictTestModel = new DictTestModel();
|
||||
$dictControlModel = new DictControlModel();
|
||||
|
||||
return view('report/index', [
|
||||
'title' => 'Reports',
|
||||
'depts' => $dictDeptModel->findAll(),
|
||||
'tests' => $dictTestModel->findAll(),
|
||||
'controls' => $dictControlModel->findAll(),
|
||||
'active_menu' => 'report',
|
||||
'page_title' => 'Reports'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\DictTestModel;
|
||||
use App\Models\DictDeptModel;
|
||||
|
||||
class Test extends BaseController
|
||||
{
|
||||
protected $dictTestModel;
|
||||
protected $dictDeptModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dictTestModel = new DictTestModel();
|
||||
$this->dictDeptModel = new DictDeptModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('test/index', [
|
||||
'title' => 'Test Dictionary',
|
||||
'tests' => $this->dictTestModel->getWithDept(),
|
||||
'depts' => $this->dictDeptModel->findAll(),
|
||||
'active_menu' => 'test',
|
||||
'page_title' => 'Test Dictionary'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,282 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateCmodQcTables extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'control_test_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'control_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'test_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'mean' => [
|
||||
'type' => 'FLOAT',
|
||||
'null' => true,
|
||||
],
|
||||
'sd' => [
|
||||
'type' => 'FLOAT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'deleted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('control_test_id', true);
|
||||
$this->forge->createTable('control_tests');
|
||||
|
||||
$this->forge->addField([
|
||||
'result_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'control_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'test_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'resdate' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'resvalue' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'rescomment' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'deleted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('result_id', true);
|
||||
$this->forge->createTable('results');
|
||||
|
||||
$this->forge->addField([
|
||||
'control_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'dept_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'lot' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'producer' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'expdate' => [
|
||||
'type' => 'DATE',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'deleted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('control_id', true);
|
||||
$this->forge->createTable('dict_controls');
|
||||
|
||||
$this->forge->addField([
|
||||
'dept_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'deleted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('dept_id', true);
|
||||
$this->forge->createTable('dict_depts');
|
||||
|
||||
$this->forge->addField([
|
||||
'test_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'dept_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'unit' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'method' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'cva' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'ba' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'tea' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'deleted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('test_id', true);
|
||||
$this->forge->createTable('dict_tests');
|
||||
|
||||
$this->forge->addField([
|
||||
'result_comment_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'control_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'test_ref_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'commonth' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 7,
|
||||
'null' => true,
|
||||
],
|
||||
'comtext' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'deleted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('result_comment_id', true);
|
||||
$this->forge->createTable('result_comments');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('control_tests');
|
||||
$this->forge->dropTable('results');
|
||||
$this->forge->dropTable('dict_controls');
|
||||
$this->forge->dropTable('dict_depts');
|
||||
$this->forge->dropTable('dict_tests');
|
||||
$this->forge->dropTable('result_comments');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class QualityControlSystem extends Migration {
|
||||
|
||||
public function up() {
|
||||
$this->forge->addField([
|
||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('dept_id', true);
|
||||
$this->forge->createTable('dict_depts');
|
||||
|
||||
$this->forge->addField([
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'producer' => ['type' => 'TEXT', 'null' => true],
|
||||
'exp_date' => ['type' => 'DATE', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('control_id', true);
|
||||
$this->forge->addForeignKey('dept_id', 'dict_depts', 'dept_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('dict_controls');
|
||||
|
||||
$this->forge->addField([
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'unit' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'method' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'cva' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'ba' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'tea' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('test_id', true);
|
||||
$this->forge->addForeignKey('dept_id', 'dict_depts', 'dept_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('dict_tests');
|
||||
|
||||
$this->forge->addField([
|
||||
'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'mean' => ['type' => 'FLOAT', 'null' => true],
|
||||
'sd' => ['type' => 'FLOAT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('control_test_id', true);
|
||||
$this->forge->addForeignKey('control_id', 'dict_controls', 'control_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->addForeignKey('test_id', 'dict_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('control_tests');
|
||||
|
||||
$this->forge->addField([
|
||||
'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'res_date' => ['type' => 'DATETIME', 'null' => true],
|
||||
'res_value' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'res_comment' => ['type' => 'TEXT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('result_id', true);
|
||||
$this->forge->addForeignKey('control_id', 'dict_controls', 'control_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->addForeignKey('test_id', 'dict_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('results');
|
||||
|
||||
$this->forge->addField([
|
||||
'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'comment_month' => ['type' => 'VARCHAR', 'constraint' => 7],
|
||||
'com_text' => ['type' => 'TEXT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('result_comment_id', true);
|
||||
$this->forge->addUniqueKey(['control_id', 'test_id', 'comment_month']);
|
||||
$this->forge->addForeignKey('control_id', 'dict_controls', 'control_id', 'CASCADE', 'CASCADE');
|
||||
$this->forge->addForeignKey('test_id', 'dict_tests', 'test_id', 'CASCADE', 'CASCADE');
|
||||
$this->forge->createTable('result_comments');
|
||||
}
|
||||
|
||||
public function down() {
|
||||
$this->forge->dropTable('result_comments', true);
|
||||
$this->forge->dropTable('results', true);
|
||||
$this->forge->dropTable('control_tests', true);
|
||||
$this->forge->dropTable('dict_tests', true);
|
||||
$this->forge->dropTable('dict_controls', true);
|
||||
$this->forge->dropTable('dict_depts', true);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class QualityControlSystem extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('dept_id', true);
|
||||
$this->forge->createTable('master_depts');
|
||||
|
||||
$this->forge->addField([
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'producer' => ['type' => 'TEXT', 'null' => true],
|
||||
'exp_date' => ['type' => 'DATE', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('control_id', true);
|
||||
$this->forge->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('master_controls');
|
||||
|
||||
$this->forge->addField([
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'unit' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'method' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'cva' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'ba' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'tea' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('test_id', true);
|
||||
$this->forge->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('master_tests');
|
||||
|
||||
$this->forge->addField([
|
||||
'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'mean' => ['type' => 'FLOAT', 'null' => true],
|
||||
'sd' => ['type' => 'FLOAT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('control_test_id', true);
|
||||
$this->forge->addForeignKey('control_id', 'master_controls', 'control_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('control_tests');
|
||||
|
||||
$this->forge->addField([
|
||||
'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'res_date' => ['type' => 'DATETIME', 'null' => true],
|
||||
'res_value' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'res_comment' => ['type' => 'TEXT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('result_id', true);
|
||||
$this->forge->addForeignKey('control_id', 'master_controls', 'control_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||
$this->forge->createTable('results');
|
||||
|
||||
$this->forge->addField([
|
||||
'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'control_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'test_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'comment_month' => ['type' => 'VARCHAR', 'constraint' => 7],
|
||||
'com_text' => ['type' => 'TEXT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addKey('result_comment_id', true);
|
||||
$this->forge->addUniqueKey(['control_id', 'test_id', 'comment_month']);
|
||||
$this->forge->addForeignKey('control_id', 'master_controls', 'control_id', 'CASCADE', 'CASCADE');
|
||||
$this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'CASCADE', 'CASCADE');
|
||||
$this->forge->createTable('result_comments');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('result_comments', true);
|
||||
$this->forge->dropTable('results', true);
|
||||
$this->forge->dropTable('control_tests', true);
|
||||
$this->forge->dropTable('master_tests', true);
|
||||
$this->forge->dropTable('master_controls', true);
|
||||
$this->forge->dropTable('master_depts', true);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class RenameDictToMasterTables extends Migration {
|
||||
|
||||
public function up() {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$db->query('SET FOREIGN_KEY_CHECKS=0');
|
||||
|
||||
$tables = $db->listTables();
|
||||
if (in_array('dict_depts', $tables)) {
|
||||
$db->query('RENAME TABLE dict_depts TO master_depts');
|
||||
}
|
||||
if (in_array('dict_controls', $tables)) {
|
||||
$db->query('RENAME TABLE dict_controls TO master_controls');
|
||||
}
|
||||
if (in_array('dict_tests', $tables)) {
|
||||
$db->query('RENAME TABLE dict_tests TO master_tests');
|
||||
}
|
||||
|
||||
$db->query('SET FOREIGN_KEY_CHECKS=1');
|
||||
}
|
||||
|
||||
public function down() {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$db->query('SET FOREIGN_KEY_CHECKS=0');
|
||||
|
||||
$tables = $db->listTables();
|
||||
if (in_array('master_depts', $tables)) {
|
||||
$db->query('RENAME TABLE master_depts TO dict_depts');
|
||||
}
|
||||
if (in_array('master_controls', $tables)) {
|
||||
$db->query('RENAME TABLE master_controls TO dict_controls');
|
||||
}
|
||||
if (in_array('master_tests', $tables)) {
|
||||
$db->query('RENAME TABLE master_tests TO dict_tests');
|
||||
}
|
||||
|
||||
$db->query('SET FOREIGN_KEY_CHECKS=1');
|
||||
}
|
||||
}
|
||||
10
app/Database/Migrations/test.php
Normal file
10
app/Database/Migrations/test.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
echo "Current migrations table:\n";
|
||||
$results = $db->query("SELECT * FROM migrations")->getResult();
|
||||
print_r($results);
|
||||
|
||||
echo "\nChecking for dict_ tables:\n";
|
||||
$tables = $db->listTables();
|
||||
print_r($tables);
|
||||
@ -1,322 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
|
||||
class CmodQcSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$data = [
|
||||
['control_test_id' => 440, 'control_ref_id' => 125, 'test_ref_id' => 18, 'mean' => 1.28, 'sd' => 0.64],
|
||||
['control_test_id' => 441, 'control_ref_id' => 126, 'test_ref_id' => 18, 'mean' => 2.26, 'sd' => 1.13],
|
||||
['control_test_id' => 541, 'control_ref_id' => 169, 'test_ref_id' => 18, 'mean' => 1.24, 'sd' => 0.155],
|
||||
['control_test_id' => 443, 'control_ref_id' => 128, 'test_ref_id' => 20, 'mean' => 105, 'sd' => 14.6],
|
||||
['control_test_id' => 565, 'control_ref_id' => 179, 'test_ref_id' => 15, 'mean' => 44, 'sd' => 2.2],
|
||||
['control_test_id' => 566, 'control_ref_id' => 179, 'test_ref_id' => 16, 'mean' => 107, 'sd' => 5.35],
|
||||
['control_test_id' => 482, 'control_ref_id' => 144, 'test_ref_id' => 3, 'mean' => 1.81, 'sd' => 0.1575],
|
||||
['control_test_id' => 444, 'control_ref_id' => 127, 'test_ref_id' => 20, 'mean' => 26.8, 'sd' => 3.75],
|
||||
['control_test_id' => 523, 'control_ref_id' => 163, 'test_ref_id' => 1, 'mean' => 49.8, 'sd' => 3.75],
|
||||
['control_test_id' => 524, 'control_ref_id' => 163, 'test_ref_id' => 2, 'mean' => 56.4, 'sd' => 4.2],
|
||||
['control_test_id' => 483, 'control_ref_id' => 144, 'test_ref_id' => 4, 'mean' => 4.17, 'sd' => 0.36],
|
||||
['control_test_id' => 449, 'control_ref_id' => 133, 'test_ref_id' => 19, 'mean' => 12.55, 'sd' => 1.883],
|
||||
['control_test_id' => 567, 'control_ref_id' => 179, 'test_ref_id' => 13, 'mean' => 168, 'sd' => 8.4],
|
||||
['control_test_id' => 568, 'control_ref_id' => 179, 'test_ref_id' => 14, 'mean' => 124, 'sd' => 6.2],
|
||||
['control_test_id' => 439, 'control_ref_id' => 124, 'test_ref_id' => 19, 'mean' => 3.03, 'sd' => 0.455],
|
||||
['control_test_id' => 569, 'control_ref_id' => 180, 'test_ref_id' => 1, 'mean' => 49.5, 'sd' => 3.75],
|
||||
['control_test_id' => 450, 'control_ref_id' => 134, 'test_ref_id' => 13, 'mean' => 166, 'sd' => 8.3],
|
||||
['control_test_id' => 570, 'control_ref_id' => 180, 'test_ref_id' => 2, 'mean' => 56.4, 'sd' => 4.2],
|
||||
['control_test_id' => 451, 'control_ref_id' => 134, 'test_ref_id' => 14, 'mean' => 127, 'sd' => 6.35],
|
||||
['control_test_id' => 452, 'control_ref_id' => 134, 'test_ref_id' => 15, 'mean' => 43, 'sd' => 2.15],
|
||||
['control_test_id' => 545, 'control_ref_id' => 173, 'test_ref_id' => 1, 'mean' => 150, 'sd' => 12],
|
||||
['control_test_id' => 546, 'control_ref_id' => 173, 'test_ref_id' => 2, 'mean' => 137, 'sd' => 10.5],
|
||||
['control_test_id' => 542, 'control_ref_id' => 170, 'test_ref_id' => 18, 'mean' => 2.02, 'sd' => 0.275],
|
||||
['control_test_id' => 547, 'control_ref_id' => 173, 'test_ref_id' => 6, 'mean' => 126, 'sd' => 9],
|
||||
['control_test_id' => 453, 'control_ref_id' => 134, 'test_ref_id' => 16, 'mean' => 105, 'sd' => 5.25],
|
||||
['control_test_id' => 454, 'control_ref_id' => 135, 'test_ref_id' => 13, 'mean' => 232, 'sd' => 11.6],
|
||||
['control_test_id' => 455, 'control_ref_id' => 135, 'test_ref_id' => 14, 'mean' => 179, 'sd' => 8.95],
|
||||
['control_test_id' => 456, 'control_ref_id' => 135, 'test_ref_id' => 15, 'mean' => 61, 'sd' => 3.05],
|
||||
['control_test_id' => 457, 'control_ref_id' => 135, 'test_ref_id' => 16, 'mean' => 146, 'sd' => 7.3],
|
||||
['control_test_id' => 473, 'control_ref_id' => 143, 'test_ref_id' => 2, 'mean' => 137, 'sd' => 10.5],
|
||||
['control_test_id' => 474, 'control_ref_id' => 143, 'test_ref_id' => 1, 'mean' => 146, 'sd' => 10.5],
|
||||
['control_test_id' => 475, 'control_ref_id' => 143, 'test_ref_id' => 7, 'mean' => 4.3, 'sd' => 0.3225],
|
||||
['control_test_id' => 476, 'control_ref_id' => 143, 'test_ref_id' => 9, 'mean' => 243, 'sd' => 19],
|
||||
['control_test_id' => 489, 'control_ref_id' => 145, 'test_ref_id' => 37, 'mean' => 0.851, 'sd' => 0.113],
|
||||
['control_test_id' => 480, 'control_ref_id' => 143, 'test_ref_id' => 5, 'mean' => 4.73, 'sd' => 2.4],
|
||||
['control_test_id' => 481, 'control_ref_id' => 143, 'test_ref_id' => 36, 'mean' => 3.31, 'sd' => 0.12],
|
||||
['control_test_id' => 484, 'control_ref_id' => 144, 'test_ref_id' => 10, 'mean' => 162, 'sd' => 20.25],
|
||||
['control_test_id' => 485, 'control_ref_id' => 144, 'test_ref_id' => 11, 'mean' => 91.7, 'sd' => 6.7],
|
||||
['control_test_id' => 580, 'control_ref_id' => 182, 'test_ref_id' => 17, 'mean' => 10.71, 'sd' => 0.25],
|
||||
['control_test_id' => 429, 'control_ref_id' => 121, 'test_ref_id' => 3, 'mean' => 0.83, 'sd' => 0.073],
|
||||
['control_test_id' => 430, 'control_ref_id' => 121, 'test_ref_id' => 4, 'mean' => 1.53, 'sd' => 0.13],
|
||||
['control_test_id' => 431, 'control_ref_id' => 121, 'test_ref_id' => 10, 'mean' => 66.6, 'sd' => 5.575],
|
||||
['control_test_id' => 432, 'control_ref_id' => 121, 'test_ref_id' => 11, 'mean' => 27.5, 'sd' => 2.025],
|
||||
['control_test_id' => 582, 'control_ref_id' => 184, 'test_ref_id' => 10, 'mean' => 61.65, 'sd' => 5.125],
|
||||
['control_test_id' => 434, 'control_ref_id' => 121, 'test_ref_id' => 30, 'mean' => 144, 'sd' => 14.5],
|
||||
['control_test_id' => 583, 'control_ref_id' => 184, 'test_ref_id' => 3, 'mean' => 0.765, 'sd' => 0.0675],
|
||||
['control_test_id' => 487, 'control_ref_id' => 144, 'test_ref_id' => 30, 'mean' => 220, 'sd' => 22],
|
||||
['control_test_id' => 488, 'control_ref_id' => 143, 'test_ref_id' => 32, 'mean' => 8.07, 'sd' => 0.4],
|
||||
['control_test_id' => 490, 'control_ref_id' => 145, 'test_ref_id' => 38, 'mean' => 1.15, 'sd' => 0.15],
|
||||
['control_test_id' => 494, 'control_ref_id' => 145, 'test_ref_id' => 39, 'mean' => 6.69, 'sd' => 0.89],
|
||||
['control_test_id' => 495, 'control_ref_id' => 146, 'test_ref_id' => 37, 'mean' => 23.219, 'sd' => 3.096],
|
||||
['control_test_id' => 496, 'control_ref_id' => 146, 'test_ref_id' => 39, 'mean' => 15.24, 'sd' => 2.03],
|
||||
['control_test_id' => 497, 'control_ref_id' => 146, 'test_ref_id' => 38, 'mean' => 3.81, 'sd' => 0.51],
|
||||
['control_test_id' => 514, 'control_ref_id' => 153, 'test_ref_id' => 47, 'mean' => 0.08, 'sd' => 0.13],
|
||||
['control_test_id' => 515, 'control_ref_id' => 154, 'test_ref_id' => 47, 'mean' => 4.91, 'sd' => 0.9],
|
||||
['control_test_id' => 516, 'control_ref_id' => 158, 'test_ref_id' => 49, 'mean' => 22.34, 'sd' => 2.98],
|
||||
['control_test_id' => 517, 'control_ref_id' => 159, 'test_ref_id' => 49, 'mean' => 63.25, 'sd' => 8.43],
|
||||
['control_test_id' => 518, 'control_ref_id' => 155, 'test_ref_id' => 47, 'mean' => 7.6, 'sd' => 1.39],
|
||||
['control_test_id' => 519, 'control_ref_id' => 156, 'test_ref_id' => 48, 'mean' => 0.01, 'sd' => 0.13],
|
||||
['control_test_id' => 520, 'control_ref_id' => 157, 'test_ref_id' => 48, 'mean' => 4.62, 'sd' => 0.77],
|
||||
['control_test_id' => 525, 'control_ref_id' => 163, 'test_ref_id' => 8, 'mean' => 5.16, 'sd' => 0.355],
|
||||
['control_test_id' => 526, 'control_ref_id' => 163, 'test_ref_id' => 6, 'mean' => 41.1, 'sd' => 3.075],
|
||||
['control_test_id' => 527, 'control_ref_id' => 163, 'test_ref_id' => 7, 'mean' => 1.07, 'sd' => 0.08],
|
||||
['control_test_id' => 528, 'control_ref_id' => 163, 'test_ref_id' => 9, 'mean' => 101, 'sd' => 7.75],
|
||||
['control_test_id' => 529, 'control_ref_id' => 163, 'test_ref_id' => 5, 'mean' => 3.16, 'sd' => 0.1575],
|
||||
['control_test_id' => 530, 'control_ref_id' => 163, 'test_ref_id' => 32, 'mean' => 5.07, 'sd' => 0.2539],
|
||||
['control_test_id' => 535, 'control_ref_id' => 163, 'test_ref_id' => 36, 'mean' => 2.2, 'sd' => 0.0838],
|
||||
['control_test_id' => 536, 'control_ref_id' => 165, 'test_ref_id' => 17, 'mean' => 5.355, 'sd' => 0.15],
|
||||
['control_test_id' => 537, 'control_ref_id' => 166, 'test_ref_id' => 17, 'mean' => 10.29, 'sd' => 0.25],
|
||||
['control_test_id' => 548, 'control_ref_id' => 173, 'test_ref_id' => 7, 'mean' => 4.11, 'sd' => 0.305],
|
||||
['control_test_id' => 549, 'control_ref_id' => 173, 'test_ref_id' => 8, 'mean' => 10.7, 'sd' => 0.75],
|
||||
['control_test_id' => 550, 'control_ref_id' => 173, 'test_ref_id' => 9, 'mean' => 241, 'sd' => 18.75],
|
||||
['control_test_id' => 551, 'control_ref_id' => 173, 'test_ref_id' => 5, 'mean' => 4.74, 'sd' => 0.36],
|
||||
['control_test_id' => 553, 'control_ref_id' => 175, 'test_ref_id' => 19, 'mean' => 3.05, 'sd' => 0.457],
|
||||
['control_test_id' => 554, 'control_ref_id' => 174, 'test_ref_id' => 19, 'mean' => 12.53, 'sd' => 1.88],
|
||||
['control_test_id' => 555, 'control_ref_id' => 176, 'test_ref_id' => 43, 'mean' => 34.29, 'sd' => 2.8],
|
||||
['control_test_id' => 559, 'control_ref_id' => 163, 'test_ref_id' => 53, 'mean' => 4.46, 'sd' => 0.22],
|
||||
['control_test_id' => 560, 'control_ref_id' => 173, 'test_ref_id' => 53, 'mean' => 9.08, 'sd' => 0.47],
|
||||
['control_test_id' => 577, 'control_ref_id' => 180, 'test_ref_id' => 32, 'mean' => 5.12, 'sd' => 0.256],
|
||||
['control_test_id' => 584, 'control_ref_id' => 184, 'test_ref_id' => 4, 'mean' => 1.33, 'sd' => 0.115],
|
||||
['control_test_id' => 585, 'control_ref_id' => 184, 'test_ref_id' => 11, 'mean' => 31.98, 'sd' => 2.325],
|
||||
['control_test_id' => 586, 'control_ref_id' => 184, 'test_ref_id' => 30, 'mean' => 145, 'sd' => 14.5],
|
||||
['control_test_id' => 587, 'control_ref_id' => 185, 'test_ref_id' => 17, 'mean' => 5.67, 'sd' => 0.15],
|
||||
['control_test_id' => 588, 'control_ref_id' => 186, 'test_ref_id' => 17, 'mean' => 10.29, 'sd' => 0.25],
|
||||
['control_test_id' => 477, 'control_ref_id' => 143, 'test_ref_id' => 8, 'mean' => 10.4, 'sd' => 0.7],
|
||||
['control_test_id' => 478, 'control_ref_id' => 143, 'test_ref_id' => 6, 'mean' => 125, 'sd' => 9],
|
||||
['control_test_id' => 499, 'control_ref_id' => 147, 'test_ref_id' => 40, 'mean' => 2.7, 'sd' => 0.36],
|
||||
['control_test_id' => 500, 'control_ref_id' => 147, 'test_ref_id' => 41, 'mean' => 1.125, 'sd' => 0.15],
|
||||
['control_test_id' => 501, 'control_ref_id' => 147, 'test_ref_id' => 42, 'mean' => 4.38, 'sd' => 0.58],
|
||||
['control_test_id' => 502, 'control_ref_id' => 147, 'test_ref_id' => 43, 'mean' => 10.09, 'sd' => 1.35],
|
||||
['control_test_id' => 503, 'control_ref_id' => 147, 'test_ref_id' => 44, 'mean' => 29.43, 'sd' => 3.92],
|
||||
['control_test_id' => 592, 'control_ref_id' => 187, 'test_ref_id' => 3, 'mean' => 2.19, 'sd' => 0.19],
|
||||
['control_test_id' => 362, 'control_ref_id' => 98, 'test_ref_id' => 3, 'mean' => 1.8, 'sd' => 0.232],
|
||||
['control_test_id' => 363, 'control_ref_id' => 98, 'test_ref_id' => 4, 'mean' => 5.02, 'sd' => 0.652],
|
||||
['control_test_id' => 469, 'control_ref_id' => 139, 'test_ref_id' => 20, 'mean' => 26.7, 'sd' => 3.75],
|
||||
['control_test_id' => 593, 'control_ref_id' => 187, 'test_ref_id' => 4, 'mean' => 4.63, 'sd' => 0.403],
|
||||
['control_test_id' => 594, 'control_ref_id' => 187, 'test_ref_id' => 10, 'mean' => 151, 'sd' => 12.5],
|
||||
['control_test_id' => 595, 'control_ref_id' => 187, 'test_ref_id' => 11, 'mean' => 88.7, 'sd' => 6.58],
|
||||
['control_test_id' => 596, 'control_ref_id' => 187, 'test_ref_id' => 30, 'mean' => 216, 'sd' => 21.5],
|
||||
['control_test_id' => 470, 'control_ref_id' => 140, 'test_ref_id' => 20, 'mean' => 104.8, 'sd' => 14.6],
|
||||
['control_test_id' => 531, 'control_ref_id' => 164, 'test_ref_id' => 13, 'mean' => 233, 'sd' => 11.65],
|
||||
['control_test_id' => 532, 'control_ref_id' => 164, 'test_ref_id' => 14, 'mean' => 174, 'sd' => 8.7],
|
||||
['control_test_id' => 557, 'control_ref_id' => 173, 'test_ref_id' => 36, 'mean' => 3.26, 'sd' => 0.18],
|
||||
['control_test_id' => 533, 'control_ref_id' => 164, 'test_ref_id' => 15, 'mean' => 61, 'sd' => 3.05],
|
||||
['control_test_id' => 534, 'control_ref_id' => 164, 'test_ref_id' => 16, 'mean' => 147, 'sd' => 7.35],
|
||||
['control_test_id' => 384, 'control_ref_id' => 98, 'test_ref_id' => 11, 'mean' => 95.6, 'sd' => 10.625],
|
||||
['control_test_id' => 581, 'control_ref_id' => 183, 'test_ref_id' => 18, 'mean' => 1.94, 'sd' => 0.26],
|
||||
['control_test_id' => 579, 'control_ref_id' => 181, 'test_ref_id' => 17, 'mean' => 5.775, 'sd' => 0.15],
|
||||
['control_test_id' => 365, 'control_ref_id' => 98, 'test_ref_id' => 10, 'mean' => 188, 'sd' => 23.5],
|
||||
['control_test_id' => 367, 'control_ref_id' => 98, 'test_ref_id' => 21, 'mean' => 12.6, 'sd' => 0.675],
|
||||
['control_test_id' => 368, 'control_ref_id' => 98, 'test_ref_id' => 29, 'mean' => 7.66, 'sd' => 0.69],
|
||||
['control_test_id' => 369, 'control_ref_id' => 98, 'test_ref_id' => 31, 'mean' => 111, 'sd' => 11.025],
|
||||
['control_test_id' => 370, 'control_ref_id' => 98, 'test_ref_id' => 30, 'mean' => 252, 'sd' => 25.25],
|
||||
['control_test_id' => 505, 'control_ref_id' => 148, 'test_ref_id' => 40, 'mean' => 27, 'sd' => 3.6],
|
||||
['control_test_id' => 506, 'control_ref_id' => 148, 'test_ref_id' => 41, 'mean' => 15.277, 'sd' => 2.037],
|
||||
['control_test_id' => 507, 'control_ref_id' => 148, 'test_ref_id' => 42, 'mean' => 47.6, 'sd' => 6.35],
|
||||
['control_test_id' => 508, 'control_ref_id' => 148, 'test_ref_id' => 43, 'mean' => 58.51, 'sd' => 7.8],
|
||||
['control_test_id' => 509, 'control_ref_id' => 148, 'test_ref_id' => 44, 'mean' => 193.03, 'sd' => 25.74],
|
||||
['control_test_id' => 510, 'control_ref_id' => 149, 'test_ref_id' => 45, 'mean' => 0.001, 'sd' => 0.017],
|
||||
['control_test_id' => 511, 'control_ref_id' => 150, 'test_ref_id' => 45, 'mean' => 0.581, 'sd' => 0.077],
|
||||
['control_test_id' => 512, 'control_ref_id' => 151, 'test_ref_id' => 46, 'mean' => 0, 'sd' => 1.67],
|
||||
['control_test_id' => 513, 'control_ref_id' => 152, 'test_ref_id' => 46, 'mean' => 19.98, 'sd' => 3],
|
||||
['control_test_id' => 538, 'control_ref_id' => 167, 'test_ref_id' => 52, 'mean' => 1.08, 'sd' => 0.1],
|
||||
['control_test_id' => 539, 'control_ref_id' => 168, 'test_ref_id' => 39, 'mean' => 6.6485, 'sd' => 0.7125],
|
||||
['control_test_id' => 540, 'control_ref_id' => 160, 'test_ref_id' => 50, 'mean' => 34.4, 'sd' => 3.51],
|
||||
['control_test_id' => 552, 'control_ref_id' => 173, 'test_ref_id' => 32, 'mean' => 8.01, 'sd' => 0.6],
|
||||
['control_test_id' => 572, 'control_ref_id' => 180, 'test_ref_id' => 9, 'mean' => 101.5, 'sd' => 7.75],
|
||||
['control_test_id' => 578, 'control_ref_id' => 180, 'test_ref_id' => 5, 'mean' => 3.16, 'sd' => 0.158],
|
||||
['control_test_id' => 571, 'control_ref_id' => 180, 'test_ref_id' => 7, 'mean' => 1.08, 'sd' => 0.0825],
|
||||
['control_test_id' => 573, 'control_ref_id' => 180, 'test_ref_id' => 8, 'mean' => 5.225, 'sd' => 0.3525],
|
||||
['control_test_id' => 574, 'control_ref_id' => 180, 'test_ref_id' => 6, 'mean' => 40.7, 'sd' => 3.05],
|
||||
['control_test_id' => 590, 'control_ref_id' => 180, 'test_ref_id' => 53, 'mean' => 4.52, 'sd' => 0.22],
|
||||
['control_test_id' => 591, 'control_ref_id' => 180, 'test_ref_id' => 36, 'mean' => 2.2, 'sd' => 0.083],
|
||||
];
|
||||
$this->db->table('control_tests')->insertBatch($data);
|
||||
|
||||
$data = [
|
||||
['result_id' => 28, 'control_ref_id' => 1, 'test_ref_id' => 1, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '152.868220727426', 'rescomment' => null],
|
||||
['result_id' => 29, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '118.684360070769', 'rescomment' => null],
|
||||
['result_id' => 30, 'control_ref_id' => 1, 'test_ref_id' => 3, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.78676744546881', 'rescomment' => null],
|
||||
['result_id' => 31, 'control_ref_id' => 1, 'test_ref_id' => 4, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.88788440216215', 'rescomment' => null],
|
||||
['result_id' => 32, 'control_ref_id' => 1, 'test_ref_id' => 5, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.54649638396286', 'rescomment' => null],
|
||||
['result_id' => 33, 'control_ref_id' => 1, 'test_ref_id' => 6, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '68.9277965359498', 'rescomment' => null],
|
||||
['result_id' => 34, 'control_ref_id' => 1, 'test_ref_id' => 7, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.7616948568887', 'rescomment' => null],
|
||||
['result_id' => 35, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '8.2805131289407', 'rescomment' => null],
|
||||
['result_id' => 36, 'control_ref_id' => 1, 'test_ref_id' => 9, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '255.718750547819', 'rescomment' => null],
|
||||
['result_id' => 37, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '185.86621742091', 'rescomment' => null],
|
||||
['result_id' => 38, 'control_ref_id' => 1, 'test_ref_id' => 11, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '96.0370738667909', 'rescomment' => null],
|
||||
['result_id' => 39, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '7.26130670682815', 'rescomment' => null],
|
||||
['result_id' => 40, 'control_ref_id' => 2, 'test_ref_id' => 13, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '158.320085275538', 'rescomment' => null],
|
||||
['result_id' => 41, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '123.642030022172', 'rescomment' => null],
|
||||
['result_id' => 42, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '36.9505985250087', 'rescomment' => null],
|
||||
['result_id' => 43, 'control_ref_id' => 2, 'test_ref_id' => 16, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '96.5175241884342', 'rescomment' => null],
|
||||
['result_id' => 44, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '110.353601999312', 'rescomment' => null],
|
||||
['result_id' => 45, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '40.9599586941077', 'rescomment' => null],
|
||||
['result_id' => 46, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1528049849.3', 'rescomment' => null],
|
||||
['result_id' => 47, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '5.1', 'rescomment' => null],
|
||||
['result_id' => 48, 'control_ref_id' => 4, 'test_ref_id' => 13, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '215.383147793402', 'rescomment' => null],
|
||||
['result_id' => 49, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '183.063991935605', 'rescomment' => null],
|
||||
['result_id' => 50, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '52.9496711121656', 'rescomment' => null],
|
||||
['result_id' => 51, 'control_ref_id' => 4, 'test_ref_id' => 16, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '133.934963501757', 'rescomment' => null],
|
||||
['result_id' => 52, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '158.484164984758', 'rescomment' => null],
|
||||
['result_id' => 53, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '56.8818601408997', 'rescomment' => null],
|
||||
['result_id' => 54, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1518899850.3', 'rescomment' => null],
|
||||
['result_id' => 55, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '10.5', 'rescomment' => null],
|
||||
['result_id' => 56, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '10.5', 'rescomment' => null],
|
||||
['result_id' => 57, 'control_ref_id' => 6, 'test_ref_id' => 1, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '48.9562924866303', 'rescomment' => null],
|
||||
['result_id' => 58, 'control_ref_id' => 6, 'test_ref_id' => 2, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '52.1440402715625', 'rescomment' => null],
|
||||
['result_id' => 59, 'control_ref_id' => 6, 'test_ref_id' => 3, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '0.827525226237078', 'rescomment' => null],
|
||||
['result_id' => 60, 'control_ref_id' => 6, 'test_ref_id' => 4, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.43268122033837', 'rescomment' => null],
|
||||
['result_id' => 61, 'control_ref_id' => 6, 'test_ref_id' => 5, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '3.32791020761993', 'rescomment' => null],
|
||||
['result_id' => 62, 'control_ref_id' => 6, 'test_ref_id' => 6, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '20.1899916884103', 'rescomment' => null],
|
||||
['result_id' => 63, 'control_ref_id' => 6, 'test_ref_id' => 7, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '0.942164469720469', 'rescomment' => null],
|
||||
['result_id' => 64, 'control_ref_id' => 6, 'test_ref_id' => 8, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.92672863627875', 'rescomment' => null],
|
||||
['result_id' => 65, 'control_ref_id' => 6, 'test_ref_id' => 9, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '97.9752041498986', 'rescomment' => null],
|
||||
['result_id' => 66, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '82', 'rescomment' => null],
|
||||
['result_id' => 67, 'control_ref_id' => 6, 'test_ref_id' => 11, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '55.6067623790927', 'rescomment' => null],
|
||||
['result_id' => 68, 'control_ref_id' => 7, 'test_ref_id' => 18, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.3534129858017', 'rescomment' => null],
|
||||
['result_id' => 69, 'control_ref_id' => 8, 'test_ref_id' => 18, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '2.92454624176025', 'rescomment' => null],
|
||||
['result_id' => 70, 'control_ref_id' => 9, 'test_ref_id' => 1, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '41.9241811453099', 'rescomment' => null],
|
||||
['result_id' => 71, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '50.2606047539432', 'rescomment' => null],
|
||||
['result_id' => 72, 'control_ref_id' => 9, 'test_ref_id' => 3, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '0.618850628140718', 'rescomment' => null],
|
||||
['result_id' => 73, 'control_ref_id' => 9, 'test_ref_id' => 4, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.64659256568823', 'rescomment' => null],
|
||||
['result_id' => 74, 'control_ref_id' => 9, 'test_ref_id' => 5, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '3.37936528279952', 'rescomment' => null],
|
||||
['result_id' => 75, 'control_ref_id' => 9, 'test_ref_id' => 6, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '19.9687141929191', 'rescomment' => null],
|
||||
['result_id' => 76, 'control_ref_id' => 9, 'test_ref_id' => 7, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.25401465739322', 'rescomment' => null],
|
||||
['result_id' => 77, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '5.20068765776226', 'rescomment' => null],
|
||||
['result_id' => 78, 'control_ref_id' => 9, 'test_ref_id' => 9, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '85.8656412403935', 'rescomment' => null],
|
||||
['result_id' => 79, 'control_ref_id' => 9, 'test_ref_id' => 10, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '73.7679469620088', 'rescomment' => null],
|
||||
['result_id' => 80, 'control_ref_id' => 9, 'test_ref_id' => 11, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '32.6032920741649', 'rescomment' => null],
|
||||
['result_id' => 81, 'control_ref_id' => 9, 'test_ref_id' => 12, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.60016347657495', 'rescomment' => null],
|
||||
['result_id' => 82, 'control_ref_id' => 10, 'test_ref_id' => 19, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '3.07556349669703', 'rescomment' => null],
|
||||
['result_id' => 83, 'control_ref_id' => 11, 'test_ref_id' => 19, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '14.2416023954004', 'rescomment' => null],
|
||||
['result_id' => 84, 'control_ref_id' => 1, 'test_ref_id' => 1, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '151.987144896542', 'rescomment' => null],
|
||||
['result_id' => 85, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '118.037436722317', 'rescomment' => null],
|
||||
['result_id' => 86, 'control_ref_id' => 1, 'test_ref_id' => 3, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.7947151556153', 'rescomment' => null],
|
||||
['result_id' => 87, 'control_ref_id' => 1, 'test_ref_id' => 4, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.73906612533412', 'rescomment' => null],
|
||||
['result_id' => 88, 'control_ref_id' => 1, 'test_ref_id' => 5, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.60361234521986', 'rescomment' => null],
|
||||
['result_id' => 89, 'control_ref_id' => 1, 'test_ref_id' => 6, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '72.6643107473908', 'rescomment' => null],
|
||||
['result_id' => 90, 'control_ref_id' => 1, 'test_ref_id' => 7, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.63980962604914', 'rescomment' => null],
|
||||
['result_id' => 91, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '8.38141623646488', 'rescomment' => null],
|
||||
['result_id' => 92, 'control_ref_id' => 1, 'test_ref_id' => 9, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '264.978406417832', 'rescomment' => null],
|
||||
['result_id' => 93, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '181.555224534248', 'rescomment' => null],
|
||||
['result_id' => 94, 'control_ref_id' => 1, 'test_ref_id' => 11, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '96.2859292539928', 'rescomment' => null],
|
||||
['result_id' => 95, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '6.9194402361869', 'rescomment' => null],
|
||||
['result_id' => 96, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '110.231586595837', 'rescomment' => null],
|
||||
['result_id' => 97, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '7.83563261140536', 'rescomment' => null],
|
||||
['result_id' => 98, 'control_ref_id' => 2, 'test_ref_id' => 13, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '162.230018707428', 'rescomment' => null],
|
||||
['result_id' => 99, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '130.52137139783', 'rescomment' => null],
|
||||
['result_id' => 100, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '40.1447263115978', 'rescomment' => null],
|
||||
['result_id' => 101, 'control_ref_id' => 2, 'test_ref_id' => 16, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '101.152360213402', 'rescomment' => null],
|
||||
['result_id' => 102, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '102.256365254374', 'rescomment' => null],
|
||||
['result_id' => 103, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '104.629167708808', 'rescomment' => null],
|
||||
['result_id' => 104, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '82.6663002141555', 'rescomment' => null],
|
||||
['result_id' => 105, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '116.848292060971', 'rescomment' => null],
|
||||
['result_id' => 106, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '6', 'rescomment' => null],
|
||||
['result_id' => 107, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.9', 'rescomment' => null],
|
||||
['result_id' => 108, 'control_ref_id' => 4, 'test_ref_id' => 13, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '225.968857118997', 'rescomment' => null],
|
||||
['result_id' => 109, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '174.601513698878', 'rescomment' => null],
|
||||
['result_id' => 110, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '60.5069551717738', 'rescomment' => null],
|
||||
['result_id' => 111, 'control_ref_id' => 4, 'test_ref_id' => 16, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '139.447942846202', 'rescomment' => null],
|
||||
['result_id' => 112, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '143.31010007369', 'rescomment' => null],
|
||||
['result_id' => 113, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '144.761372351536', 'rescomment' => null],
|
||||
['result_id' => 114, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '115.071814150515', 'rescomment' => null],
|
||||
['result_id' => 115, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '161.317934555282', 'rescomment' => null],
|
||||
['result_id' => 116, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '10.2', 'rescomment' => null],
|
||||
['result_id' => 117, 'control_ref_id' => 6, 'test_ref_id' => 1, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '49.5516111762261', 'rescomment' => null],
|
||||
['result_id' => 118, 'control_ref_id' => 6, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '52.3007836764823', 'rescomment' => null],
|
||||
['result_id' => 119, 'control_ref_id' => 6, 'test_ref_id' => 3, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '0.826492230714472', 'rescomment' => null],
|
||||
['result_id' => 120, 'control_ref_id' => 6, 'test_ref_id' => 4, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.4488869704763', 'rescomment' => null],
|
||||
['result_id' => 121, 'control_ref_id' => 6, 'test_ref_id' => 5, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '3.44673661899837', 'rescomment' => null],
|
||||
['result_id' => 122, 'control_ref_id' => 6, 'test_ref_id' => 6, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '20.730181610828', 'rescomment' => null],
|
||||
['result_id' => 123, 'control_ref_id' => 6, 'test_ref_id' => 7, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '0.931063374396039', 'rescomment' => null],
|
||||
['result_id' => 124, 'control_ref_id' => 6, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.90811693843901', 'rescomment' => null],
|
||||
['result_id' => 125, 'control_ref_id' => 6, 'test_ref_id' => 9, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '101.930759918121', 'rescomment' => null],
|
||||
['result_id' => 126, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '75.5729386133723', 'rescomment' => null],
|
||||
['result_id' => 127, 'control_ref_id' => 6, 'test_ref_id' => 11, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '56.5900268982851', 'rescomment' => null],
|
||||
['result_id' => 128, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '102.528037735786', 'rescomment' => null],
|
||||
['result_id' => 129, 'control_ref_id' => 12, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '51.0905355215073', 'rescomment' => null],
|
||||
['result_id' => 130, 'control_ref_id' => 12, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '37.0758876204491', 'rescomment' => null],
|
||||
['result_id' => 131, 'control_ref_id' => 13, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '104.115432500839', 'rescomment' => null],
|
||||
['result_id' => 132, 'control_ref_id' => 13, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '126.745772361755', 'rescomment' => null],
|
||||
['result_id' => 133, 'control_ref_id' => 9, 'test_ref_id' => 1, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '44.2766951686621', 'rescomment' => null],
|
||||
['result_id' => 134, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '52.3172542490699', 'rescomment' => null],
|
||||
['result_id' => 135, 'control_ref_id' => 9, 'test_ref_id' => 3, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '0.611564331171467', 'rescomment' => null],
|
||||
['result_id' => 136, 'control_ref_id' => 9, 'test_ref_id' => 4, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.61071374749995', 'rescomment' => null],
|
||||
['result_id' => 137, 'control_ref_id' => 9, 'test_ref_id' => 5, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '3.40677107339127', 'rescomment' => null],
|
||||
['result_id' => 138, 'control_ref_id' => 9, 'test_ref_id' => 6, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '20.3040445233696', 'rescomment' => null],
|
||||
['result_id' => 139, 'control_ref_id' => 9, 'test_ref_id' => 7, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.18843758036108', 'rescomment' => null],
|
||||
['result_id' => 140, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '5.30604556512929', 'rescomment' => null],
|
||||
['result_id' => 141, 'control_ref_id' => 9, 'test_ref_id' => 9, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '89.4499417480113', 'rescomment' => null],
|
||||
['result_id' => 142, 'control_ref_id' => 9, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '74.0960687372425', 'rescomment' => null],
|
||||
['result_id' => 143, 'control_ref_id' => 9, 'test_ref_id' => 11, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '32.4566384751244', 'rescomment' => null],
|
||||
['result_id' => 144, 'control_ref_id' => 9, 'test_ref_id' => 12, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.48437909776323', 'rescomment' => null],
|
||||
['result_id' => 145, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '50.9667682640676', 'rescomment' => null],
|
||||
['result_id' => 146, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.93310398630639', 'rescomment' => null],
|
||||
['result_id' => 147, 'control_ref_id' => 1, 'test_ref_id' => 1, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '145.49911151053', 'rescomment' => null],
|
||||
['result_id' => 148, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '113.383579655159', 'rescomment' => null],
|
||||
['result_id' => 149, 'control_ref_id' => 1, 'test_ref_id' => 3, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.78951109759924', 'rescomment' => null],
|
||||
['result_id' => 150, 'control_ref_id' => 1, 'test_ref_id' => 4, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.93788942107251', 'rescomment' => null],
|
||||
['result_id' => 151, 'control_ref_id' => 1, 'test_ref_id' => 5, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.6838893149483', 'rescomment' => null],
|
||||
['result_id' => 152, 'control_ref_id' => 1, 'test_ref_id' => 6, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '71.2844487736166', 'rescomment' => null],
|
||||
['result_id' => 153, 'control_ref_id' => 1, 'test_ref_id' => 7, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.66921401448631', 'rescomment' => null],
|
||||
['result_id' => 154, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '7.75508570982025', 'rescomment' => null],
|
||||
['result_id' => 155, 'control_ref_id' => 1, 'test_ref_id' => 9, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '259.337398768362', 'rescomment' => null],
|
||||
['result_id' => 156, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '237.342516597661', 'rescomment' => null],
|
||||
['result_id' => 157, 'control_ref_id' => 1, 'test_ref_id' => 11, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '95.377689131424', 'rescomment' => null],
|
||||
['result_id' => 158, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '6.33840481787399', 'rescomment' => null],
|
||||
['result_id' => 159, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '254.058423430058', 'rescomment' => null],
|
||||
['result_id' => 160, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '7.61797516866643', 'rescomment' => null],
|
||||
['result_id' => 161, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '243.755676108857', 'rescomment' => null],
|
||||
['result_id' => 162, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '193.063562956371', 'rescomment' => null],
|
||||
['result_id' => 163, 'control_ref_id' => 2, 'test_ref_id' => 13, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '156.243798489035', 'rescomment' => null],
|
||||
['result_id' => 164, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '119.907027927493', 'rescomment' => null],
|
||||
['result_id' => 165, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '40.447059699393', 'rescomment' => null],
|
||||
['result_id' => 166, 'control_ref_id' => 2, 'test_ref_id' => 16, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '102.231291242292', 'rescomment' => null],
|
||||
['result_id' => 167, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '5.3', 'rescomment' => null],
|
||||
['result_id' => 168, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1921499810', 'rescomment' => null],
|
||||
['result_id' => 169, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '5.1', 'rescomment' => null],
|
||||
['result_id' => 170, 'control_ref_id' => 4, 'test_ref_id' => 13, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '219.038828305261', 'rescomment' => null],
|
||||
['result_id' => 171, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '166.155678322747', 'rescomment' => null],
|
||||
['result_id' => 172, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '56.8925909121002', 'rescomment' => null],
|
||||
['result_id' => 173, 'control_ref_id' => 4, 'test_ref_id' => 16, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '140.823342734069', 'rescomment' => null],
|
||||
['result_id' => 174, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '10.2', 'rescomment' => null],
|
||||
['result_id' => 175, 'control_ref_id' => 6, 'test_ref_id' => 1, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '48.9587059015567', 'rescomment' => null],
|
||||
['result_id' => 176, 'control_ref_id' => 6, 'test_ref_id' => 2, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '49.1468267441428', 'rescomment' => null],
|
||||
['result_id' => 177, 'control_ref_id' => 6, 'test_ref_id' => 3, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '0.799690212662777', 'rescomment' => null],
|
||||
['result_id' => 178, 'control_ref_id' => 6, 'test_ref_id' => 4, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.45552498470141', 'rescomment' => null],
|
||||
['result_id' => 179, 'control_ref_id' => 6, 'test_ref_id' => 5, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '3.41160793396446', 'rescomment' => null],
|
||||
['result_id' => 180, 'control_ref_id' => 6, 'test_ref_id' => 6, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '20.2910405834124', 'rescomment' => null],
|
||||
['result_id' => 181, 'control_ref_id' => 6, 'test_ref_id' => 7, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '0.94094934133914', 'rescomment' => null],
|
||||
['result_id' => 182, 'control_ref_id' => 6, 'test_ref_id' => 8, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.49152415745592', 'rescomment' => null],
|
||||
['result_id' => 183, 'control_ref_id' => 6, 'test_ref_id' => 9, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '100.024475839182', 'rescomment' => null],
|
||||
['result_id' => 184, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '85.9679935713317', 'rescomment' => null],
|
||||
['result_id' => 185, 'control_ref_id' => 6, 'test_ref_id' => 11, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '53.8509856427683', 'rescomment' => null],
|
||||
['result_id' => 186, 'control_ref_id' => 9, 'test_ref_id' => 1, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '42.7806264740846', 'rescomment' => null],
|
||||
['result_id' => 187, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '51.9985050454933', 'rescomment' => null],
|
||||
['result_id' => 188, 'control_ref_id' => 9, 'test_ref_id' => 3, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '0.649795363795779', 'rescomment' => null],
|
||||
['result_id' => 189, 'control_ref_id' => 9, 'test_ref_id' => 4, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.80260045112525', 'rescomment' => null],
|
||||
['result_id' => 190, 'control_ref_id' => 9, 'test_ref_id' => 5, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '3.35288773545917', 'rescomment' => null],
|
||||
['result_id' => 191, 'control_ref_id' => 9, 'test_ref_id' => 6, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '20.6904621653494', 'rescomment' => null],
|
||||
['result_id' => 192, 'control_ref_id' => 9, 'test_ref_id' => 7, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.19703451489628', 'rescomment' => null],
|
||||
['result_id' => 193, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.89184138396658', 'rescomment' => null],
|
||||
['result_id' => 194, 'control_ref_id' => 9, 'test_ref_id' => 9, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '91.9123811518993', 'rescomment' => null],
|
||||
['result_id' => 195, 'control_ref_id' => 9, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '100.544617296457', 'rescomment' => null],
|
||||
['result_id' => 196, 'control_ref_id' => 9, 'test_ref_id' => 11, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '31.390065068111', 'rescomment' => null],
|
||||
['result_id' => 197, 'control_ref_id' => 9, 'test_ref_id' => 12, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.17808071506795', 'rescomment' => null],
|
||||
];
|
||||
$this->db->table('results')->insertBatch($data);
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ControlModel extends BaseModel
|
||||
{
|
||||
protected $table = 'dict_controls';
|
||||
protected $primaryKey = 'control_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['dept_ref_id', 'name', 'lot', 'producer', 'expdate'];
|
||||
|
||||
public function getByDept($deptId)
|
||||
{
|
||||
return $this->where('dept_ref_id', $deptId)->findAll();
|
||||
}
|
||||
|
||||
public function getWithDept()
|
||||
{
|
||||
$builder = $this->db->table('dict_controls c');
|
||||
$builder->select('c.*, d.name as dept_name');
|
||||
$builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getActiveByDate($date, $deptId = null)
|
||||
{
|
||||
$builder = $this->db->table('dict_controls c');
|
||||
$builder->select('c.*');
|
||||
$builder->where('c.expdate >=', $date);
|
||||
if ($deptId) {
|
||||
$builder->where('c.dept_ref_id', $deptId);
|
||||
}
|
||||
$builder->orderBy('c.name', 'ASC');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ControlTestModel extends BaseModel
|
||||
{
|
||||
protected $table = 'control_tests';
|
||||
protected $primaryKey = 'control_test_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['control_ref_id', 'test_ref_id', 'mean', 'sd'];
|
||||
|
||||
public function getByControl($controlId)
|
||||
{
|
||||
$builder = $this->db->table('control_tests ct');
|
||||
$builder->select('ct.*, t.name as test_name, t.unit');
|
||||
$builder->join('dict_tests t', 't.test_id = ct.test_ref_id');
|
||||
$builder->where('ct.control_ref_id', $controlId);
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getByControlAndTest($controlId, $testId)
|
||||
{
|
||||
return $this->where('control_ref_id', $controlId)
|
||||
->where('test_ref_id', $testId)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class DailyResultModel extends BaseModel
|
||||
{
|
||||
protected $table = 'daily_result';
|
||||
protected $primaryKey = 'daily_result_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['control_ref_id', 'test_ref_id', 'resdate', 'resvalue', 'rescomment'];
|
||||
|
||||
public function getByMonth($controlId, $testId, $yearMonth)
|
||||
{
|
||||
$startDate = $yearMonth . '-01';
|
||||
$endDate = $yearMonth . '-31';
|
||||
|
||||
$builder = $this->db->table('daily_result');
|
||||
$builder->select('*');
|
||||
$builder->where('control_ref_id', $controlId);
|
||||
$builder->where('test_ref_id', $testId);
|
||||
$builder->where('resdate >=', $startDate);
|
||||
$builder->where('resdate <=', $endDate);
|
||||
$builder->orderBy('resdate', 'ASC');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getByControlMonth($controlId, $yearMonth)
|
||||
{
|
||||
$startDate = $yearMonth . '-01';
|
||||
$endDate = $yearMonth . '-31';
|
||||
|
||||
$builder = $this->db->table('daily_result');
|
||||
$builder->select('*');
|
||||
$builder->where('control_ref_id', $controlId);
|
||||
$builder->where('resdate >=', $startDate);
|
||||
$builder->where('resdate <=', $endDate);
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function saveResult($data)
|
||||
{
|
||||
$builder = $this->db->table('daily_result');
|
||||
$existing = $builder->select('*')
|
||||
->where('control_ref_id', $data['control_ref_id'])
|
||||
->where('test_ref_id', $data['test_ref_id'])
|
||||
->where('resdate', $data['resdate'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($existing) {
|
||||
return $builder->where('daily_result_id', $existing['daily_result_id'])->update($data);
|
||||
} else {
|
||||
return $builder->insert($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class DeptModel extends BaseModel
|
||||
{
|
||||
protected $table = 'dict_depts';
|
||||
protected $primaryKey = 'dept_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['name'];
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class DictControlModel extends BaseModel
|
||||
{
|
||||
protected $table = 'dict_controls';
|
||||
protected $primaryKey = 'control_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['dept_ref_id', 'name', 'lot', 'producer', 'expdate'];
|
||||
|
||||
public function getByDept($deptId)
|
||||
{
|
||||
return $this->where('dept_ref_id', $deptId)->findAll();
|
||||
}
|
||||
|
||||
public function getWithDept($keyword = null, $deptId = null)
|
||||
{
|
||||
$builder = $this->db->table('dict_controls c');
|
||||
$builder->select('c.*, d.name as dept_name');
|
||||
$builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left');
|
||||
|
||||
if ($keyword) {
|
||||
$builder->groupStart();
|
||||
$builder->like('c.name', $keyword);
|
||||
$builder->orLike('c.lot', $keyword);
|
||||
$builder->groupEnd();
|
||||
}
|
||||
|
||||
if ($deptId) {
|
||||
$builder->where('c.dept_ref_id', $deptId);
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getActiveByDate($date, $deptId = null)
|
||||
{
|
||||
$builder = $this->db->table('dict_controls c');
|
||||
$builder->select('c.*');
|
||||
$builder->where('c.expdate >=', $date);
|
||||
if ($deptId) {
|
||||
$builder->where('c.dept_ref_id', $deptId);
|
||||
}
|
||||
$builder->orderBy('c.name', 'ASC');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class DictDeptModel extends BaseModel
|
||||
{
|
||||
protected $table = 'dict_depts';
|
||||
protected $primaryKey = 'dept_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['name'];
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class DictTestModel extends BaseModel
|
||||
{
|
||||
protected $table = 'dict_tests';
|
||||
protected $primaryKey = 'test_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['dept_ref_id', 'name', 'unit', 'method', 'cva', 'ba', 'tea'];
|
||||
|
||||
public function getByDept($deptId)
|
||||
{
|
||||
return $this->where('dept_ref_id', $deptId)->findAll();
|
||||
}
|
||||
|
||||
public function getWithDept()
|
||||
{
|
||||
$builder = $this->db->table('dict_tests t');
|
||||
$builder->select('t.*, d.name as dept_name');
|
||||
$builder->join('dict_depts d', 'd.dept_id = t.dept_ref_id', 'left');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
}
|
||||
32
app/Models/Master/MasterControlsModel.php
Normal file
32
app/Models/Master/MasterControlsModel.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace App\Models\Master;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class MasterControlsModel extends BaseModel {
|
||||
protected $table = 'master_controls';
|
||||
protected $primaryKey = 'control_id';
|
||||
protected $allowedFields = [
|
||||
'dept_id',
|
||||
'name',
|
||||
'lot',
|
||||
'producer',
|
||||
'exp_date',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('name', $keyword)
|
||||
->orLike('lot', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
27
app/Models/Master/MasterDeptsModel.php
Normal file
27
app/Models/Master/MasterDeptsModel.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace App\Models\Master;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class MasterDeptsModel extends BaseModel {
|
||||
protected $table = 'master_depts';
|
||||
protected $primaryKey = 'dept_id';
|
||||
protected $allowedFields = [
|
||||
'name',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('name', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
33
app/Models/Master/MasterTestsModel.php
Normal file
33
app/Models/Master/MasterTestsModel.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace App\Models\Master;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class MasterTestsModel extends BaseModel {
|
||||
protected $table = 'master_tests';
|
||||
protected $primaryKey = 'test_id';
|
||||
protected $allowedFields = [
|
||||
'dept_id',
|
||||
'name',
|
||||
'unit',
|
||||
'method',
|
||||
'cva',
|
||||
'ba',
|
||||
'tea',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('name', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class MonthlyCommentModel extends BaseModel
|
||||
{
|
||||
protected $table = 'monthly_comment';
|
||||
protected $primaryKey = 'monthly_comment_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['control_ref_id', 'test_ref_id', 'commonth', 'comtext'];
|
||||
|
||||
public function getByControlTestMonth($controlId, $testId, $yearMonth)
|
||||
{
|
||||
return $this->where('control_ref_id', $controlId)
|
||||
->where('test_ref_id', $testId)
|
||||
->where('commonth', $yearMonth)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function saveComment($data)
|
||||
{
|
||||
$existing = $this->where('control_ref_id', $data['control_ref_id'])
|
||||
->where('test_ref_id', $data['test_ref_id'])
|
||||
->where('commonth', $data['commonth'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return $this->update($existing['monthly_comment_id'], $data);
|
||||
} else {
|
||||
return $this->insert($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/Models/Qc/ControlTestsModel.php
Normal file
30
app/Models/Qc/ControlTestsModel.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace App\Models\Qc;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ControlTestsModel extends BaseModel {
|
||||
protected $table = 'control_tests';
|
||||
protected $primaryKey = 'control_test_id';
|
||||
protected $allowedFields = [
|
||||
'control_id',
|
||||
'test_id',
|
||||
'mean',
|
||||
'sd',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('mean', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
31
app/Models/Qc/ResultCommentsModel.php
Normal file
31
app/Models/Qc/ResultCommentsModel.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace App\Models\Qc;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ResultCommentsModel extends BaseModel {
|
||||
protected $table = 'result_comments';
|
||||
protected $primaryKey = 'result_comment_id';
|
||||
protected $allowedFields = [
|
||||
'control_id',
|
||||
'test_id',
|
||||
'comment_month',
|
||||
'com_text',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('comment_month', $keyword)
|
||||
->orLike('com_text', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
31
app/Models/Qc/ResultsModel.php
Normal file
31
app/Models/Qc/ResultsModel.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace App\Models\Qc;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ResultsModel extends BaseModel {
|
||||
protected $table = 'results';
|
||||
protected $primaryKey = 'result_id';
|
||||
protected $allowedFields = [
|
||||
'control_id',
|
||||
'test_id',
|
||||
'res_date',
|
||||
'res_value',
|
||||
'res_comment',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
public function search($keyword = null) {
|
||||
if ($keyword) {
|
||||
return $this->groupStart()
|
||||
->like('res_value', $keyword)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ResultCommentModel extends BaseModel
|
||||
{
|
||||
protected $table = 'result_comments';
|
||||
protected $primaryKey = 'result_comment_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['control_ref_id', 'test_ref_id', 'commonth', 'comtext'];
|
||||
|
||||
public function getByControlTestMonth($controlId, $testId, $yearMonth)
|
||||
{
|
||||
return $this->where('control_ref_id', $controlId)
|
||||
->where('test_ref_id', $testId)
|
||||
->where('commonth', $yearMonth)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function saveComment($data)
|
||||
{
|
||||
$existing = $this->where('control_ref_id', $data['control_ref_id'])
|
||||
->where('test_ref_id', $data['test_ref_id'])
|
||||
->where('commonth', $data['commonth'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return $this->update($existing['result_comment_id'], $data);
|
||||
} else {
|
||||
return $this->insert($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ResultModel extends BaseModel
|
||||
{
|
||||
protected $table = 'results';
|
||||
protected $primaryKey = 'result_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['control_ref_id', 'test_ref_id', 'resdate', 'resvalue', 'rescomment'];
|
||||
|
||||
public function getByMonth($controlId, $testId, $yearMonth)
|
||||
{
|
||||
$startDate = $yearMonth . '-01';
|
||||
$endDate = $yearMonth . '-31';
|
||||
|
||||
$builder = $this->db->table('results');
|
||||
$builder->select('*');
|
||||
$builder->where('control_ref_id', $controlId);
|
||||
$builder->where('test_ref_id', $testId);
|
||||
$builder->where('resdate >=', $startDate);
|
||||
$builder->where('resdate <=', $endDate);
|
||||
$builder->orderBy('resdate', 'ASC');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getByControlMonth($controlId, $yearMonth)
|
||||
{
|
||||
$startDate = $yearMonth . '-01';
|
||||
$endDate = $yearMonth . '-31';
|
||||
|
||||
$builder = $this->db->table('results');
|
||||
$builder->select('*');
|
||||
$builder->where('control_ref_id', $controlId);
|
||||
$builder->where('resdate >=', $startDate);
|
||||
$builder->where('resdate <=', $endDate);
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function saveResult($data)
|
||||
{
|
||||
$builder = $this->db->table('results');
|
||||
$existing = $builder->select('*')
|
||||
->where('control_ref_id', $data['control_ref_id'])
|
||||
->where('test_ref_id', $data['test_ref_id'])
|
||||
->where('resdate', $data['resdate'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($existing) {
|
||||
return $builder->where('result_id', $existing['result_id'])->update($data);
|
||||
} else {
|
||||
return $builder->insert($data);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkExisting($controlId, $testId, $resdate)
|
||||
{
|
||||
$builder = $this->db->table('results');
|
||||
return $builder->select('result_id')
|
||||
->where('control_ref_id', $controlId)
|
||||
->where('test_ref_id', $testId)
|
||||
->where('resdate', $resdate)
|
||||
->get()
|
||||
->getRowArray();
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class TestModel extends BaseModel
|
||||
{
|
||||
protected $table = 'dict_tests';
|
||||
protected $primaryKey = 'test_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = ['dept_ref_id', 'name', 'unit', 'method', 'cva', 'ba', 'tea'];
|
||||
|
||||
public function getByDept($deptId)
|
||||
{
|
||||
return $this->where('dept_ref_id', $deptId)->findAll();
|
||||
}
|
||||
|
||||
public function getWithDept()
|
||||
{
|
||||
$builder = $this->db->table('dict_tests t');
|
||||
$builder->select('t.*, d.name as dept_name');
|
||||
$builder->join('dict_depts d', 'd.dept_id = t.dept_ref_id', 'left');
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
<!-- Backdrop -->
|
||||
<div x-show="showModal"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-40"
|
||||
@click="closeModal()">
|
||||
</div>
|
||||
|
||||
<!-- Modal Panel -->
|
||||
<div x-show="showModal"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl" @click.stop>
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-slate-800" x-text="form.control_id ? 'Edit Control' : 'Add Control'"></h3>
|
||||
<button @click="closeModal()" class="text-slate-400 hover:text-slate-600">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="save()">
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Department <span class="text-red-500">*</span></label>
|
||||
<select x-model="form.dept_ref_id" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" :class="{'border-red-300 bg-red-50': errors.dept_ref_id}" required>
|
||||
<option value="">Select Department</option>
|
||||
<template x-for="dept in depts" :key="dept.dept_id">
|
||||
<option :value="dept.dept_id" x-text="dept.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p x-show="errors.dept_ref_id" x-text="errors.dept_ref_id" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Control Name <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="form.name" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" :class="{'border-red-300 bg-red-50': errors.name}" placeholder="Enter control name" required>
|
||||
<p x-show="errors.name" x-text="errors.name" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Lot Number</label>
|
||||
<input type="text" x-model="form.lot" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Enter lot number">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Producer</label>
|
||||
<input type="text" x-model="form.producer" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Enter producer">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Expiry Date</label>
|
||||
<input type="date" x-model="form.expdate" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Assigned Tests</label>
|
||||
<div class="border border-slate-200 rounded-lg p-3 bg-slate-50 max-h-48 overflow-y-auto">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<template x-for="test in tests" :key="test.test_id">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" :value="test.test_id" x-model="form.test_ids" class="w-4 h-4 text-blue-600 rounded border-slate-300 focus:ring-blue-500">
|
||||
<span class="text-sm text-slate-700" x-text="test.name + ' (' + (test.dept_name || '') + ')'"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">Select tests to associate with this control</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50 rounded-b-2xl">
|
||||
<button type="button" @click="closeModal()" class="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-800 transition-colors">Cancel</button>
|
||||
<button type="submit" :disabled="loading" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<template x-if="!loading">
|
||||
<span><i class="fa-solid fa-check mr-1"></i> <span x-text="form.control_id ? 'Update' : 'Save'"></span></span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span><i class="fa-solid fa-spinner fa-spin mr-1"></i> Saving...</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-6"><?= isset($control) ? 'Edit Control' : 'Add New Control' ?></h2>
|
||||
|
||||
<form action="<?= isset($control) ? base_url('/control/update/' . $control['control_id']) : base_url('/control/save') ?>" method="post">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Department</label>
|
||||
<select name="deptid" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
<option value="">Select Department</option>
|
||||
<?php foreach ($depts as $dept): ?>
|
||||
<option value="<?= $dept['dept_id'] ?>" <?= (isset($control) && $control['dept_ref_id'] == $dept['dept_id']) ? 'selected' : '' ?>>
|
||||
<?= $dept['name'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Control Name</label>
|
||||
<input type="text" name="name" value="<?= $control['name'] ?? '' ?>" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Lot Number</label>
|
||||
<input type="text" name="lot" value="<?= $control['lot'] ?? '' ?>" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Expiry Date</label>
|
||||
<input type="date" name="expdate" value="<?= $control['expdate'] ?? '' ?>" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Producer</label>
|
||||
<textarea name="producer" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"><?= $control['producer'] ?? '' ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4 pt-4">
|
||||
<a href="<?= base_url('/control') ?>" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-lg">Cancel</a>
|
||||
<button type="submit" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,261 +0,0 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="controlIndex()">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Control Dictionary</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage control materials and lot numbers</p>
|
||||
</div>
|
||||
<button @click="showForm()" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus mr-2"></i>Add Control
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Card -->
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-4 mb-6">
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input type="text" x-model="keyword" @keyup.enter="fetchList()" class="w-full pl-10 pr-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Search controls...">
|
||||
</div>
|
||||
<select x-model="deptId" @change="fetchList()" class="select select-bordered select-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">All Departments</option>
|
||||
<template x-for="dept in depts" :key="dept.dept_id">
|
||||
<option :value="dept.dept_id" x-text="dept.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button @click="fetchList()" class="btn btn-primary">
|
||||
<i class="fa-solid fa-magnifying-glass mr-2"></i>Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table Card -->
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<template x-if="loading">
|
||||
<div class="p-12 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-blue-50 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fa-solid fa-spinner fa-spin text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm">Loading controls...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="!loading && (!list || list.length === 0)">
|
||||
<div class="flex-1 flex items-center justify-center p-8">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
|
||||
<i class="fa-solid fa-sliders text-slate-400 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm">No controls found</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Data Table -->
|
||||
<template x-if="!loading && list && list.length > 0">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="py-3 px-5 font-semibold">#</th>
|
||||
<th class="py-3 px-5 font-semibold">Name</th>
|
||||
<th class="py-3 px-5 font-semibold">Lot</th>
|
||||
<th class="py-3 px-5 font-semibold">Department</th>
|
||||
<th class="py-3 px-5 font-semibold">Status</th>
|
||||
<th class="py-3 px-5 font-semibold">Expiry Date</th>
|
||||
<th class="py-3 px-5 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<template x-for="(item, index) in list" :key="item.control_id">
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="py-3 px-5" x-text="index + 1"></td>
|
||||
<td class="py-3 px-5 font-medium text-slate-800" x-text="item.name"></td>
|
||||
<td class="py-3 px-5 text-slate-600">
|
||||
<span class="font-mono text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded" x-text="item.lot || '-'"></span>
|
||||
</td>
|
||||
<td class="py-3 px-5 text-slate-600" x-text="item.dept_name || '-'"></td>
|
||||
<td class="py-3 px-5">
|
||||
<span :class="isExpired(item.expdate) ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'" class="px-2 py-1 text-xs font-medium rounded-full" x-text="isExpired(item.expdate) ? 'Expired' : 'Active'"></span>
|
||||
</td>
|
||||
<td class="py-3 px-5 text-slate-600" x-text="item.expdate ? new Date(item.expdate).toLocaleDateString() : '-'"></td>
|
||||
<td class="py-3 px-5 text-right">
|
||||
<button @click="showForm(item.control_id)" class="text-blue-600 hover:text-blue-800 mr-3">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button @click="deleteItem(item.control_id)" class="text-red-600 hover:text-red-800">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Include -->
|
||||
<?= $this->include('control/dialog_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("controlIndex", () => ({
|
||||
loading: false,
|
||||
showModal: false,
|
||||
list: [],
|
||||
form: {},
|
||||
errors: {},
|
||||
error: '',
|
||||
keyword: '',
|
||||
deptId: '',
|
||||
depts: <?= json_encode($depts ?? []) ?>,
|
||||
tests: <?= json_encode($tests ?? []) ?>,
|
||||
|
||||
init() {
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('keyword', this.keyword);
|
||||
if (this.deptId) params.append('deptId', this.deptId);
|
||||
const res = await fetch(`${window.BASEURL}/api/control?${params}`);
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.list = data.data || [];
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load controls';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
isExpired(expdate) {
|
||||
if (!expdate) return false;
|
||||
return new Date(expdate) < new Date();
|
||||
},
|
||||
|
||||
async showForm(id = null) {
|
||||
this.errors = {};
|
||||
if (id) {
|
||||
const item = this.list.find(x => x.control_id === id);
|
||||
if (item) {
|
||||
this.form = {
|
||||
control_id: item.control_id,
|
||||
dept_ref_id: item.dept_ref_id,
|
||||
name: item.name,
|
||||
lot: item.lot || '',
|
||||
producer: item.producer || '',
|
||||
expdate: item.expdate || '',
|
||||
test_ids: []
|
||||
};
|
||||
// Fetch assigned tests
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}/api/control/${id}/tests`);
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.form.test_ids = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch assigned tests', err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.form = {
|
||||
dept_ref_id: '',
|
||||
name: '',
|
||||
lot: '',
|
||||
producer: '',
|
||||
expdate: '',
|
||||
test_ids: []
|
||||
};
|
||||
}
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
this.form = {};
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.form.dept_ref_id) this.errors.dept_ref_id = 'Department is required';
|
||||
if (!this.form.name) this.errors.name = 'Control name is required';
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const url = this.form.control_id
|
||||
? `${window.BASEURL}/api/control/${this.form.control_id}`
|
||||
: `${window.BASEURL}/api/control`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: this.form.control_id ? 'PATCH' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
this.closeModal();
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to save control';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteItem(id) {
|
||||
if (!confirm('Are you sure you want to delete this control?')) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}/api/control/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to delete control';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
@ -1,142 +1,67 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="dashboard()">
|
||||
<div class="space-y-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow duration-200 group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-500 mb-1">Total Tests</p>
|
||||
<h3 class="text-3xl font-bold text-slate-800 group-hover:text-primary-600 transition-colors"><?= count($tests) ?></h3>
|
||||
</div>
|
||||
<div class="h-12 w-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
||||
<i class="fa-solid fa-file-medical text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center text-sm text-slate-500">
|
||||
<span class="text-green-600 font-medium flex items-center mr-2">
|
||||
<i class="fa-solid fa-check-circle mr-1"></i>
|
||||
Active
|
||||
</span>
|
||||
<span>in library</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow duration-200 group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-500 mb-1">Total Controls</p>
|
||||
<h3 class="text-3xl font-bold text-slate-800 group-hover:text-primary-600 transition-colors"><?= count($controls) ?></h3>
|
||||
</div>
|
||||
<div class="h-12 w-12 rounded-xl bg-emerald-50 text-emerald-600 flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
||||
<i class="fa-solid fa-vial text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center text-sm text-slate-500">
|
||||
<span class="text-slate-600 font-medium flex items-center mr-2">Reference</span>
|
||||
<span>definitions</span>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-base-content">Dashboard</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Quality Control Overview</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow duration-200 group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-500 mb-1">Departments</p>
|
||||
<h3 class="text-3xl font-bold text-slate-800 group-hover:text-primary-600 transition-colors"><?= count($depts) ?></h3>
|
||||
</div>
|
||||
<div class="h-12 w-12 rounded-xl bg-violet-50 text-violet-600 flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
||||
<i class="fa-solid fa-building text-2xl"></i>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Total Controls</p>
|
||||
<p class="text-2xl font-bold text-base-content mt-1">24</p>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center text-sm text-slate-500">
|
||||
<span class="text-slate-600 font-medium flex items-center mr-2">Active</span>
|
||||
<span>units</span>
|
||||
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<i class="fa-solid fa-vial text-primary text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div class="px-6 py-5 border-b border-slate-100 bg-white">
|
||||
<h3 class="text-lg font-bold text-slate-800">Menu</h3>
|
||||
<p class="text-sm text-slate-500">Quick access to all features</p>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Tests Today</p>
|
||||
<p class="text-2xl font-bold text-base-content mt-1">156</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-lg bg-success/10 flex items-center justify-center">
|
||||
<i class="fa-solid fa-check text-success text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<a href="<?= base_url('/test') ?>" class="flex items-start p-4 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-all duration-200 group">
|
||||
<div class="h-10 w-10 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-vial"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-800 group-hover:text-primary-600">Test Definitions</h4>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage test types, methods, and units</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="<?= base_url('/control') ?>" class="flex items-start p-4 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-all duration-200 group">
|
||||
<div class="h-10 w-10 rounded-lg bg-emerald-100 text-emerald-600 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-800 group-hover:text-primary-600">Control Dictionary</h4>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage control materials and lot numbers</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="<?= base_url('/dept') ?>" class="flex items-start p-4 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-all duration-200 group">
|
||||
<div class="h-10 w-10 rounded-lg bg-violet-100 text-violet-600 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-800 group-hover:text-primary-600">Department Dictionary</h4>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage departments and units</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="<?= base_url('/entry') ?>" class="flex items-start p-4 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-all duration-200 group">
|
||||
<div class="h-10 w-10 rounded-lg bg-amber-100 text-amber-600 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-calendar-check"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-800 group-hover:text-primary-600">Monthly Entry</h4>
|
||||
<p class="text-sm text-slate-500 mt-1">Enter monthly QC results</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="<?= base_url('/entry/daily') ?>" class="flex items-start p-4 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-all duration-200 group">
|
||||
<div class="h-10 w-10 rounded-lg bg-rose-100 text-rose-600 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-800 group-hover:text-primary-600">Daily Entry</h4>
|
||||
<p class="text-sm text-slate-500 mt-1">Enter daily QC results</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="<?= base_url('/report') ?>" class="flex items-start p-4 rounded-xl border border-slate-200 hover:border-primary-300 hover:bg-primary-50/50 transition-all duration-200 group">
|
||||
<div class="h-10 w-10 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
|
||||
<i class="fa-solid fa-chart-column"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-800 group-hover:text-primary-600">Reports</h4>
|
||||
<p class="text-sm text-slate-500 mt-1">View and generate QC reports</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Pass Rate</p>
|
||||
<p class="text-2xl font-bold text-base-content mt-1">98.5%</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-lg bg-warning/10 flex items-center justify-center">
|
||||
<i class="fa-solid fa-chart-line text-warning text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Alerts</p>
|
||||
<p class="text-2xl font-bold text-base-content mt-1">3</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-lg bg-error/10 flex items-center justify-center">
|
||||
<i class="fa-solid fa-triangle-exclamation text-error text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-base-content mb-4">Recent QC Results</h2>
|
||||
<p class="text-base-content/60 text-center py-8">Dashboard content coming soon...</p>
|
||||
</div>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("dashboard", () => ({
|
||||
init() {
|
||||
// Dashboard init
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
<!-- Backdrop -->
|
||||
<div x-show="showModal"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-40"
|
||||
@click="closeModal()">
|
||||
</div>
|
||||
|
||||
<!-- Modal Panel -->
|
||||
<div x-show="showModal"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-md" @click.stop>
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-slate-800" x-text="form.dept_id ? 'Edit Department' : 'Add Department'"></h3>
|
||||
<button @click="closeModal()" class="text-slate-400 hover:text-slate-600">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="save()">
|
||||
<div class="p-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Department Name <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="form.name"
|
||||
class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||
:class="{'border-red-300 bg-red-50': errors.name}"
|
||||
placeholder="Enter department name" required>
|
||||
<p x-show="errors.name" x-text="errors.name" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50 rounded-b-2xl">
|
||||
<button type="button" @click="closeModal()" class="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-800 transition-colors">Cancel</button>
|
||||
<button type="submit" :disabled="loading" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<template x-if="!loading">
|
||||
<span><i class="fa-solid fa-check mr-1"></i> <span x-text="form.dept_id ? 'Update' : 'Save'"></span></span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span><i class="fa-solid fa-spinner fa-spin mr-1"></i> Saving...</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,187 +0,0 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="deptIndex()">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Department Dictionary</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage department entries</p>
|
||||
</div>
|
||||
<button @click="showForm()" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus mr-2"></i>Add Department
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table Card -->
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<template x-if="loading">
|
||||
<div class="p-12 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-blue-50 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fa-solid fa-spinner fa-spin text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm">Loading departments...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="!loading && (!list || list.length === 0)">
|
||||
<div class="flex-1 flex items-center justify-center p-8">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
|
||||
<i class="fa-solid fa-inbox text-slate-400"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm">No departments found</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Data Table -->
|
||||
<template x-if="!loading && list && list.length > 0">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="py-3 px-5 font-semibold">#</th>
|
||||
<th class="py-3 px-5 font-semibold">Name</th>
|
||||
<th class="py-3 px-5 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<template x-for="(item, index) in list" :key="item.dept_id">
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="py-3 px-5" x-text="index + 1"></td>
|
||||
<td class="py-3 px-5 font-medium text-slate-800" x-text="item.name"></td>
|
||||
<td class="py-3 px-5 text-right">
|
||||
<button @click="showForm(item.dept_id)" class="text-blue-600 hover:text-blue-800 mr-3">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button @click="deleteItem(item.dept_id)" class="text-red-600 hover:text-red-800">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Include -->
|
||||
<?= $this->include('dept/dialog_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("deptIndex", () => ({
|
||||
loading: false,
|
||||
showModal: false,
|
||||
list: [],
|
||||
form: {},
|
||||
errors: {},
|
||||
error: '',
|
||||
|
||||
init() {
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}/api/dept`);
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.list = data.data || [];
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load departments';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
showForm(id = null) {
|
||||
this.form = id ? JSON.parse(JSON.stringify(this.list.find(x => x.dept_id === id))) : {};
|
||||
this.errors = {};
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
this.form = {};
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.form.name) {
|
||||
this.errors.name = 'Department name is required';
|
||||
}
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const url = this.form.dept_id
|
||||
? `${window.BASEURL}/api/dept/${this.form.dept_id}`
|
||||
: `${window.BASEURL}/api/dept`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: this.form.dept_id ? 'PATCH' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: this.form.name })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
this.closeModal();
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to save department';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteItem(id) {
|
||||
if (!confirm('Are you sure you want to delete this department?')) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}/api/dept/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to delete department';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
@ -1,256 +1,16 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="dailyEntry()">
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Daily Entry</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Enter daily QC results</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-slate-500">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<span>Press Ctrl+S to save</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">Daily Entry</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Record daily QC test results</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Department <span class="text-red-500">*</span></label>
|
||||
<select x-model="dept" @change="loadControls()" :class="errors.dept ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Select Department</option>
|
||||
<?php foreach ($depts as $d): ?>
|
||||
<option value="<?= $d['dept_id'] ?>"><?= $d['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p x-show="errors.dept" x-text="errors.dept" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Date <span class="text-red-500">*</span></label>
|
||||
<input type="date" x-model="date" :class="errors.date ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<p x-show="errors.date" x-text="errors.date" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Control <span class="text-red-500">*</span></label>
|
||||
<select x-model="control" @change="loadTests()" :class="errors.control ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Select Control</option>
|
||||
<template x-for="c in controls" :key="c.control_id">
|
||||
<option :value="c.control_id" x-text="c.name + ' (' + (c.lot || 'N/A') + ')'"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p x-show="errors.control" x-text="errors.control" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Test <span class="text-red-500">*</span></label>
|
||||
<select x-model="test" :class="errors.test ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Select Test</option>
|
||||
<template x-for="t in tests" :key="t.test_ref_id">
|
||||
<option :value="t.test_ref_id" x-text="t.test_name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p x-show="errors.test" x-text="errors.test" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6" x-show="control && test" x-transition>
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4">Result</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Value <span class="text-red-500">*</span></label>
|
||||
<input type="number" step="0.01" x-model="resvalue" :class="errors.resvalue ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Enter value">
|
||||
<p x-show="errors.resvalue" x-text="errors.resvalue" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Comment</label>
|
||||
<input type="text" x-model="rescomment" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Optional comment">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button @click="saveResult()" data-save-btn :disabled="loading" class="btn btn-primary">
|
||||
<template x-if="!loading">
|
||||
<span class="flex items-center">
|
||||
<i class="fa-solid fa-check mr-2"></i>
|
||||
Save Result
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span class="flex items-center">
|
||||
<i class="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Saving...
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<p class="text-base-content/60 text-center py-8">Daily entry form coming soon...</p>
|
||||
</div>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("dailyEntry", () => ({
|
||||
dept: '',
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
control: '',
|
||||
test: '',
|
||||
resvalue: '',
|
||||
rescomment: '',
|
||||
controls: [],
|
||||
tests: [],
|
||||
loading: false,
|
||||
errors: {},
|
||||
error: '',
|
||||
|
||||
init() {
|
||||
this.loadDraft();
|
||||
},
|
||||
|
||||
async loadControls() {
|
||||
this.errors = {};
|
||||
this.controls = [];
|
||||
this.control = '';
|
||||
this.tests = [];
|
||||
this.test = '';
|
||||
|
||||
if (!this.dept || !this.date) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/controls?date=${this.date}&deptid=${this.dept}`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
this.controls = data.data || [];
|
||||
if (this.controls.length === 0) {
|
||||
App.showToast('No controls found for selected criteria', 'info');
|
||||
}
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load controls';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = 'Failed to load controls';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTests() {
|
||||
this.errors = {};
|
||||
this.tests = [];
|
||||
this.test = '';
|
||||
|
||||
if (!this.control) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/tests?controlid=${this.control}`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
this.tests = data.data || [];
|
||||
if (this.tests.length === 0) {
|
||||
App.showToast('No tests found for this control', 'info');
|
||||
}
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load tests';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = 'Failed to load tests';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.dept) this.errors.dept = 'Department is required';
|
||||
if (!this.date) this.errors.date = 'Date is required';
|
||||
if (!this.control) this.errors.control = 'Control is required';
|
||||
if (!this.test) this.errors.test = 'Test is required';
|
||||
if (!this.resvalue) this.errors.resvalue = 'Value is required';
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async saveResult() {
|
||||
if (!this.validate()) {
|
||||
App.showToast('Please fill all required fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!App.confirmSave('Save result?')) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('controlid', this.control);
|
||||
formData.append('testid', this.test);
|
||||
formData.append('resdate', this.date);
|
||||
formData.append('resvalue', this.resvalue);
|
||||
formData.append('rescomment', this.rescomment);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/daily`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
App.showToast('Result saved successfully!');
|
||||
this.clearForm();
|
||||
this.saveDraft();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to save result';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
clearForm() {
|
||||
this.resvalue = '';
|
||||
this.rescomment = '';
|
||||
},
|
||||
|
||||
saveDraft() {
|
||||
localStorage.setItem('dailyEntry', JSON.stringify({
|
||||
dept: this.dept,
|
||||
date: this.date,
|
||||
control: this.control,
|
||||
test: this.test
|
||||
}));
|
||||
},
|
||||
|
||||
loadDraft() {
|
||||
const draft = localStorage.getItem('dailyEntry');
|
||||
if (draft) {
|
||||
const data = JSON.parse(draft);
|
||||
this.dept = data.dept || '';
|
||||
this.date = data.date || new Date().toISOString().split('T')[0];
|
||||
this.control = data.control || '';
|
||||
this.test = data.test || '';
|
||||
if (this.dept && this.date) this.loadControls();
|
||||
if (this.control) this.loadTests();
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
|
||||
40
app/Views/entry/index.php
Normal file
40
app/Views/entry/index.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">QC Entry</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Record quality control results</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<a href="<?= base_url('/entry/daily') ?>" class="block p-6 rounded-xl border-2 border-dashed border-base-300 hover:border-blue-500 hover:bg-blue-50 transition-all group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-xl bg-blue-100 flex items-center justify-center group-hover:bg-blue-200 transition-colors">
|
||||
<i class="fa-solid fa-calendar-day text-2xl text-blue-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-base-content">Daily Entry</h3>
|
||||
<p class="text-sm text-base-content/60 mt-1">Record daily QC test results</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="block p-6 rounded-xl border-2 border-dashed border-base-300 hover:border-green-500 hover:bg-green-50 transition-all group cursor-not-allowed opacity-60">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-xl bg-green-100 flex items-center justify-center group-hover:bg-green-200 transition-colors">
|
||||
<i class="fa-solid fa-chart-column text-2xl text-green-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-base-content">Monthly Review</h3>
|
||||
<p class="text-sm text-base-content/60 mt-1">Monthly QC analysis & comments</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -1,498 +0,0 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="monthlyEntry()" @keydown.window.ctrl.s.prevent="saveData()">
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Monthly Entry</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Enter monthly QC results</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-slate-500">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<span>Press Ctrl+S to save</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Department <span class="text-red-500">*</span></label>
|
||||
<select x-model="dept" @change="loadControls()" :class="errors.dept ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Select Department</option>
|
||||
<?php foreach ($depts as $d): ?>
|
||||
<option value="<?= $d['dept_id'] ?>"><?= $d['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p x-show="errors.dept" x-text="errors.dept" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Month <span class="text-red-500">*</span></label>
|
||||
<input type="month" x-model="date" @change="loadControls(); loadMonthData()" :class="errors.date ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<p x-show="errors.date" x-text="errors.date" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Control <span class="text-red-500">*</span></label>
|
||||
<select x-model="control" @change="loadTests(); loadMonthData()" :class="errors.control ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Select Control</option>
|
||||
<template x-for="c in controls" :key="c.control_id">
|
||||
<option :value="c.control_id" x-text="c.name + ' (' + (c.lot || 'N/A') + ')'"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p x-show="errors.control" x-text="errors.control" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="control && tests.length > 0" x-transition>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Select Tests <span class="text-red-500">*</span></label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="t in tests" :key="t.test_ref_id">
|
||||
<label class="inline-flex items-center px-3 py-1.5 rounded-lg border cursor-pointer transition-all"
|
||||
:class="selectedTests.includes(t.test_ref_id) ? 'bg-blue-50 border-blue-500 text-blue-700' : 'bg-slate-50 border-slate-200 hover:border-slate-300'">
|
||||
<input type="checkbox" :value="t.test_ref_id" x-model="selectedTests" @change="loadMonthData()" class="hidden">
|
||||
<span class="text-sm font-medium" x-text="t.test_name"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="control && selectedTests.length > 0" x-transition>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-slate-800">
|
||||
<span x-text="monthName"></span> <span x-text="year"></span>
|
||||
</h3>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded bg-slate-100 border border-slate-200"></span>
|
||||
<span class="text-slate-600">Weekday</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded bg-red-50 border border-red-200"></span>
|
||||
<span class="text-slate-600">Weekend</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template x-if="loading">
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<i class="fa-solid fa-spinner fa-spin text-blue-500 text-3xl"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading">
|
||||
<div class="border border-slate-200 rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="py-3 px-3 text-left font-semibold text-slate-600 w-32 sticky left-0 bg-slate-50">Test</th>
|
||||
<template x-for="day in daysInMonth" :key="day">
|
||||
<th class="py-3 px-2 text-center font-medium text-slate-600 min-w-[60px]"
|
||||
:class="isWeekend(day) ? 'bg-red-50 text-red-600' : ''">
|
||||
<span x-text="day"></span>
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="testId in selectedTests" :key="testId">
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
<td class="py-2 px-3 font-medium text-slate-700 sticky left-0 bg-white border-r border-slate-100">
|
||||
<span x-text="getTestName(testId)"></span>
|
||||
</td>
|
||||
<template x-for="day in daysInMonth" :key="day">
|
||||
<td class="py-1 px-1 relative" :class="isWeekend(day) ? 'bg-red-50' : ''">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="w-full px-2 py-1.5 text-center text-sm bg-white border border-slate-200 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500"
|
||||
:placeholder="'-'"
|
||||
x-model="monthlyData[testId + '_' + day]"
|
||||
@change="markDirty(testId, day)"
|
||||
>
|
||||
<button
|
||||
class="p-1 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
:title="monthlyData[testId + '_' + day + '_comment'] || 'Add comment'"
|
||||
@click.stop="toggleComment(testId, day)"
|
||||
>
|
||||
<i class="fa-solid fa-comment text-xs" :class="monthlyData[testId + '_' + day + '_comment'] ? 'text-blue-500' : ''"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
x-show="showCommentId === testId + '_' + day"
|
||||
x-transition
|
||||
class="absolute z-10 top-full left-0 mt-1 w-48"
|
||||
@click.outside="showCommentId = null"
|
||||
>
|
||||
<textarea
|
||||
class="w-full px-2 py-1.5 text-xs bg-white border border-slate-200 rounded shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 resize-none"
|
||||
rows="2"
|
||||
placeholder="Add comment..."
|
||||
x-model="monthlyData[testId + '_' + day + '_comment']"
|
||||
@keydown.enter.prevent
|
||||
></textarea>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Monthly Comment</label>
|
||||
<textarea x-model="comment" rows="2" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Optional monthly comment"></textarea>
|
||||
</div>
|
||||
<div class="flex items-end gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Operation Mode</label>
|
||||
<select x-model="operation" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="replace">Replace All</option>
|
||||
<option value="add">Add Only (Skip Existing)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="saveData()" data-save-btn :disabled="loading || selectedTests.length === 0" class="btn btn-primary flex-shrink-0">
|
||||
<template x-if="!loading">
|
||||
<span class="flex items-center justify-center">
|
||||
<i class="fa-solid fa-check mr-2"></i>
|
||||
Save All Data
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span class="flex items-center justify-center">
|
||||
<i class="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Saving...
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="control && (!selectedTests || selectedTests.length === 0)" x-transition class="text-center py-12 text-slate-500">
|
||||
<i class="fa-solid fa-clipboard-list text-4xl mb-3 text-slate-300"></i>
|
||||
<p>Select one or more tests above to view and enter monthly data</p>
|
||||
</div>
|
||||
|
||||
<div x-show="!control" x-transition class="text-center py-12 text-slate-500">
|
||||
<i class="fa-solid fa-list-check text-4xl mb-3 text-slate-300"></i>
|
||||
<p>Select a control to view available tests</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("monthlyEntry", () => ({
|
||||
dept: '',
|
||||
date: new Date().toISOString().slice(0, 7),
|
||||
control: '',
|
||||
tests: [],
|
||||
selectedTests: [],
|
||||
controls: [],
|
||||
loading: false,
|
||||
errors: {},
|
||||
error: '',
|
||||
monthlyData: {},
|
||||
comment: '',
|
||||
dirtyCells: new Set(),
|
||||
operation: 'replace',
|
||||
saveResult: null,
|
||||
showCommentId: null,
|
||||
|
||||
init() {
|
||||
this.loadDraft();
|
||||
},
|
||||
|
||||
get year() {
|
||||
return this.date ? this.date.split('-')[0] : '';
|
||||
},
|
||||
|
||||
get monthName() {
|
||||
if (!this.date) return '';
|
||||
const month = parseInt(this.date.split('-')[1]);
|
||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
return months[month - 1] || '';
|
||||
},
|
||||
|
||||
get daysInMonth() {
|
||||
if (!this.date) return 31;
|
||||
const [year, month] = this.date.split('-').map(Number);
|
||||
return new Date(year, month, 0).getDate();
|
||||
},
|
||||
|
||||
isWeekend(day) {
|
||||
if (!this.date) return false;
|
||||
const [year, month] = this.date.split('-').map(Number);
|
||||
const date = new Date(year, month - 1, day);
|
||||
const dayOfWeek = date.getDay();
|
||||
return dayOfWeek === 0 || dayOfWeek === 6;
|
||||
},
|
||||
|
||||
getTestName(testId) {
|
||||
const test = this.tests.find(t => t.test_ref_id === testId);
|
||||
return test ? test.test_name : '';
|
||||
},
|
||||
|
||||
markDirty(testId, day) {
|
||||
this.dirtyCells.add(testId + '_' + day);
|
||||
},
|
||||
|
||||
toggleComment(testId, day) {
|
||||
const id = testId + '_' + day;
|
||||
if (this.showCommentId === id) {
|
||||
this.showCommentId = null;
|
||||
} else {
|
||||
this.showCommentId = id;
|
||||
}
|
||||
},
|
||||
|
||||
async loadControls() {
|
||||
this.errors = {};
|
||||
this.controls = [];
|
||||
this.control = '';
|
||||
this.tests = [];
|
||||
this.selectedTests = [];
|
||||
this.monthlyData = {};
|
||||
this.comment = '';
|
||||
|
||||
if (!this.dept || !this.date) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/controls?date=${this.date}&deptid=${this.dept}`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
this.controls = data.data || [];
|
||||
if (this.controls.length === 0) {
|
||||
App.showToast('No controls found for selected criteria', 'info');
|
||||
}
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load controls';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = 'Failed to load controls';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTests() {
|
||||
this.errors = {};
|
||||
this.tests = [];
|
||||
this.selectedTests = [];
|
||||
this.monthlyData = {};
|
||||
this.comment = '';
|
||||
|
||||
if (!this.control) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/tests?controlid=${this.control}`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
this.tests = data.data || [];
|
||||
if (this.tests.length === 0) {
|
||||
App.showToast('No tests found for this control', 'info');
|
||||
} else {
|
||||
this.selectedTests = this.tests.map(t => t.test_ref_id);
|
||||
}
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load tests';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = 'Failed to load tests';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadMonthData() {
|
||||
if (!this.control || this.selectedTests.length === 0 || !this.date) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.monthlyData = {};
|
||||
this.comment = '';
|
||||
|
||||
try {
|
||||
for (const testId of this.selectedTests) {
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/monthly?controlid=${this.control}&testid=${testId}&yearmonth=${this.date}`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
const formValues = data.data.formValues || {};
|
||||
const comments = data.data.comments || {};
|
||||
for (const [day, value] of Object.entries(formValues)) {
|
||||
this.monthlyData[testId + '_' + day] = value;
|
||||
}
|
||||
for (const [day, comment] of Object.entries(comments)) {
|
||||
this.monthlyData[testId + '_' + day + '_comment'] = comment;
|
||||
}
|
||||
if (!this.comment && data.data.comment) {
|
||||
this.comment = data.data.comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = 'Failed to load monthly data';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.dept) this.errors.dept = 'Department is required';
|
||||
if (!this.date) this.errors.date = 'Date is required';
|
||||
if (!this.control) this.errors.control = 'Control is required';
|
||||
if (this.selectedTests.length === 0) {
|
||||
App.showToast('Please select at least one test', 'error');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async saveData() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!App.confirmSave('Save monthly entry data?')) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.saveResult = null;
|
||||
|
||||
try {
|
||||
const tests = [];
|
||||
for (const testId of this.selectedTests) {
|
||||
const resvalue = {};
|
||||
const rescomment = {};
|
||||
let hasData = false;
|
||||
for (let day = 1; day <= this.daysInMonth; day++) {
|
||||
const key = testId + '_' + day;
|
||||
const value = this.monthlyData[key];
|
||||
const commentKey = testId + '_' + day + '_comment';
|
||||
const comment = this.monthlyData[commentKey];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
resvalue[day] = value;
|
||||
hasData = true;
|
||||
}
|
||||
if (comment !== undefined && comment !== null && comment !== '') {
|
||||
rescomment[day] = comment;
|
||||
}
|
||||
}
|
||||
if (hasData) {
|
||||
tests.push({ testId, resvalue, rescomment });
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${window.BASEURL}/api/entry/monthly`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
controlId: this.control,
|
||||
yearMonth: this.date,
|
||||
operation: this.operation,
|
||||
tests: tests
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
this.saveResult = data.data;
|
||||
|
||||
let message = 'Data saved successfully!';
|
||||
if (data.data.statistics && data.data.statistics.length > 0) {
|
||||
message += `\n\nStatistics:\n`;
|
||||
for (const stat of data.data.statistics) {
|
||||
const testName = this.getTestName(stat.testId);
|
||||
message += `${testName}: n=${stat.n}, Mean=${stat.mean}, SD=${stat.sd}, CV=${stat.cv}%\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.data.validations && data.data.validations.length > 0) {
|
||||
message += `\n⚠️ ${data.data.validations.length} value(s) out of control limits!`;
|
||||
}
|
||||
|
||||
App.showToast(message, 'success');
|
||||
this.dirtyCells.clear();
|
||||
this.saveDraft();
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to save data');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = error.message || 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveComment() {
|
||||
const formData = new FormData();
|
||||
formData.append('controlid', this.control);
|
||||
formData.append('testid', this.selectedTests[0]);
|
||||
formData.append('commonth', this.date);
|
||||
formData.append('comtext', this.comment);
|
||||
|
||||
try {
|
||||
await fetch(`${window.BASEURL}/api/entry/comment`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save comment:', error);
|
||||
}
|
||||
},
|
||||
|
||||
saveDraft() {
|
||||
localStorage.setItem('monthlyEntry', JSON.stringify({
|
||||
dept: this.dept,
|
||||
date: this.date,
|
||||
control: this.control,
|
||||
test: this.test
|
||||
}));
|
||||
},
|
||||
|
||||
loadDraft() {
|
||||
const draft = localStorage.getItem('monthlyEntry');
|
||||
if (draft) {
|
||||
const data = JSON.parse(draft);
|
||||
this.dept = data.dept || '';
|
||||
this.date = data.date || new Date().toISOString().slice(0, 7);
|
||||
this.control = data.control || '';
|
||||
this.test = data.test || '';
|
||||
if (this.dept && this.date) this.loadControls();
|
||||
if (this.control) this.loadTests();
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -1,65 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-slate-50">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $page_title ?? 'QC Application' ?></title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eef2ff',
|
||||
100: '#e0e7ff',
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
400: '#818cf8',
|
||||
500: '#6366f1',
|
||||
600: '#4f46e5',
|
||||
700: '#4338ca',
|
||||
800: '#3730a3',
|
||||
900: '#312e81',
|
||||
950: '#1e1b4b',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const baseUrl = '<?= base_url() ?>';
|
||||
window.BASEURL = '<?= base_url() ?>';
|
||||
</script>
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-slate-50 min-h-screen flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-800"><?= $page_title ?? 'QC Application' ?></h1>
|
||||
</div>
|
||||
<?= $this->renderSection("content"); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?= base_url('js/app.js') ?>"></script>
|
||||
<?= $this->renderSection("script"); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,252 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-slate-50">
|
||||
|
||||
<html lang="en" data-theme="autumn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $page_title ?? 'QC Application' ?></title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<title><?= $pageData['title'] ?? 'TinyQC - QC Management System' ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eef2ff',
|
||||
100: '#e0e7ff',
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
400: '#818cf8',
|
||||
500: '#6366f1',
|
||||
600: '#4f46e5',
|
||||
700: '#4338ca',
|
||||
800: '#3730a3',
|
||||
900: '#312e81',
|
||||
950: '#1e1b4b',
|
||||
},
|
||||
surface: {
|
||||
50: '#f8fafc',
|
||||
100: '#f1f5f9',
|
||||
200: '#e2e8f0',
|
||||
300: '#cbd5e1',
|
||||
400: '#94a3b8',
|
||||
500: '#64748b',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1e293b',
|
||||
900: '#0f172a',
|
||||
950: '#020617',
|
||||
}
|
||||
},
|
||||
boxShadow: {
|
||||
'glass': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(255, 255, 255, 0.5) inset',
|
||||
'card': '0 0 0 1px rgba(226, 232, 240, 1), 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const baseUrl = '<?= base_url() ?>';
|
||||
window.BASEURL = '<?= base_url() ?>';
|
||||
const BASEURL = '<?= base_url('/') ?>';
|
||||
</script>
|
||||
<style>
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body x-data="layoutManager()" :class="{ 'overflow-hidden': $store.appState.sidebarOpen && isMobile() }" class="bg-gray-50 h-full text-slate-800 antialiased">
|
||||
<div x-cloak x-show="App.loading" x-transition.opacity
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-white/80 backdrop-blur-sm">
|
||||
<div class="bg-white shadow-xl rounded-2xl p-6 flex items-center space-x-4 border border-slate-100">
|
||||
<div class="animate-spin h-6 w-6 border-[3px] border-primary-600 border-t-transparent rounded-full"></div>
|
||||
<span class="text-slate-600 font-medium">Loading application...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-screen bg-slate-50">
|
||||
<div id="sidebar-backdrop" x-show="$store.appState.sidebarOpen" @click="$store.appState.sidebarOpen = false" x-transition.opacity aria-hidden="true"
|
||||
class="fixed inset-0 z-40 bg-slate-900/50 backdrop-blur-sm lg:hidden"></div>
|
||||
|
||||
<aside id="sidebar"
|
||||
x-data="{
|
||||
resultsOpen: <?= in_array($active_menu, ['entry', 'entry_daily']) ? 'true' : 'false' ?>,
|
||||
masterOpen: <?= in_array($active_menu, ['test', 'control', 'dept']) ? 'true' : 'false' ?>
|
||||
}"
|
||||
:class="$store.appState.sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:-translate-x-full'"
|
||||
class="fixed inset-y-0 left-0 z-50 w-64 transition-transform duration-300 ease-in-out flex flex-col shadow-lg">
|
||||
<div class="flex h-16 items-center px-6 border-b border-slate-100 bg-white/50">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="h-8 w-8 rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 flex items-center justify-center shadow-lg shadow-primary-500/30">
|
||||
<i class="fa-solid fa-flask text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-lg font-bold text-slate-800 tracking-tight">TinyQC</h1>
|
||||
<p class="text-xs text-slate-500 font-medium tracking-wide">LABORATORY</p>
|
||||
<body class="bg-base-200 text-base-content" x-data="appData()">
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle" x-ref="sidebarDrawer">
|
||||
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
<nav class="navbar bg-base-200/80 backdrop-blur-md border-b border-base-300 sticky top-0 z-30 w-full shadow-sm">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="sidebar-drawer" class="btn btn-square btn-ghost text-primary">
|
||||
<i class="fa-solid fa-bars text-xl"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-1 px-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<i class="fa-solid fa-flask text-white text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-base-content tracking-tight">tinyqc</h1>
|
||||
<p class="text-xs opacity-70">QC Management System</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 overflow-y-auto px-4 py-4 space-y-1">
|
||||
<p class="px-2 text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Main Menu</p>
|
||||
|
||||
<?php $active_menu = $active_menu ?? ''; ?>
|
||||
<?php $navClass = 'group flex items-center px-3 py-2 text-sm font-medium rounded-xl transition-all duration-200 w-full'; ?>
|
||||
<?php $activeClass = 'bg-primary-50 text-primary-700 shadow-sm ring-1 ring-primary-100'; ?>
|
||||
<?php $inactiveClass = 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'; ?>
|
||||
<?php $subActiveClass = 'text-primary-700 font-semibold bg-primary-50/50'; ?>
|
||||
<?php $subInactiveClass = 'text-slate-500 hover:text-slate-800 hover:bg-slate-50'; ?>
|
||||
|
||||
<a href="<?= base_url('/') ?>"
|
||||
class="<?= $navClass ?> <?= $active_menu == 'dashboard' ? $activeClass : $inactiveClass ?>">
|
||||
<i class="fa-solid fa-house h-5 w-5 mr-3 flex items-center justify-center <?= $active_menu == 'dashboard' ? 'text-primary-600' : 'text-slate-400 group-hover:text-slate-600' ?> transition-colors"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
|
||||
<!-- Results Dropdown -->
|
||||
<div class="space-y-1">
|
||||
<button @click="resultsOpen = !resultsOpen"
|
||||
class="<?= $navClass ?> <?= in_array($active_menu, ['entry', 'entry_daily']) ? 'text-primary-700' : 'text-slate-600 hover:bg-slate-50' ?>">
|
||||
<i class="fa-solid fa-square-poll-vertical h-5 w-5 mr-3 flex items-center justify-center <?= in_array($active_menu, ['entry', 'entry_daily']) ? 'text-primary-600' : 'text-slate-400' ?>"></i>
|
||||
<span class="flex-1 text-left">Results</span>
|
||||
<i class="fa-solid fa-chevron-right text-[10px] transition-transform duration-200" :class="resultsOpen ? 'rotate-90' : ''"></i>
|
||||
<div class="flex-none">
|
||||
<button class="btn btn-ghost rounded-full mr-2" @click="toggleTheme()">
|
||||
<i class="fa-solid fa-sun text-warning" x-show="currentTheme === themeConfig.light"></i>
|
||||
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
|
||||
</button>
|
||||
|
||||
<div x-show="resultsOpen" x-cloak x-collapse class="pl-11 space-y-1">
|
||||
<a href="<?= base_url('/entry') ?>" class="block px-3 py-1.5 text-sm rounded-lg transition-colors <?= $active_menu == 'entry' ? $subActiveClass : $subInactiveClass ?>">
|
||||
Monthly Entry
|
||||
</a>
|
||||
<a href="<?= base_url('/entry/daily') ?>" class="block px-3 py-1.5 text-sm rounded-lg transition-colors <?= $active_menu == 'entry_daily' ? $subActiveClass : $subInactiveClass ?>">
|
||||
Daily Entry
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="<?= base_url('/report') ?>"
|
||||
class="<?= $navClass ?> <?= $active_menu == 'report' ? $activeClass : $inactiveClass ?>">
|
||||
<i class="fa-solid fa-chart-column h-5 w-5 mr-3 flex items-center justify-center <?= $active_menu == 'report' ? 'text-primary-600' : 'text-slate-400 group-hover:text-slate-600' ?> transition-colors"></i>
|
||||
Reports
|
||||
</a>
|
||||
|
||||
<p class="px-2 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-6 mb-2">Settings</p>
|
||||
|
||||
<!-- Master Data Dropdown -->
|
||||
<div class="space-y-1">
|
||||
<button @click="masterOpen = !masterOpen"
|
||||
class="<?= $navClass ?> <?= in_array($active_menu, ['test', 'control', 'dept']) ? 'text-primary-700' : 'text-slate-600 hover:bg-slate-50' ?>">
|
||||
<i class="fa-solid fa-database h-5 w-5 mr-3 flex items-center justify-center <?= in_array($active_menu, ['test', 'control', 'dept']) ? 'text-primary-600' : 'text-slate-400' ?>"></i>
|
||||
<span class="flex-1 text-left">Master Data</span>
|
||||
<i class="fa-solid fa-chevron-right text-[10px] transition-transform duration-200" :class="masterOpen ? 'rotate-90' : ''"></i>
|
||||
</button>
|
||||
|
||||
<div x-show="masterOpen" x-cloak x-collapse class="pl-11 space-y-1">
|
||||
<a href="<?= base_url('/test') ?>" class="block px-3 py-1.5 text-sm rounded-lg transition-colors <?= $active_menu == 'test' ? $subActiveClass : $subInactiveClass ?>">
|
||||
Test Definitions
|
||||
</a>
|
||||
<a href="<?= base_url('/control') ?>" class="block px-3 py-1.5 text-sm rounded-lg transition-colors <?= $active_menu == 'control' ? $subActiveClass : $subInactiveClass ?>">
|
||||
Control Dictionary
|
||||
</a>
|
||||
<a href="<?= base_url('/dept') ?>" class="block px-3 py-1.5 text-sm rounded-lg transition-colors <?= $active_menu == 'dept' ? $subActiveClass : $subInactiveClass ?>">
|
||||
Department Dictionary
|
||||
</a>
|
||||
<div class="dropdown dropdown-end" x-data="{ dropdownOpen: false }">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder" @click="dropdownOpen = !dropdownOpen">
|
||||
<div class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
|
||||
<span><?= $pageData['userInitials'] ?? 'DR' ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300" x-show="dropdownOpen" @click.outside="dropdownOpen = false" x-transition>
|
||||
<li class="menu-title px-4 py-2">
|
||||
<span class="text-base-content font-bold"><?= $pageData['userName'] ?? 'Lab User' ?></span>
|
||||
<span class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'Administrator' ?></span>
|
||||
</li>
|
||||
<div class="divider my-0 h-px opacity-10"></div>
|
||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-user text-primary"></i> Profile</a></li>
|
||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-gear text-primary"></i> Settings</a></li>
|
||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-question-circle text-primary"></i> Help</a></li>
|
||||
<div class="divider my-0 h-px opacity-10"></div>
|
||||
<li><a href="<?= base_url('/logout') ?>" class="text-error hover:bg-error/10"><i class="fa-solid fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<div class="px-6 py-4 border-t border-slate-100">
|
||||
<div class="bg-gradient-to-br from-primary-800 to-indigo-900 rounded-xl p-4 text-white shadow-lg overflow-hidden relative">
|
||||
<div class="absolute -right-4 -top-4 bg-white/10 w-24 h-24 rounded-full blur-xl"></div>
|
||||
<h4 class="font-medium text-sm relative z-10">Need Help?</h4>
|
||||
<p class="text-primary-200 text-xs mt-1 relative z-10">Check the documentation or contact support.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="flex-1 flex flex-col overflow-hidden bg-slate-50/50 transition-all duration-300"
|
||||
:class="$store.appState.sidebarOpen ? 'lg:ml-64' : 'lg:ml-0'">
|
||||
<header class="bg-white/80 backdrop-blur-md sticky top-0 z-30 border-b border-slate-200/60 supports-backdrop-blur:bg-white/60">
|
||||
<div class="flex items-center justify-between h-16 px-8">
|
||||
<div class="flex items-center">
|
||||
<button id="sidebar-toggle" @click="$store.appState.sidebarOpen = !$store.appState.sidebarOpen" class="p-2 -ml-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg">
|
||||
<i class="fa-solid fa-bars h-6 w-6 flex items-center justify-center"></i>
|
||||
</button>
|
||||
<h2 class="ml-2 lg:ml-0 text-xl font-bold text-slate-800 tracking-tight"><?= $page_title ?? 'Dashboard' ?></h2>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="hidden md:flex items-center px-3 py-1 bg-slate-100 rounded-full border border-slate-200">
|
||||
<i class="fa-solid fa-calendar-days text-slate-400 mr-2 text-xs"></i>
|
||||
<span class="text-sm font-medium text-slate-600"><?= date('F d, Y') ?></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="h-8 w-8 rounded-full bg-primary-100 border border-primary-200 flex items-center justify-center text-primary-700 font-bold text-sm">AD</div>
|
||||
|
||||
<?= $this->renderSection('content') ?>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-40">
|
||||
<label for="sidebar-drawer" class="drawer-overlay"></label>
|
||||
<aside class="bg-base-300 border-r border-base-300 w-64 min-h-full flex flex-col">
|
||||
<ul class="menu p-4 text-base-content flex-1 w-full">
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === '' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/') ?>">
|
||||
<i class="fa-solid fa-chart-line w-5"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mt-6 mb-2 min-h-0">
|
||||
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">Master Data</p>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/dept') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/master/dept') ?>">
|
||||
<i class="fa-solid fa-building w-5"></i>
|
||||
Departments
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/test') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/master/test') ?>">
|
||||
<i class="fa-solid fa-flask-vial w-5"></i>
|
||||
Tests
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/control') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/master/control') ?>">
|
||||
<i class="fa-solid fa-vial w-5"></i>
|
||||
Controls
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mt-6 mb-2 min-h-0">
|
||||
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">QC Operations</p>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'entry') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/entry') ?>">
|
||||
<i class="fa-solid fa-pen-to-square w-5"></i>
|
||||
QC Entry
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'report') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/report') ?>">
|
||||
<i class="fa-solid fa-chart-bar w-5"></i>
|
||||
Reports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="p-4 border-t border-base-300">
|
||||
<div class="bg-base-100/50 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs opacity-60">Storage</span>
|
||||
<span class="text-xs text-primary font-bold">68%</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full h-2" value="68" max="100"></progress>
|
||||
<p class="text-xs opacity-50 mt-2">6.8 GB of 10 GB used</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<div class="w-full">
|
||||
<?= $this->renderSection("content"); ?>
|
||||
</div>
|
||||
</main>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?= base_url('js/app.js') ?>"></script>
|
||||
<script src="<?= base_url('js/tables.js') ?>"></script>
|
||||
<script src="<?= base_url('js/charts.js') ?>"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('layoutManager', () => ({
|
||||
isMobile() {
|
||||
return window.innerWidth < 1024;
|
||||
Alpine.data('appData', () => ({
|
||||
showModal: false,
|
||||
themeConfig: {
|
||||
light: 'autumn',
|
||||
dark: 'dracula'
|
||||
},
|
||||
get currentTheme() {
|
||||
return localStorage.getItem('theme') || this.themeConfig.light;
|
||||
},
|
||||
set currentTheme(value) {
|
||||
localStorage.setItem('theme', value);
|
||||
document.documentElement.setAttribute('data-theme', value);
|
||||
},
|
||||
get isDark() {
|
||||
return this.currentTheme === this.themeConfig.dark;
|
||||
},
|
||||
init() {
|
||||
document.documentElement.setAttribute('data-theme', this.currentTheme);
|
||||
},
|
||||
toggleSidebar() {
|
||||
this.$refs.sidebarDrawer.checked = !this.$refs.sidebarDrawer.checked;
|
||||
},
|
||||
toggleTheme() {
|
||||
this.currentTheme = this.isDark ? this.themeConfig.light : this.themeConfig.dark;
|
||||
},
|
||||
openModal() {
|
||||
this.showModal = true;
|
||||
},
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->renderSection("script"); ?>
|
||||
|
||||
<?= $this->renderSection('script') ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
83
app/Views/master/control/dialog_control_form.php
Normal file
83
app/Views/master/control/dialog_control_form.php
Normal file
@ -0,0 +1,83 @@
|
||||
<dialog class="modal modal-bottom sm:modal-middle" :class="{ 'modal-open': showModal }">
|
||||
<div class="modal-box border border-base-300 shadow-2xl bg-base-100">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2 text-base-content">
|
||||
<i class="fa-solid fa-vial text-primary"></i>
|
||||
<span x-text="form.controlId ? 'Edit Control' : 'New Control'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Control Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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"
|
||||
placeholder="Enter control name"
|
||||
/>
|
||||
<template x-if="errors.controlName">
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error" x-text="errors.controlName"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Lot Number</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.lotNumber"
|
||||
placeholder="e.g., LOT12345"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Expiry Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary bg-base-200 border-base-300 text-base-content"
|
||||
x-model="form.expDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Producer</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.producer"
|
||||
placeholder="Enter producer name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button class="btn btn-ghost opacity-70" @click="closeModal()">Cancel</button>
|
||||
<button
|
||||
class="btn btn-primary gap-2 shadow-lg shadow-primary/20 font-medium"
|
||||
:class="{'loading': loading}"
|
||||
@click="save()"
|
||||
:disabled="loading"
|
||||
>
|
||||
<template x-if="!loading">
|
||||
<span><i class="fa-solid fa-save"></i> Save</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span>Saving...</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop bg-black/60" @click="closeModal()"></form>
|
||||
</dialog>
|
||||
242
app/Views/master/control/index.php
Normal file
242
app/Views/master/control/index.php
Normal file
@ -0,0 +1,242 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto" x-data="controls()">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">Controls</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Manage QC control standards</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm gap-2 shadow-sm border-0 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white transition-all duration-200"
|
||||
@click="showForm()"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i> New Control
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-4 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
class="w-full pl-10 pr-4 py-2.5 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>
|
||||
<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()"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden">
|
||||
<template x-if="loading">
|
||||
<div class="p-8 text-center">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p class="mt-2 text-base-content/60">Loading...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && error">
|
||||
<div class="p-8 text-center">
|
||||
<i class="fa-solid fa-triangle-exclamation text-4xl text-error mb-2"></i>
|
||||
<p class="text-error" x-text="error"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && !error && list">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="uppercase tracking-wider font-semibold bg-base-200 text-base-content/70 text-xs">
|
||||
<tr>
|
||||
<th class="py-3 px-5 font-semibold">Control Name</th>
|
||||
<th class="py-3 px-5 font-semibold">Lot Number</th>
|
||||
<th class="py-3 px-5 font-semibold">Producer</th>
|
||||
<th class="py-3 px-5 font-semibold">Expiry Date</th>
|
||||
<th class="py-3 px-5 font-semibold text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-base-content/80 divide-y divide-base-300">
|
||||
<template x-for="item in list" :key="item.controlId">
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-3 px-5 font-medium text-base-content" x-text="item.controlName"></td>
|
||||
<td class="py-3 px-5">
|
||||
<span class="font-mono text-xs bg-base-200 text-base-content/70 px-2 py-1 rounded" x-text="item.lotNumber"></span>
|
||||
</td>
|
||||
<td class="py-3 px-5" x-text="item.producer"></td>
|
||||
<td class="py-3 px-5" x-text="item.expDate"></td>
|
||||
<td class="py-3 px-5 text-right">
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
@click="showForm(item.controlId)"
|
||||
>
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-error bg-error/10 hover:bg-error/20 rounded-lg transition-colors ml-1"
|
||||
@click="deleteData(item.controlId)"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="list.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="py-8 text-center text-base-content/60">No data available</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<?= $this->include('master/control/dialog_control_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script"); ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("controls", () => ({
|
||||
loading: false,
|
||||
showModal: false,
|
||||
errors: {},
|
||||
error: null,
|
||||
keyword: "",
|
||||
list: null,
|
||||
form: {
|
||||
controlId: null,
|
||||
controlName: "",
|
||||
lotNumber: "",
|
||||
producer: "",
|
||||
expDate: "",
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.list = null;
|
||||
try {
|
||||
const params = new URLSearchParams({ keyword: this.keyword });
|
||||
const response = await fetch(`${window.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;
|
||||
}
|
||||
},
|
||||
|
||||
async loadData(id) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}api/master/controls/${id}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load item");
|
||||
const data = await response.json();
|
||||
this.form = data.data[0];
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
this.form = {};
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async showForm(id = null) {
|
||||
this.showModal = true;
|
||||
this.errors = {};
|
||||
if (id) {
|
||||
await this.loadData(id);
|
||||
} else {
|
||||
this.form = { controlId: null, controlName: "", lotNumber: "", producer: "", expDate: "" };
|
||||
}
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.form = { controlId: null, controlName: "", lotNumber: "", producer: "", expDate: "" };
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.form.controlName) this.errors.controlName = "Name is required.";
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
this.loading = true;
|
||||
let method = '';
|
||||
let url = '';
|
||||
if (this.form.controlId) {
|
||||
method = 'PATCH';
|
||||
url = `${window.BASEURL}api/master/controls/${this.form.controlId}`;
|
||||
} else {
|
||||
method = 'POST';
|
||||
url = `${window.BASEURL}api/master/controls`;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
alert("Data saved successfully!");
|
||||
this.closeModal();
|
||||
this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Something went wrong.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save data.");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteData(id) {
|
||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}api/master/controls/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
alert("Data deleted successfully!");
|
||||
this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to delete.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete data.");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
44
app/Views/master/dept/dialog_dept_form.php
Normal file
44
app/Views/master/dept/dialog_dept_form.php
Normal file
@ -0,0 +1,44 @@
|
||||
<dialog class="modal modal-bottom sm:modal-middle" :class="{ 'modal-open': showModal }">
|
||||
<div class="modal-box border border-base-300 shadow-2xl bg-base-100">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2 text-base-content">
|
||||
<i class="fa-solid fa-building text-primary"></i>
|
||||
<span x-text="form.deptId ? 'Edit Department' : 'New Department'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Department Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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.deptName}"
|
||||
x-model="form.deptName"
|
||||
placeholder="Enter department name"
|
||||
/>
|
||||
<template x-if="errors.deptName">
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error" x-text="errors.deptName"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button class="btn btn-ghost opacity-70" @click="closeModal()">Cancel</button>
|
||||
<button
|
||||
class="btn btn-primary gap-2 shadow-lg shadow-primary/20 font-medium"
|
||||
:class="{'loading': loading}"
|
||||
@click="save()"
|
||||
:disabled="loading"
|
||||
>
|
||||
<template x-if="!loading">
|
||||
<span><i class="fa-solid fa-save"></i> Save</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span>Saving...</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop bg-black/60" @click="closeModal()"></form>
|
||||
</dialog>
|
||||
231
app/Views/master/dept/index.php
Normal file
231
app/Views/master/dept/index.php
Normal file
@ -0,0 +1,231 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto" x-data="departments()">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">Departments</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Manage laboratory departments</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm gap-2 shadow-sm border-0 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white transition-all duration-200"
|
||||
@click="showForm()"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i> New Department
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-4 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
class="w-full pl-10 pr-4 py-2.5 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>
|
||||
<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()"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden">
|
||||
<template x-if="loading">
|
||||
<div class="p-8 text-center">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p class="mt-2 text-base-content/60">Loading...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && error">
|
||||
<div class="p-8 text-center">
|
||||
<i class="fa-solid fa-triangle-exclamation text-4xl text-error mb-2"></i>
|
||||
<p class="text-error" x-text="error"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && !error && list">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="uppercase tracking-wider font-semibold bg-base-200 text-base-content/70 text-xs">
|
||||
<tr>
|
||||
<th class="py-3 px-5 font-semibold">Name</th>
|
||||
<th class="py-3 px-5 font-semibold text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-base-content/80 divide-y divide-base-300">
|
||||
<template x-for="item in list" :key="item.deptId">
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-3 px-5 font-medium text-base-content" x-text="item.deptName"></td>
|
||||
<td class="py-3 px-5 text-right">
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
@click="showForm(item.deptId)"
|
||||
>
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-error bg-error/10 hover:bg-error/20 rounded-lg transition-colors ml-1"
|
||||
@click="deleteData(item.deptId)"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="list.length === 0">
|
||||
<tr>
|
||||
<td colspan="2" class="py-8 text-center text-base-content/60">No data available</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<?= $this->include('master/dept/dialog_dept_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script"); ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("departments", () => ({
|
||||
loading: false,
|
||||
showModal: false,
|
||||
errors: {},
|
||||
error: null,
|
||||
keyword: "",
|
||||
list: null,
|
||||
form: {
|
||||
deptId: null,
|
||||
deptName: "",
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.list = null;
|
||||
try {
|
||||
const params = new URLSearchParams({ keyword: this.keyword });
|
||||
const response = await fetch(`${window.BASEURL}api/master/depts?${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;
|
||||
}
|
||||
},
|
||||
|
||||
async loadData(id) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}api/master/depts/${id}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load item");
|
||||
const data = await response.json();
|
||||
this.form = data.data[0];
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
this.form = {};
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async showForm(id = null) {
|
||||
this.showModal = true;
|
||||
this.errors = {};
|
||||
if (id) {
|
||||
await this.loadData(id);
|
||||
} else {
|
||||
this.form = { deptId: null, deptName: "" };
|
||||
}
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.form = { deptId: null, deptName: "" };
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.form.deptName) this.errors.deptName = "Name is required.";
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
this.loading = true;
|
||||
let method = '';
|
||||
let url = '';
|
||||
if (this.form.deptId) {
|
||||
method = 'PATCH';
|
||||
url = `${window.BASEURL}api/master/depts/${this.form.deptId}`;
|
||||
} else {
|
||||
method = 'POST';
|
||||
url = `${window.BASEURL}api/master/depts`;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
alert("Data saved successfully!");
|
||||
this.closeModal();
|
||||
this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Something went wrong.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save data.");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteData(id) {
|
||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}api/master/depts/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
alert("Data deleted successfully!");
|
||||
this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to delete.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete data.");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
113
app/Views/master/test/dialog_test_form.php
Normal file
113
app/Views/master/test/dialog_test_form.php
Normal file
@ -0,0 +1,113 @@
|
||||
<dialog class="modal modal-bottom sm:modal-middle" :class="{ 'modal-open': showModal }">
|
||||
<div class="modal-box border border-base-300 shadow-2xl bg-base-100">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2 text-base-content">
|
||||
<i class="fa-solid fa-flask-vial text-primary"></i>
|
||||
<span x-text="form.testId ? 'Edit Test' : 'New Test'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Test Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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.testName}"
|
||||
x-model="form.testName"
|
||||
placeholder="Enter test name"
|
||||
/>
|
||||
<template x-if="errors.testName">
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error" x-text="errors.testName"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Unit</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.testUnit"
|
||||
placeholder="e.g., mg/dL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Method</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.testMethod"
|
||||
placeholder="e.g., Enzymatic"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">CVa</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.cva"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">BA</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.ba"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">TEa</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-bordered 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"
|
||||
x-model="form.tea"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button class="btn btn-ghost opacity-70" @click="closeModal()">Cancel</button>
|
||||
<button
|
||||
class="btn btn-primary gap-2 shadow-lg shadow-primary/20 font-medium"
|
||||
:class="{'loading': loading}"
|
||||
@click="save()"
|
||||
:disabled="loading"
|
||||
>
|
||||
<template x-if="!loading">
|
||||
<span><i class="fa-solid fa-save"></i> Save</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span>Saving...</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop bg-black/60" @click="closeModal()"></form>
|
||||
</dialog>
|
||||
240
app/Views/master/test/index.php
Normal file
240
app/Views/master/test/index.php
Normal file
@ -0,0 +1,240 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto" x-data="tests()">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">Tests</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Manage laboratory tests and parameters</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm gap-2 shadow-sm border-0 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white transition-all duration-200"
|
||||
@click="showForm()"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i> New Test
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-4 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
class="w-full pl-10 pr-4 py-2.5 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>
|
||||
<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()"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden">
|
||||
<template x-if="loading">
|
||||
<div class="p-8 text-center">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p class="mt-2 text-base-content/60">Loading...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && error">
|
||||
<div class="p-8 text-center">
|
||||
<i class="fa-solid fa-triangle-exclamation text-4xl text-error mb-2"></i>
|
||||
<p class="text-error" x-text="error"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && !error && list">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="uppercase tracking-wider font-semibold bg-base-200 text-base-content/70 text-xs">
|
||||
<tr>
|
||||
<th class="py-3 px-5 font-semibold">Test Name</th>
|
||||
<th class="py-3 px-5 font-semibold">Unit</th>
|
||||
<th class="py-3 px-5 font-semibold">Method</th>
|
||||
<th class="py-3 px-5 font-semibold text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-base-content/80 divide-y divide-base-300">
|
||||
<template x-for="item in list" :key="item.testId">
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-3 px-5 font-medium text-base-content" x-text="item.testName"></td>
|
||||
<td class="py-3 px-5" x-text="item.testUnit"></td>
|
||||
<td class="py-3 px-5" x-text="item.testMethod"></td>
|
||||
<td class="py-3 px-5 text-right">
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
@click="showForm(item.testId)"
|
||||
>
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-error bg-error/10 hover:bg-error/20 rounded-lg transition-colors ml-1"
|
||||
@click="deleteData(item.testId)"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="list.length === 0">
|
||||
<tr>
|
||||
<td colspan="4" class="py-8 text-center text-base-content/60">No data available</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<?= $this->include('master/test/dialog_test_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script"); ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("tests", () => ({
|
||||
loading: false,
|
||||
showModal: false,
|
||||
errors: {},
|
||||
error: null,
|
||||
keyword: "",
|
||||
list: null,
|
||||
form: {
|
||||
testId: null,
|
||||
testName: "",
|
||||
testUnit: "",
|
||||
testMethod: "",
|
||||
cva: "",
|
||||
ba: "",
|
||||
tea: "",
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.list = null;
|
||||
try {
|
||||
const params = new URLSearchParams({ keyword: this.keyword });
|
||||
const response = await fetch(`${window.BASEURL}api/master/tests?${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;
|
||||
}
|
||||
},
|
||||
|
||||
async loadData(id) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(`${window.BASEURL}api/master/tests/${id}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to load item");
|
||||
const data = await response.json();
|
||||
this.form = data.data[0];
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
this.form = {};
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async showForm(id = null) {
|
||||
this.showModal = true;
|
||||
this.errors = {};
|
||||
if (id) {
|
||||
await this.loadData(id);
|
||||
} else {
|
||||
this.form = { testId: null, testName: "", testUnit: "", testMethod: "", cva: "", ba: "", tea: "" };
|
||||
}
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.form = { testId: null, testName: "", testUnit: "", testMethod: "", cva: "", ba: "", tea: "" };
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.form.testName) this.errors.testName = "Name is required.";
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
this.loading = true;
|
||||
let method = '';
|
||||
let url = '';
|
||||
if (this.form.testId) {
|
||||
method = 'PATCH';
|
||||
url = `${window.BASEURL}api/master/tests/${this.form.testId}`;
|
||||
} else {
|
||||
method = 'POST';
|
||||
url = `${window.BASEURL}api/master/tests`;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
alert("Data saved successfully!");
|
||||
this.closeModal();
|
||||
this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Something went wrong.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save data.");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteData(id) {
|
||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}api/master/tests/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
alert("Data deleted successfully!");
|
||||
this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Failed to delete.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to delete data.");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
@ -1,141 +1,16 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="reportIndex()">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-800">Generate Report</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Select parameters to generate QC report</p>
|
||||
</div>
|
||||
|
||||
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="generateReport()">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Month <span class="text-red-500">*</span></label>
|
||||
<input type="month" name="dates" x-model="dates" :class="errors.dates ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" required>
|
||||
<p x-show="errors.dates" x-text="errors.dates" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Test <span class="text-red-500">*</span></label>
|
||||
<select name="test" x-model="test" :class="errors.test ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" required>
|
||||
<option value="">Select Test</option>
|
||||
<?php if (isset($tests)): ?>
|
||||
<?php foreach ($tests as $t): ?>
|
||||
<option value="<?= $t['test_id'] ?>"><?= $t['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<p x-show="errors.test" x-text="errors.test" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Control 1 <span class="text-red-500">*</span></label>
|
||||
<select name="control1" x-model="control1" :class="errors.control1 ? 'border-red-300 bg-red-50' : ''" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" required>
|
||||
<option value="">Select Control</option>
|
||||
<?php if (isset($controls)): ?>
|
||||
<?php foreach ($controls as $c): ?>
|
||||
<option value="<?= $c['control_id'] ?>"><?= $c['name'] ?> (<?= $c['lot'] ?? 'N/A' ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<p x-show="errors.control1" x-text="errors.control1" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Control 2</label>
|
||||
<select name="control2" x-model="control2" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Optional</option>
|
||||
<?php if (isset($controls)): ?>
|
||||
<?php foreach ($controls as $c): ?>
|
||||
<option value="<?= $c['control_id'] ?>"><?= $c['name'] ?> (<?= $c['lot'] ?? 'N/A' ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Control 3</label>
|
||||
<select name="control3" x-model="control3" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
|
||||
<option value="">Optional</option>
|
||||
<?php if (isset($controls)): ?>
|
||||
<?php foreach ($controls as $c): ?>
|
||||
<option value="<?= $c['control_id'] ?>"><?= $c['name'] ?> (<?= $c['lot'] ?? 'N/A' ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4">
|
||||
<button type="submit" :disabled="loading" class="btn btn-primary">
|
||||
<template x-if="!loading">
|
||||
<span class="flex items-center">
|
||||
<i class="fa-solid fa-chart-column mr-2"></i>
|
||||
Generate Report
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span class="flex items-center">
|
||||
<i class="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Generating...
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">Reports</h1>
|
||||
<p class="text-sm mt-1 opacity-70">Generate and view QC reports</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<p class="text-base-content/60 text-center py-8">Reports module coming soon...</p>
|
||||
</div>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("reportIndex", () => ({
|
||||
loading: false,
|
||||
error: '',
|
||||
dates: '<?= date('Y-m') ?>',
|
||||
test: '',
|
||||
control1: '',
|
||||
control2: '',
|
||||
control3: '',
|
||||
errors: {},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.dates) this.errors.dates = 'Month is required';
|
||||
if (!this.test) this.errors.test = 'Test is required';
|
||||
if (!this.control1) this.errors.control1 = 'Control 1 is required';
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
generateReport() {
|
||||
if (!this.validate()) {
|
||||
App.showToast('Please fill all required fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
dates: this.dates,
|
||||
test: this.test,
|
||||
control1: this.control1
|
||||
});
|
||||
|
||||
if (this.control2) params.append('control2', this.control2);
|
||||
if (this.control3) params.append('control3', this.control3);
|
||||
|
||||
window.location.href = `${window.BASEURL}/report/view?${params.toString()}`;
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
|
||||
@ -1,286 +1,16 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold text-slate-800">QC Report - <?= $dates ?></h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="window.print()" class="btn btn-secondary">
|
||||
<i class="fa-solid fa-print mr-2"></i>
|
||||
Print Report
|
||||
</button>
|
||||
|
||||
<?= $this->section("content"); ?>
|
||||
<main class="flex-1 p-6 overflow-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-base-content tracking-tight">Report View</h1>
|
||||
<p class="text-sm mt-1 opacity-70">View detailed QC report</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (count($reportData) > 1): ?>
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4">QC Trend Overview</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="h-80">
|
||||
<canvas id="trend-chart"></canvas>
|
||||
</div>
|
||||
<div class="h-80">
|
||||
<canvas id="comparison-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
||||
<p class="text-base-content/60 text-center py-8">Report viewer coming soon...</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($reportData as $index => $data): ?>
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden page-break" id="report-<?= $index ?>">
|
||||
<div class="px-6 py-4 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-800"><?= $data['control']['name'] ?> (Lot: <?= $data['control']['lot'] ?? 'N/A' ?>)</h3>
|
||||
<p class="text-sm text-slate-500"><?= $data['test']['name'] ?? 'N/A' ?></p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="px-3 py-1 text-sm rounded-full <?= $data['outOfRange'] > 0 ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' ?>">
|
||||
<?= $data['outOfRange'] ?> Out of Range
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($data['controlTest']): ?>
|
||||
<?php
|
||||
$mean = $data['controlTest']['mean'];
|
||||
$sd = $data['controlTest']['sd'];
|
||||
$results = $data['results'];
|
||||
$daysInMonth = date('t', strtotime($dates . '-01'));
|
||||
$chartLabels = [];
|
||||
$chartValues = [];
|
||||
$upperLimit = $mean + 2 * $sd;
|
||||
$lowerLimit = $mean - 2 * $sd;
|
||||
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$chartLabels[] = $day;
|
||||
$value = null;
|
||||
foreach ($results as $result) {
|
||||
if (date('j', strtotime($result['resdate'])) == $day) {
|
||||
$value = $result['resvalue'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$chartValues[] = $value !== null ? $value : 'null';
|
||||
}
|
||||
?>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="text-center p-4 bg-slate-50 rounded-xl">
|
||||
<p class="text-sm text-slate-500">Mean</p>
|
||||
<p class="text-xl font-bold text-slate-800"><?= number_format($mean, 3) ?></p>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-slate-50 rounded-xl">
|
||||
<p class="text-sm text-slate-500">SD</p>
|
||||
<p class="text-xl font-bold text-slate-800"><?= number_format($sd, 3) ?></p>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-green-50 rounded-xl">
|
||||
<p class="text-sm text-green-600">+2SD</p>
|
||||
<p class="text-xl font-bold text-green-700"><?= number_format($upperLimit, 3) ?></p>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-red-50 rounded-xl">
|
||||
<p class="text-sm text-red-600">-2SD</p>
|
||||
<p class="text-xl font-bold text-red-700"><?= number_format($lowerLimit, 3) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-medium text-slate-700 mb-2">Trend Chart</h4>
|
||||
<div class="h-64">
|
||||
<canvas id="chart-<?= $index ?>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="py-3 px-4 font-semibold">Day</th>
|
||||
<th class="py-3 px-4 font-semibold">Value</th>
|
||||
<th class="py-3 px-4 font-semibold">Z-Score</th>
|
||||
<th class="py-3 px-4 font-semibold">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<?php
|
||||
for ($day = 1; $day <= $daysInMonth; $day++):
|
||||
$value = null;
|
||||
foreach ($results as $result) {
|
||||
if (date('j', strtotime($result['resdate'])) == $day) {
|
||||
$value = $result['resvalue'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$zScore = ($value && $sd > 0) ? ($value - $mean) / $sd : null;
|
||||
$status = $zScore !== null ? (abs($zScore) > 2 ? 'Out' : (abs($zScore) > 1 ? 'Warn' : 'OK')) : '-';
|
||||
$statusClass = $status == 'Out' ? 'text-red-600 font-bold bg-red-50' : ($status == 'Warn' ? 'text-yellow-600 bg-yellow-50' : 'text-green-600');
|
||||
?>
|
||||
<tr class="hover:bg-slate-50/50">
|
||||
<td class="py-3 px-4 text-slate-800"><?= $day ?></td>
|
||||
<td class="py-3 px-4 font-medium text-slate-800"><?= $value ?? '-' ?></td>
|
||||
<td class="py-3 px-4 text-slate-600"><?= $zScore !== null ? number_format($zScore, 2) : '-' ?></td>
|
||||
<td class="py-3 px-4 <?= $statusClass ?>"><?= $status ?></td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($data['comment']): ?>
|
||||
<div class="mt-4 p-4 bg-amber-50 rounded-xl">
|
||||
<p class="text-sm font-medium text-amber-800">Monthly Comment:</p>
|
||||
<p class="text-slate-700"><?= $data['comment']['comtext'] ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="p-6 text-center text-slate-500">
|
||||
No test data found for this control.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
const reportData = <?= json_encode(array_map(function($data) use ($dates) {
|
||||
$mean = $data['controlTest']['mean'] ?? 0;
|
||||
$sd = $data['controlTest']['sd'] ?? 1;
|
||||
$daysInMonth = date('t', strtotime($dates . '-01'));
|
||||
$values = [];
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$value = null;
|
||||
foreach ($data['results'] as $result) {
|
||||
if (date('j', strtotime($result['resdate'])) == $day) {
|
||||
$value = $result['resvalue'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$values[] = $value;
|
||||
}
|
||||
return [
|
||||
'name' => $data['control']['name'],
|
||||
'lot' => $data['control']['lot'],
|
||||
'mean' => $mean,
|
||||
'sd' => $sd,
|
||||
'values' => $values
|
||||
];
|
||||
}, $reportData)) ?>;
|
||||
|
||||
const dates = '<?= $dates ?>';
|
||||
const daysInMonth = Array.from({length: 31}, (_, i) => i + 1);
|
||||
|
||||
function initReportCharts() {
|
||||
if (typeof ChartManager === 'undefined') {
|
||||
setTimeout(initReportCharts, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reportData.length > 1) {
|
||||
const trendDatasets = reportData.map((data, i) => ({
|
||||
label: data.name,
|
||||
data: data.values,
|
||||
color: ChartManager.getColor(i)
|
||||
}));
|
||||
ChartManager.createTrendChart('trend-chart', daysInMonth, trendDatasets);
|
||||
|
||||
const comparisonDatasets = reportData.map((data, i) => ({
|
||||
label: data.name,
|
||||
data: data.values,
|
||||
color: ChartManager.getColor(i)
|
||||
}));
|
||||
ChartManager.createComparisonChart('comparison-chart', daysInMonth, comparisonDatasets);
|
||||
}
|
||||
|
||||
reportData.forEach((data, index) => {
|
||||
const ctx = document.getElementById(`chart-${index}`);
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: daysInMonth,
|
||||
datasets: [{
|
||||
label: data.name,
|
||||
data: data.values,
|
||||
borderColor: ChartManager.getColor(index),
|
||||
backgroundColor: ChartManager.getColor(index, 0.1),
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: data.values.map(v => {
|
||||
if (v === null) return 'transparent';
|
||||
if (data.sd === 0) return ChartManager.getColor(index);
|
||||
const z = (v - data.mean) / data.sd;
|
||||
return Math.abs(z) > 2 ? '#ef4444' : (Math.abs(z) > 1 ? '#f59e0b' : ChartManager.getColor(index));
|
||||
})
|
||||
}, {
|
||||
label: '+2SD',
|
||||
data: Array(31).fill(data.mean + 2 * data.sd),
|
||||
borderColor: '#22c55e',
|
||||
borderDash: [5, 5],
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}, {
|
||||
label: '-2SD',
|
||||
data: Array(31).fill(data.mean - 2 * data.sd),
|
||||
borderColor: '#ef4444',
|
||||
borderDash: [5, 5],
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}, {
|
||||
label: 'Mean',
|
||||
data: Array(31).fill(data.mean),
|
||||
borderColor: '#3b82f6',
|
||||
borderDash: [2, 2],
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: `${data.name} (Mean: ${data.mean.toFixed(3)}, SD: ${data.sd.toFixed(3)})`
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Value'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Day'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initReportCharts);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.page-break {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.bg-white {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
<!-- Backdrop -->
|
||||
<div x-show="showModal"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-40"
|
||||
@click="closeModal()">
|
||||
</div>
|
||||
|
||||
<!-- Modal Panel -->
|
||||
<div x-show="showModal"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl" @click.stop>
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-slate-800" x-text="form.test_id ? 'Edit Test' : 'Add Test'"></h3>
|
||||
<button @click="closeModal()" class="text-slate-400 hover:text-slate-600">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="save()">
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Department <span class="text-red-500">*</span></label>
|
||||
<select x-model="form.dept_ref_id" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" :class="{'border-red-300 bg-red-50': errors.dept_ref_id}" required>
|
||||
<option value="">Select Department</option>
|
||||
<template x-for="dept in depts" :key="dept.dept_id">
|
||||
<option :value="dept.dept_id" x-text="dept.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p x-show="errors.dept_ref_id" x-text="errors.dept_ref_id" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Test Name <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="form.name" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" :class="{'border-red-300 bg-red-50': errors.name}" placeholder="Enter test name" required>
|
||||
<p x-show="errors.name" x-text="errors.name" class="text-red-500 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Unit</label>
|
||||
<input type="text" x-model="form.unit" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="e.g., mg/dL">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Method</label>
|
||||
<input type="text" x-model="form.method" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="e.g., Spectrophotometry">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">CVA</label>
|
||||
<input type="text" x-model="form.cva" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Coefficient of Variation A">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">BA</label>
|
||||
<input type="text" x-model="form.ba" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Bias A">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">TEA</label>
|
||||
<input type="text" x-model="form.tea" class="w-full px-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Total Error Allowable">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50 rounded-b-2xl">
|
||||
<button type="button" @click="closeModal()" class="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-800 transition-colors">Cancel</button>
|
||||
<button type="submit" :disabled="loading" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<template x-if="!loading">
|
||||
<span><i class="fa-solid fa-check mr-1"></i> <span x-text="form.test_id ? 'Update' : 'Save'"></span></span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span><i class="fa-solid fa-spinner fa-spin mr-1"></i> Saving...</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,231 +0,0 @@
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
<?= $this->section("content") ?>
|
||||
<main x-data="testIndex()">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Test Dictionary</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage test types, methods, and units</p>
|
||||
</div>
|
||||
<button @click="showForm()" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus mr-2"></i>Add Test
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span x-text="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Card -->
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm p-4 mb-6">
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input type="text" x-model="keyword" @keyup.enter="fetchList()" class="w-full pl-10 pr-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="Search tests...">
|
||||
</div>
|
||||
<button @click="fetchList()" class="btn btn-primary">
|
||||
<i class="fa-solid fa-magnifying-glass mr-2"></i>Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table Card -->
|
||||
<div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<!-- Loading State -->
|
||||
<template x-if="loading">
|
||||
<div class="p-12 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-blue-50 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fa-solid fa-spinner fa-spin text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm">Loading tests...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="!loading && (!list || list.length === 0)">
|
||||
<div class="flex-1 flex items-center justify-center p-8">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
|
||||
<i class="fa-solid fa-vial text-slate-400 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm">No tests found</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Data Table -->
|
||||
<template x-if="!loading && list && list.length > 0">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
<th class="py-3 px-5 font-semibold">#</th>
|
||||
<th class="py-3 px-5 font-semibold">Name</th>
|
||||
<th class="py-3 px-5 font-semibold">Department</th>
|
||||
<th class="py-3 px-5 font-semibold">Unit</th>
|
||||
<th class="py-3 px-5 font-semibold">Method</th>
|
||||
<th class="py-3 px-5 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<template x-for="(item, index) in list" :key="item.test_id">
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="py-3 px-5" x-text="index + 1"></td>
|
||||
<td class="py-3 px-5 font-medium text-slate-800" x-text="item.name"></td>
|
||||
<td class="py-3 px-5 text-slate-600" x-text="item.dept_name || '-'"></td>
|
||||
<td class="py-3 px-5 text-slate-600" x-text="item.unit || '-'"></td>
|
||||
<td class="py-3 px-5 text-slate-600" x-text="item.method || '-'"></td>
|
||||
<td class="py-3 px-5 text-right">
|
||||
<button @click="showForm(item.test_id)" class="text-blue-600 hover:text-blue-800 mr-3">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button @click="deleteItem(item.test_id)" class="text-red-600 hover:text-red-800">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Include -->
|
||||
<?= $this->include('test/dialog_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("testIndex", () => ({
|
||||
loading: false,
|
||||
showModal: false,
|
||||
list: [],
|
||||
form: {},
|
||||
errors: {},
|
||||
error: '',
|
||||
keyword: '',
|
||||
depts: <?= json_encode($depts ?? []) ?>,
|
||||
|
||||
init() {
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}/api/test`);
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.list = data.data || [];
|
||||
} else {
|
||||
this.error = data.message || 'Failed to load tests';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
showForm(id = null) {
|
||||
this.errors = {};
|
||||
if (id) {
|
||||
const item = this.list.find(x => x.test_id === id);
|
||||
if (item) {
|
||||
this.form = {
|
||||
test_id: item.test_id,
|
||||
dept_ref_id: item.dept_ref_id,
|
||||
name: item.name,
|
||||
unit: item.unit || '',
|
||||
method: item.method || '',
|
||||
cva: item.cva || '',
|
||||
ba: item.ba || '',
|
||||
tea: item.tea || ''
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this.form = {
|
||||
dept_ref_id: '',
|
||||
name: '',
|
||||
unit: '',
|
||||
method: '',
|
||||
cva: '',
|
||||
ba: '',
|
||||
tea: ''
|
||||
};
|
||||
}
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
this.form = {};
|
||||
},
|
||||
|
||||
validate() {
|
||||
this.errors = {};
|
||||
if (!this.form.dept_ref_id) this.errors.dept_ref_id = 'Department is required';
|
||||
if (!this.form.name) this.errors.name = 'Test name is required';
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const url = this.form.test_id
|
||||
? `${window.BASEURL}/api/test/${this.form.test_id}`
|
||||
: `${window.BASEURL}/api/test`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: this.form.test_id ? 'PATCH' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
this.closeModal();
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to save test';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteItem(id) {
|
||||
if (!confirm('Are you sure you want to delete this test?')) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${window.BASEURL}/api/test/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.error = data.message || 'Failed to delete test';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = 'Network error. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
BIN
backup/qc20260114.bak
Normal file
BIN
backup/qc20260114.bak
Normal file
Binary file not shown.
BIN
backup/script.sql
Normal file
BIN
backup/script.sql
Normal file
Binary file not shown.
170
backup/script_utf8.sql
Normal file
170
backup/script_utf8.sql
Normal file
@ -0,0 +1,170 @@
|
||||
USE [master]
|
||||
GO
|
||||
/****** Object: Database [cmod_qc] Script Date: 17/01/2026 18:26:32 ******/
|
||||
CREATE DATABASE [cmod_qc]
|
||||
CONTAINMENT = NONE
|
||||
ON PRIMARY
|
||||
( NAME = N'cmod_qc', FILENAME = N'C:\db\cmod_qc.mdf' , SIZE = 8192KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
|
||||
LOG ON
|
||||
( NAME = N'cmod_qc_log', FILENAME = N'C:\db\cmod_qc_log.ldf' , SIZE = 32448KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
|
||||
WITH CATALOG_COLLATION = DATABASE_DEFAULT, LEDGER = OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET COMPATIBILITY_LEVEL = 110
|
||||
GO
|
||||
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
|
||||
begin
|
||||
EXEC [cmod_qc].[dbo].[sp_fulltext_database] @action = 'enable'
|
||||
end
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ANSI_NULL_DEFAULT OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ANSI_NULLS OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ANSI_PADDING OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ANSI_WARNINGS OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ARITHABORT OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET AUTO_CLOSE OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET AUTO_SHRINK OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET AUTO_UPDATE_STATISTICS ON
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET CURSOR_CLOSE_ON_COMMIT OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET CURSOR_DEFAULT GLOBAL
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET CONCAT_NULL_YIELDS_NULL OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET NUMERIC_ROUNDABORT OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET QUOTED_IDENTIFIER OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET RECURSIVE_TRIGGERS OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET DISABLE_BROKER
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET DATE_CORRELATION_OPTIMIZATION OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET TRUSTWORTHY OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ALLOW_SNAPSHOT_ISOLATION OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET PARAMETERIZATION SIMPLE
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET READ_COMMITTED_SNAPSHOT OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET HONOR_BROKER_PRIORITY OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET RECOVERY SIMPLE
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET MULTI_USER
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET PAGE_VERIFY CHECKSUM
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET DB_CHAINING OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF )
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET TARGET_RECOVERY_TIME = 0 SECONDS
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET DELAYED_DURABILITY = DISABLED
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET ACCELERATED_DATABASE_RECOVERY = OFF
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET QUERY_STORE = OFF
|
||||
GO
|
||||
USE [cmod_qc]
|
||||
GO
|
||||
/****** Object: Table [dbo].[CONTROL_TEST] Script Date: 17/01/2026 18:26:33 ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
CREATE TABLE [dbo].[CONTROL_TEST](
|
||||
[id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[controlid] [int] NULL,
|
||||
[testid] [int] NULL,
|
||||
[mean] [float] NULL,
|
||||
[sd] [float] NULL
|
||||
) ON [PRIMARY]
|
||||
GO
|
||||
/****** Object: Table [dbo].[DAILY_RESULT] Script Date: 17/01/2026 18:26:33 ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
CREATE TABLE [dbo].[DAILY_RESULT](
|
||||
[id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[controlid] [int] NULL,
|
||||
[testid] [int] NULL,
|
||||
[resdate] [datetime] NULL,
|
||||
[resvalue] [varchar](50) NULL,
|
||||
[rescomment] [text] NULL
|
||||
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
|
||||
GO
|
||||
/****** Object: Table [dbo].[DICT_CONTROL] Script Date: 17/01/2026 18:26:33 ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
CREATE TABLE [dbo].[DICT_CONTROL](
|
||||
[id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[deptid] [int] NULL,
|
||||
[name] [varchar](50) NULL,
|
||||
[lot] [varchar](50) NULL,
|
||||
[producer] [text] NULL,
|
||||
[expdate] [date] NULL
|
||||
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
|
||||
GO
|
||||
/****** Object: Table [dbo].[DICT_DEPT] Script Date: 17/01/2026 18:26:33 ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
CREATE TABLE [dbo].[DICT_DEPT](
|
||||
[id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[name] [varchar](50) NULL,
|
||||
CONSTRAINT [PK_DICT_DEPT] PRIMARY KEY CLUSTERED
|
||||
(
|
||||
[id] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
|
||||
) ON [PRIMARY]
|
||||
GO
|
||||
/****** Object: Table [dbo].[DICT_TEST] Script Date: 17/01/2026 18:26:33 ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
CREATE TABLE [dbo].[DICT_TEST](
|
||||
[id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[deptid] [int] NULL,
|
||||
[name] [varchar](50) NULL,
|
||||
[unit] [varchar](50) NULL,
|
||||
[method] [varchar](50) NULL,
|
||||
[cva] [varchar](50) NULL,
|
||||
[ba] [varchar](50) NULL,
|
||||
[tea] [varchar](50) NULL
|
||||
) ON [PRIMARY]
|
||||
GO
|
||||
/****** Object: Table [dbo].[MONTHLY_COMMENT] Script Date: 17/01/2026 18:26:33 ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
CREATE TABLE [dbo].[MONTHLY_COMMENT](
|
||||
[id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[controlid] [int] NOT NULL,
|
||||
[testid] [int] NOT NULL,
|
||||
[commonth] [varchar](7) NOT NULL,
|
||||
[comtext] [text] NULL
|
||||
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
|
||||
GO
|
||||
USE [master]
|
||||
GO
|
||||
ALTER DATABASE [cmod_qc] SET READ_WRITE
|
||||
GO
|
||||
1009
docs/PRD.md
Normal file
1009
docs/PRD.md
Normal file
File diff suppressed because it is too large
Load Diff
1852
docs/llms.txt
Normal file
1852
docs/llms.txt
Normal file
File diff suppressed because it is too large
Load Diff
221
docs/prd.json
Normal file
221
docs/prd.json
Normal file
@ -0,0 +1,221 @@
|
||||
{
|
||||
"name": "TinyQC Product Requirements Document (PRD)",
|
||||
"description": "**TinyQC** is a lightweight, web-based Laboratory Quality Control (QC) Management System designed to help laboratories record, track, and analyze quality control test results with statistical analysis capabilities.",
|
||||
"branchName": "main",
|
||||
"userStories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "View Departments",
|
||||
"description": "View Departments",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 1,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-002",
|
||||
"title": "Create Department",
|
||||
"description": "Create Department",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 2,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-003",
|
||||
"title": "Edit Department",
|
||||
"description": "Edit Department",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 3,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-004",
|
||||
"title": "Delete Department",
|
||||
"description": "Delete Department",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-005",
|
||||
"title": "View Tests",
|
||||
"description": "View Tests",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-006",
|
||||
"title": "Create Test",
|
||||
"description": "Create Test",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-007",
|
||||
"title": "Edit Test",
|
||||
"description": "Edit Test",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-008",
|
||||
"title": "Delete Test",
|
||||
"description": "Delete Test",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-009",
|
||||
"title": "View Controls",
|
||||
"description": "View Controls",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-010",
|
||||
"title": "Create Control",
|
||||
"description": "Create Control",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-011",
|
||||
"title": "Edit Control",
|
||||
"description": "Edit Control",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-012",
|
||||
"title": "Delete Control",
|
||||
"description": "Delete Control",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-013",
|
||||
"title": "Daily Entry Interface",
|
||||
"description": "Daily Entry Interface",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-014",
|
||||
"title": "Save Daily Result",
|
||||
"description": "Save Daily Result",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-015",
|
||||
"title": "Monthly Entry Interface",
|
||||
"description": "Monthly Entry Interface",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-016",
|
||||
"title": "Save Monthly Results",
|
||||
"description": "Save Monthly Results",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-017",
|
||||
"title": "Daily Comments",
|
||||
"description": "User can entry comment per day, per test.",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-018",
|
||||
"title": "Report Generation Form",
|
||||
"description": "Report Generation Form",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
},
|
||||
{
|
||||
"id": "US-019",
|
||||
"title": "Report Display",
|
||||
"description": "Report Display",
|
||||
"acceptanceCriteria": [],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"labels": [],
|
||||
"dependsOn": [],
|
||||
"completionNotes": "Completed by agent"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"createdAt": "2026-01-16T09:22:41.726Z",
|
||||
"version": "1.0.0",
|
||||
"updatedAt": "2026-01-16T10:27:48.837Z"
|
||||
}
|
||||
}
|
||||
238
index.json
Normal file
238
index.json
Normal file
@ -0,0 +1,238 @@
|
||||
{
|
||||
"generatedAt": "2026-01-18T10:00:00Z",
|
||||
"techStack": {
|
||||
"backend": "CodeIgniter 4 (PHP 8.1+)",
|
||||
"frontend": "Alpine.js + TailwindCSS + daisyUI",
|
||||
"database": "MySQL/MariaDB"
|
||||
},
|
||||
"summary": {
|
||||
"controllers": 7,
|
||||
"models": 7,
|
||||
"migrations": 1,
|
||||
"apiEndpoints": 45,
|
||||
"views": 18,
|
||||
"tables": 6
|
||||
},
|
||||
"agentsMdSummary": {
|
||||
"codingConventions": [
|
||||
"Controllers extend BaseController + ResponseTrait",
|
||||
"Models extend App\\Models\\BaseModel (auto snake/camel conversion)",
|
||||
"All tables use soft deletes (deleted_at)",
|
||||
"API response format: { status, message, data }",
|
||||
"Frontend uses camelCase, DB uses snake_case"
|
||||
],
|
||||
"namingPatterns": {
|
||||
"controllers": "PascalCase + Controller",
|
||||
"models": "PascalCase + Model",
|
||||
"tables": "snake_case, prefix master_ for master data",
|
||||
"primaryKeys": "{table_singular}_id"
|
||||
},
|
||||
"directoryStructure": {
|
||||
"app/Controllers/": "API & page controllers",
|
||||
"app/Controllers/Master/": "Master data CRUD",
|
||||
"app/Controllers/Qc/": "QC operations",
|
||||
"app/Models/": "Eloquent-style models",
|
||||
"app/Models/Master/": "Master data models",
|
||||
"app/Models/Qc/": "QC operation models",
|
||||
"app/Database/Migrations/": "Schema definitions",
|
||||
"app/Config/Routes.php": "All routes",
|
||||
"app/Helpers/": "Utility functions",
|
||||
"app/Views/": "PHP views with Alpine.js"
|
||||
},
|
||||
"apiResponseFormat": {
|
||||
"success": "{ status: 'success', message: 'fetch success', data: rows }",
|
||||
"created": "{ status: 'success', message: id }",
|
||||
"error": "failServerError or failValidationErrors"
|
||||
}
|
||||
},
|
||||
"tables": [
|
||||
{
|
||||
"name": "master_depts",
|
||||
"pk": "dept_id",
|
||||
"columns": ["dept_id", "name", "created_at", "updated_at", "deleted_at"],
|
||||
"softDeletes": true,
|
||||
"description": "Departments/laboratory sections"
|
||||
},
|
||||
{
|
||||
"name": "master_controls",
|
||||
"pk": "control_id",
|
||||
"columns": ["control_id", "dept_id", "name", "lot", "producer", "exp_date", "created_at", "updated_at", "deleted_at"],
|
||||
"softDeletes": true,
|
||||
"description": "QC control materials"
|
||||
},
|
||||
{
|
||||
"name": "master_tests",
|
||||
"pk": "test_id",
|
||||
"columns": ["test_id", "dept_id", "name", "unit", "method", "cva", "ba", "tea", "created_at", "updated_at", "deleted_at"],
|
||||
"softDeletes": true,
|
||||
"description": "Available tests/methods"
|
||||
},
|
||||
{
|
||||
"name": "control_tests",
|
||||
"pk": "control_test_id",
|
||||
"columns": ["control_test_id", "control_id", "test_id", "mean", "sd", "created_at", "updated_at", "deleted_at"],
|
||||
"softDeletes": true,
|
||||
"description": "Junction: controls linked to tests with mean/sd"
|
||||
},
|
||||
{
|
||||
"name": "results",
|
||||
"pk": "result_id",
|
||||
"columns": ["result_id", "control_id", "test_id", "res_date", "res_value", "res_comment", "created_at", "updated_at", "deleted_at"],
|
||||
"softDeletes": true,
|
||||
"description": "Individual QC test results"
|
||||
},
|
||||
{
|
||||
"name": "result_comments",
|
||||
"pk": "result_comment_id",
|
||||
"columns": ["result_comment_id", "control_id", "test_id", "comment_month", "com_text", "created_at", "updated_at", "deleted_at"],
|
||||
"softDeletes": true,
|
||||
"description": "Monthly comments per control/test"
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"name": "BaseController",
|
||||
"namespace": "App\\Controllers",
|
||||
"file": "app/Controllers/BaseController.php",
|
||||
"methods": ["initController"],
|
||||
"abstract": true
|
||||
},
|
||||
{
|
||||
"name": "PageController",
|
||||
"namespace": "App\\Controllers",
|
||||
"file": "app/Controllers/PageController.php",
|
||||
"methods": ["dashboard", "masterDept", "masterTest", "masterControl", "entry", "entryDaily", "report", "reportView"]
|
||||
},
|
||||
{
|
||||
"name": "MasterDeptsController",
|
||||
"namespace": "App\\Controllers\\Master",
|
||||
"file": "app/Controllers/Master/MasterDeptsController.php",
|
||||
"methods": ["index", "show", "create", "update", "delete"]
|
||||
},
|
||||
{
|
||||
"name": "MasterTestsController",
|
||||
"namespace": "App\\Controllers\\Master",
|
||||
"file": "app/Controllers/Master/MasterTestsController.php",
|
||||
"methods": ["index", "show", "create", "update", "delete"]
|
||||
},
|
||||
{
|
||||
"name": "MasterControlsController",
|
||||
"namespace": "App\\Controllers\\Master",
|
||||
"file": "app/Controllers/Master/MasterControlsController.php",
|
||||
"methods": ["index", "show", "create", "update", "delete"]
|
||||
},
|
||||
{
|
||||
"name": "ResultsController",
|
||||
"namespace": "App\\Controllers\\Qc",
|
||||
"file": "app/Controllers/Qc/ResultsController.php",
|
||||
"methods": ["index", "show", "create", "update", "delete"]
|
||||
},
|
||||
{
|
||||
"name": "ControlTestsController",
|
||||
"namespace": "App\\Controllers\\Qc",
|
||||
"file": "app/Controllers/Qc/ControlTestsController.php",
|
||||
"methods": ["index", "show", "create", "update", "delete"]
|
||||
},
|
||||
{
|
||||
"name": "ResultCommentsController",
|
||||
"namespace": "App\\Controllers\\Qc",
|
||||
"file": "app/Controllers/Qc/ResultCommentsController.php",
|
||||
"methods": ["index", "show", "create", "update", "delete"]
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
{
|
||||
"name": "BaseModel",
|
||||
"namespace": "App\\Models",
|
||||
"file": "app/Models/BaseModel.php",
|
||||
"table": null,
|
||||
"pk": null,
|
||||
"abstract": true,
|
||||
"features": ["auto snake/camel conversion", "soft deletes", "timestamps"]
|
||||
},
|
||||
{
|
||||
"name": "MasterDeptsModel",
|
||||
"namespace": "App\\Models\\Master",
|
||||
"file": "app/Models/Master/MasterDeptsModel.php",
|
||||
"table": "master_depts",
|
||||
"pk": "dept_id"
|
||||
},
|
||||
{
|
||||
"name": "MasterTestsModel",
|
||||
"namespace": "App\\Models\\Master",
|
||||
"file": "app/Models/Master/MasterTestsModel.php",
|
||||
"table": "master_tests",
|
||||
"pk": "test_id"
|
||||
},
|
||||
{
|
||||
"name": "MasterControlsModel",
|
||||
"namespace": "App\\Models\\Master",
|
||||
"file": "app/Models/Master/MasterControlsModel.php",
|
||||
"table": "master_controls",
|
||||
"pk": "control_id"
|
||||
},
|
||||
{
|
||||
"name": "ResultsModel",
|
||||
"namespace": "App\\Models\\Qc",
|
||||
"file": "app/Models/Qc/ResultsModel.php",
|
||||
"table": "results",
|
||||
"pk": "result_id"
|
||||
},
|
||||
{
|
||||
"name": "ControlTestsModel",
|
||||
"namespace": "App\\Models\\Qc",
|
||||
"file": "app/Models/Qc/ControlTestsModel.php",
|
||||
"table": "control_tests",
|
||||
"pk": "control_test_id"
|
||||
},
|
||||
{
|
||||
"name": "ResultCommentsModel",
|
||||
"namespace": "App\\Models\\Qc",
|
||||
"file": "app/Models/Qc/ResultCommentsModel.php",
|
||||
"table": "result_comments",
|
||||
"pk": "result_comment_id"
|
||||
}
|
||||
],
|
||||
"apiEndpoints": [
|
||||
{ "method": "GET", "path": "/api/master/depts", "controller": "Master\\MasterDeptsController::index" },
|
||||
{ "method": "GET", "path": "/api/master/depts/(:num)", "controller": "Master\\MasterDeptsController::show/$1" },
|
||||
{ "method": "POST", "path": "/api/master/depts", "controller": "Master\\MasterDeptsController::create" },
|
||||
{ "method": "PATCH", "path": "/api/master/depts/(:num)", "controller": "Master\\MasterDeptsController::update/$1" },
|
||||
{ "method": "DELETE", "path": "/api/master/depts/(:num)", "controller": "Master\\MasterDeptsController::delete/$1" },
|
||||
{ "method": "GET", "path": "/api/master/tests", "controller": "Master\\MasterTestsController::index" },
|
||||
{ "method": "GET", "path": "/api/master/tests/(:num)", "controller": "Master\\MasterTestsController::show/$1" },
|
||||
{ "method": "POST", "path": "/api/master/tests", "controller": "Master\\MasterTestsController::create" },
|
||||
{ "method": "PATCH", "path": "/api/master/tests/(:num)", "controller": "Master\\MasterTestsController::update/$1" },
|
||||
{ "method": "DELETE", "path": "/api/master/tests/(:num)", "controller": "Master\\MasterTestsController::delete/$1" },
|
||||
{ "method": "GET", "path": "/api/master/controls", "controller": "Master\\MasterControlsController::index" },
|
||||
{ "method": "GET", "path": "/api/master/controls/(:num)", "controller": "Master\\MasterControlsController::show/$1" },
|
||||
{ "method": "POST", "path": "/api/master/controls", "controller": "Master\\MasterControlsController::create" },
|
||||
{ "method": "PATCH", "path": "/api/master/controls/(:num)", "controller": "Master\\MasterControlsController::update/$1" },
|
||||
{ "method": "DELETE", "path": "/api/master/controls/(:num)", "controller": "Master\\MasterControlsController::delete/$1" },
|
||||
{ "method": "GET", "path": "/api/qc/control-tests", "controller": "Qc\\ControlTestsController::index" },
|
||||
{ "method": "GET", "path": "/api/qc/control-tests/(:num)", "controller": "Qc\\ControlTestsController::show/$1" },
|
||||
{ "method": "POST", "path": "/api/qc/control-tests", "controller": "Qc\\ControlTestsController::create" },
|
||||
{ "method": "PATCH", "path": "/api/qc/control-tests/(:num)", "controller": "Qc\\ControlTestsController::update/$1" },
|
||||
{ "method": "DELETE", "path": "/api/qc/control-tests/(:num)", "controller": "Qc\\ControlTestsController::delete/$1" },
|
||||
{ "method": "GET", "path": "/api/qc/results", "controller": "Qc\\ResultsController::index" },
|
||||
{ "method": "GET", "path": "/api/qc/results/(:num)", "controller": "Qc\\ResultsController::show/$1" },
|
||||
{ "method": "POST", "path": "/api/qc/results", "controller": "Qc\\ResultsController::create" },
|
||||
{ "method": "PATCH", "path": "/api/qc/results/(:num)", "controller": "Qc\\ResultsController::update/$1" },
|
||||
{ "method": "DELETE", "path": "/api/qc/results/(:num)", "controller": "Qc\\ResultsController::delete/$1" },
|
||||
{ "method": "GET", "path": "/api/qc/result-comments", "controller": "Qc\\ResultCommentsController::index" },
|
||||
{ "method": "GET", "path": "/api/qc/result-comments/(:num)", "controller": "Qc\\ResultCommentsController::show/$1" },
|
||||
{ "method": "POST", "path": "/api/qc/result-comments", "controller": "Qc\\ResultCommentsController::create" },
|
||||
{ "method": "PATCH", "path": "/api/qc/result-comments/(:num)", "controller": "Qc\\ResultCommentsController::update/$1" },
|
||||
{ "method": "DELETE", "path": "/api/qc/result-comments/(:num)", "controller": "Qc\\ResultCommentsController::delete/$1" }
|
||||
],
|
||||
"routes": [
|
||||
{ "method": "GET", "path": "/", "controller": "PageController::dashboard" },
|
||||
{ "method": "GET", "path": "/master/dept", "controller": "PageController::masterDept" },
|
||||
{ "method": "GET", "path": "/master/test", "controller": "PageController::masterTest" },
|
||||
{ "method": "GET", "path": "/master/control", "controller": "PageController::masterControl" },
|
||||
{ "method": "GET", "path": "/entry", "controller": "PageController::entry" },
|
||||
{ "method": "GET", "path": "/entry/daily", "controller": "PageController::entryDaily" },
|
||||
{ "method": "GET", "path": "/report", "controller": "PageController::report" },
|
||||
{ "method": "GET", "path": "/report/view", "controller": "PageController::reportView" }
|
||||
]
|
||||
}
|
||||
BIN
public/images/favicon.png
Normal file
BIN
public/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
18
public/images/favicon.svg
Normal file
18
public/images/favicon.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#10B981" />
|
||||
<stop offset="100%" stop-color="#3B82F6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Main shape: A rounded vial shape -->
|
||||
<path d="M180 100 H332 V320 C332 362 298 396 256 396 C214 396 180 362 180 320 V100 Z" fill="url(#grad)" />
|
||||
<rect x="160" y="80" width="192" height="24" rx="12" fill="url(#grad)" />
|
||||
|
||||
<!-- Content: Bar chart and Checkmark -->
|
||||
<rect x="210" y="280" width="24" height="60" rx="12" fill="white" />
|
||||
<rect x="244" y="240" width="24" height="100" rx="12" fill="white" />
|
||||
<rect x="278" y="210" width="24" height="130" rx="12" fill="white" />
|
||||
|
||||
<path d="M200 200 L240 240 L340 140" stroke="white" stroke-width="32" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 956 B |
BIN
public/images/logo.png
Normal file
BIN
public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@ -1,88 +0,0 @@
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('appState', {
|
||||
loading: false,
|
||||
sidebarOpen: false
|
||||
});
|
||||
});
|
||||
|
||||
window.App = {
|
||||
loading: false,
|
||||
sidebarOpen: false,
|
||||
|
||||
init() {
|
||||
this.setupSidebar();
|
||||
this.setupKeyboardShortcuts();
|
||||
},
|
||||
|
||||
setupSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const backdrop = document.getElementById('sidebar-backdrop');
|
||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
this.sidebarOpen = !this.sidebarOpen;
|
||||
if (window.Alpine) {
|
||||
Alpine.store('appState').sidebarOpen = this.sidebarOpen;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (backdrop) {
|
||||
backdrop.addEventListener('click', () => {
|
||||
this.sidebarOpen = false;
|
||||
if (window.Alpine) {
|
||||
Alpine.store('appState').sidebarOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
const saveBtn = document.querySelector('[data-save-btn]');
|
||||
if (saveBtn && confirm('Save changes?')) {
|
||||
saveBtn.click();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
this.closeAllModals();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeAllModals() {
|
||||
document.querySelectorAll('[x-data]').forEach(el => {
|
||||
if (el._x_dataStack) {
|
||||
const data = el._x_dataStack[0];
|
||||
if (typeof data.open !== 'undefined') data.open = false;
|
||||
if (typeof data.showModal !== 'undefined') data.showModal = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showToast(message, type = 'success') {
|
||||
Toastify({
|
||||
text: message,
|
||||
duration: 3000,
|
||||
gravity: 'top',
|
||||
position: 'right',
|
||||
className: type === 'success' ? 'bg-green-500' : type === 'error' ? 'bg-red-500' : 'bg-blue-500',
|
||||
stopOnFocus: true
|
||||
}).showToast();
|
||||
},
|
||||
|
||||
confirmAction(message = 'Are you sure?') {
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
confirmSave(message = 'Save changes?') {
|
||||
return confirm(message);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
App.init();
|
||||
});
|
||||
@ -1,128 +0,0 @@
|
||||
window.ChartManager = {
|
||||
charts: {},
|
||||
|
||||
init(containerId, chartData, options = {}) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const ctx = container.getContext('2d');
|
||||
|
||||
if (this.charts[containerId]) {
|
||||
this.charts[containerId].destroy();
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.charts[containerId] = new Chart(ctx, {
|
||||
type: options.type || 'line',
|
||||
data: chartData,
|
||||
options: { ...defaultOptions, ...options }
|
||||
});
|
||||
|
||||
return this.charts[containerId];
|
||||
},
|
||||
|
||||
createTrendChart(containerId, labels, datasets) {
|
||||
return this.init(containerId, {
|
||||
labels,
|
||||
datasets: datasets.map((ds, i) => ({
|
||||
label: ds.label || `Control ${i + 1}`,
|
||||
data: ds.data,
|
||||
borderColor: ds.color || this.getColor(i),
|
||||
backgroundColor: this.getColor(i, 0.1),
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6
|
||||
}))
|
||||
}, {
|
||||
type: 'line',
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Monthly QC Trend'
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createComparisonChart(containerId, labels, datasets) {
|
||||
return this.init(containerId, {
|
||||
labels,
|
||||
datasets: datasets.map((ds, i) => ({
|
||||
label: ds.label || `Control ${i + 1}`,
|
||||
data: ds.data,
|
||||
backgroundColor: this.getColor(i, 0.6),
|
||||
borderColor: this.getColor(i),
|
||||
borderWidth: 1
|
||||
}))
|
||||
}, {
|
||||
type: 'bar',
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Control Comparison'
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createViolationChart(containerId, labels, datasets) {
|
||||
return this.init(containerId, {
|
||||
labels,
|
||||
datasets: datasets.map((ds, i) => ({
|
||||
label: ds.label || `Control ${i + 1}`,
|
||||
data: ds.data,
|
||||
backgroundColor: ds.data.map(v => v > 100 ? 'rgba(239, 68, 68, 0.6)' : 'rgba(34, 197, 94, 0.6)'),
|
||||
borderColor: ds.data.map(v => v > 100 ? 'rgb(239, 68, 68)' : 'rgb(34, 197, 94)'),
|
||||
borderWidth: 1
|
||||
}))
|
||||
}, {
|
||||
type: 'bar',
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'QC Violations (Values > 100)'
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getColor(index, alpha = 1) {
|
||||
const colors = [
|
||||
`rgba(59, 130, 246, ${alpha})`,
|
||||
`rgba(16, 185, 129, ${alpha})`,
|
||||
`rgba(245, 158, 11, ${alpha})`,
|
||||
`rgba(239, 68, 68, ${alpha})`,
|
||||
`rgba(139, 92, 246, ${alpha})`,
|
||||
`rgba(236, 72, 153, ${alpha})`
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
},
|
||||
|
||||
destroy(containerId) {
|
||||
if (this.charts[containerId]) {
|
||||
this.charts[containerId].destroy();
|
||||
delete this.charts[containerId];
|
||||
}
|
||||
},
|
||||
|
||||
destroyAll() {
|
||||
Object.keys(this.charts).forEach(id => this.destroy(id));
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chartsContainer = document.getElementById('charts-container');
|
||||
if (chartsContainer && typeof initReportCharts === 'function') {
|
||||
initReportCharts();
|
||||
}
|
||||
});
|
||||
@ -1,186 +0,0 @@
|
||||
window.TableEnhancer = {
|
||||
init(selector, options = {}) {
|
||||
const defaults = {
|
||||
perPage: 10,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
persistKey: null
|
||||
};
|
||||
const config = { ...defaults, ...options };
|
||||
document.querySelectorAll(selector).forEach(table => this.enhance(table, config));
|
||||
},
|
||||
|
||||
enhance(table, config) {
|
||||
const container = table.closest('.table-container') || table.parentElement;
|
||||
if (!container.querySelector('.table-search')) {
|
||||
this.addControls(container, table, config);
|
||||
}
|
||||
this.setupSearch(container, table, config);
|
||||
this.setupSort(container, table, config);
|
||||
this.setupPagination(container, table, config);
|
||||
},
|
||||
|
||||
addControls(container, table, config) {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'table-controls flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-t-lg';
|
||||
controls.innerHTML = `
|
||||
${config.searchable ? `
|
||||
<div class="relative">
|
||||
<input type="text" class="table-search pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 w-64" placeholder="Search...">
|
||||
<svg class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="table-info text-sm text-gray-500"></div>
|
||||
`;
|
||||
container.insertBefore(controls, table);
|
||||
|
||||
const pagination = document.createElement('div');
|
||||
pagination.className = 'table-pagination flex justify-center items-center space-x-2 mt-4';
|
||||
container.appendChild(pagination);
|
||||
},
|
||||
|
||||
setupSearch(container, table, config) {
|
||||
const searchInput = container.querySelector('.table-search');
|
||||
if (!searchInput) return;
|
||||
|
||||
const key = config.persistKey ? `table_search_${config.persistKey}` : null;
|
||||
if (key && localStorage.getItem(key)) {
|
||||
searchInput.value = localStorage.getItem(key);
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
if (key) localStorage.setItem(key, e.target.value);
|
||||
this.filterTable(table, e.target.value);
|
||||
this.updatePagination(container, table, 1, config);
|
||||
});
|
||||
},
|
||||
|
||||
setupSort(container, table, config) {
|
||||
if (!config.sortable) return;
|
||||
|
||||
const headers = table.querySelectorAll('th');
|
||||
headers.forEach((th, index) => {
|
||||
if (th.textContent.trim()) {
|
||||
th.classList.add('cursor-pointer', 'hover:bg-gray-100');
|
||||
th.dataset.sortCol = index;
|
||||
th.innerHTML += `
|
||||
<svg class="inline h-4 w-4 ml-1 text-gray-400 sort-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
container.querySelectorAll('th[data-sort-col]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const col = th.dataset.sortCol;
|
||||
const currentSort = th.dataset.sort || 'none';
|
||||
const newSort = currentSort === 'asc' ? 'desc' : 'asc';
|
||||
this.sortTable(table, col, newSort);
|
||||
th.dataset.sort = newSort;
|
||||
th.querySelector('.sort-icon').innerHTML = newSort === 'asc'
|
||||
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>'
|
||||
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>';
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupPagination(container, table, config) {
|
||||
this.updatePagination(container, table, 1, config);
|
||||
},
|
||||
|
||||
filterTable(table, query) {
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
const lowerQuery = query.toLowerCase();
|
||||
let visibleCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
const match = text.includes(lowerQuery);
|
||||
row.style.display = match ? '' : 'none';
|
||||
if (match) visibleCount++;
|
||||
});
|
||||
|
||||
const infoEl = container.querySelector('.table-info');
|
||||
if (infoEl) {
|
||||
infoEl.textContent = `${visibleCount} row${visibleCount !== 1 ? 's' : ''} found`;
|
||||
}
|
||||
},
|
||||
|
||||
sortTable(table, col, direction) {
|
||||
const tbody = table.querySelector('tbody');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aVal = a.querySelectorAll('td')[col]?.textContent.trim() || '';
|
||||
const bVal = b.querySelectorAll('td')[col]?.textContent.trim() || '';
|
||||
const aNum = parseFloat(aVal);
|
||||
const bNum = parseFloat(bVal);
|
||||
|
||||
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
return direction === 'asc' ? aNum - bNum : bNum - aNum;
|
||||
}
|
||||
return direction === 'asc'
|
||||
? aVal.localeCompare(bVal)
|
||||
: bVal.localeCompare(aVal);
|
||||
});
|
||||
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
},
|
||||
|
||||
updatePagination(container, table, page, config) {
|
||||
const rows = Array.from(table.querySelectorAll('tbody tr:not([style*=\"display: none\"])'));
|
||||
const totalPages = Math.ceil(rows.length / config.perPage);
|
||||
const pagination = container.querySelector('.table-pagination');
|
||||
if (!pagination) return;
|
||||
|
||||
const start = (page - 1) * config.perPage;
|
||||
const end = start + config.perPage;
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
row.style.display = index >= start && index < end ? '' : 'none';
|
||||
});
|
||||
|
||||
if (totalPages <= 1) {
|
||||
pagination.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<button class="px-3 py-1 border rounded hover:bg-gray-100" ${page === 1 ? 'disabled' : ''} onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${page - 1})">Prev</button>
|
||||
`;
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) {
|
||||
html += `<button class="px-3 py-1 border rounded ${i === page ? 'bg-primary-600 text-white' : 'hover:bg-gray-100'}" onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${i})">${i}</button>`;
|
||||
} else if (i === page - 2 || i === page + 2) {
|
||||
html += `<span class="px-2">...</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<button class="px-3 py-1 border rounded hover:bg-gray-100" ${page === totalPages ? 'disabled' : ''} onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${page + 1})">Next</button>
|
||||
`;
|
||||
|
||||
pagination.innerHTML = html;
|
||||
},
|
||||
|
||||
goToPage(tableId, page) {
|
||||
const table = tableId ? document.getElementById(tableId) : document.querySelector('table');
|
||||
if (!table) return;
|
||||
const container = table.closest('.table-container') || table.parentElement;
|
||||
const config = { perPage: 10 };
|
||||
this.updatePagination(container, table, page, config);
|
||||
},
|
||||
|
||||
goToPageBySelector(selector, page) {
|
||||
const container = document.querySelector(selector);
|
||||
if (!container) return;
|
||||
const table = container.querySelector('table');
|
||||
if (!table) return;
|
||||
const config = { perPage: 10 };
|
||||
this.updatePagination(container, table, page, config);
|
||||
}
|
||||
};
|
||||
450
public/template.html
Normal file
450
public/template.html
Normal file
@ -0,0 +1,450 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="autumn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TinyQC - QC Management System</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
</head>
|
||||
<body class="bg-base-200 text-base-content" x-data="appData()">
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle" x-ref="sidebarDrawer">
|
||||
|
||||
<div class="drawer-content flex flex-col min-h-screen">
|
||||
<nav class="navbar bg-base-200/80 backdrop-blur-md border-b border-base-300 sticky top-0 z-30 w-full shadow-sm">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="sidebar-drawer" class="btn btn-square btn-ghost text-primary">
|
||||
<i class="fa-solid fa-bars text-xl"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-1 px-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<i class="fa-solid fa-flask text-white text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-base-content tracking-tight">tinyqc</h1>
|
||||
<p class="text-xs opacity-70">QC Management System</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<button class="btn btn-ghost rounded-full mr-2" @click="toggleTheme()">
|
||||
<i class="fa-solid fa-sun text-warning" x-show="currentTheme === themeConfig.light"></i>
|
||||
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
|
||||
</button>
|
||||
<div class="dropdown dropdown-end" x-data="{ dropdownOpen: false }">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder" @click="dropdownOpen = !dropdownOpen">
|
||||
<div class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
|
||||
<span>DR</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300" x-show="dropdownOpen" @click.outside="dropdownOpen = false" x-transition>
|
||||
<li class="menu-title px-4 py-2">
|
||||
<span class="text-base-content font-bold">Dr. Sarah Mitchell</span>
|
||||
<span class="text-xs text-primary font-medium">Lab Director</span>
|
||||
</li>
|
||||
<div class="divider my-0 h-px opacity-10"></div>
|
||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-user text-primary"></i> Profile</a></li>
|
||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-gear text-primary"></i> Settings</a></li>
|
||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-question-circle text-primary"></i> Help</a></li>
|
||||
<div class="divider my-0 h-px opacity-10"></div>
|
||||
<li><a class="text-error hover:bg-error/10"><i class="fa-solid fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="flex-1 p-6 overflow-auto">
|
||||
<div class="mx-auto">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight text-base-content">Sample Management</h2>
|
||||
<p class="text-sm mt-1 opacity-70">Manage laboratory samples, track status, and generate reports</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary gap-2 shadow-lg shadow-primary/20" @click="openModal()">
|
||||
<i class="fa-solid fa-plus"></i> Add Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-base-300 shadow-xl overflow-hidden bg-base-100">
|
||||
<div class="p-4 border-b border-base-300 flex flex-col md:flex-row md:items-center gap-4">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
||||
<input type="text" placeholder="Search by sample ID, patient name, or test type..." class="w-full pl-10 pr-4 py-2.5 text-sm rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all bg-base-200 border border-base-300 text-base-content placeholder:opacity-50">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-bordered select-sm focus:outline-none focus:ring-2 focus:ring-primary/50 bg-base-200 border-base-300 text-base-content">
|
||||
<option selected>All Status</option>
|
||||
<option>Pending</option>
|
||||
<option>Processing</option>
|
||||
<option>Completed</option>
|
||||
<option>Cancelled</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-ghost opacity-70 hover:opacity-100">
|
||||
<i class="fa-solid fa-filter"></i> Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="uppercase tracking-wider font-semibold bg-base-200 text-base-content/70 text-xs">
|
||||
<tr>
|
||||
<th class="py-4 px-5">Sample ID</th>
|
||||
<th class="py-4 px-5">Patient Name</th>
|
||||
<th class="py-4 px-5">Test Type</th>
|
||||
<th class="py-4 px-5">Collection Date</th>
|
||||
<th class="py-4 px-5">Status</th>
|
||||
<th class="py-4 px-5 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-base-300">
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-4 px-5">
|
||||
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-001</span>
|
||||
</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
||||
<span class="text-xs" x-text="'JD'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-medium text-base-content">John Doe</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 opacity-80">Complete Blood Count</td>
|
||||
<td class="py-4 px-5 opacity-60">Jan 15, 2024</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="badge badge-warning gap-1">
|
||||
<i class="fa-solid fa-clock text-xs"></i> Pending
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-4 px-5">
|
||||
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-002</span>
|
||||
</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
||||
<span class="text-xs">AS</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-medium text-base-content">Alice Smith</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 opacity-80">Lipid Panel</td>
|
||||
<td class="py-4 px-5 opacity-60">Jan 15, 2024</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="badge badge-info gap-1">
|
||||
<i class="fa-solid fa-flask text-xs"></i> Processing
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-4 px-5">
|
||||
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-003</span>
|
||||
</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
||||
<span class="text-xs">BJ</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-medium text-base-content">Bob Johnson</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 opacity-80">Liver Function Test</td>
|
||||
<td class="py-4 px-5 opacity-60">Jan 14, 2024</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="badge badge-success gap-1">
|
||||
<i class="fa-solid fa-check text-xs"></i> Completed
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-4 px-5">
|
||||
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-004</span>
|
||||
</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
||||
<span class="text-xs">EW</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-medium text-base-content">Emily Watson</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 opacity-80">Thyroid Panel</td>
|
||||
<td class="py-4 px-5 opacity-60">Jan 14, 2024</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="badge badge-success gap-1">
|
||||
<i class="fa-solid fa-check text-xs"></i> Completed
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-base-200 transition-colors">
|
||||
<td class="py-4 px-5">
|
||||
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-005</span>
|
||||
</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
||||
<span class="text-xs">MC</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-medium text-base-content">Michael Chen</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 opacity-80">Urinalysis</td>
|
||||
<td class="py-4 px-5 opacity-60">Jan 13, 2024</td>
|
||||
<td class="py-4 px-5">
|
||||
<div class="badge badge-error gap-1">
|
||||
<i class="fa-solid fa-times text-xs"></i> Cancelled
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-5 text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
||||
<i class="fa-solid fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-base-300 flex items-center justify-between">
|
||||
<span class="text-sm opacity-60">Showing 1 to 5 of 127 entries</span>
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm bg-base-200 border-base-300">Previous</button>
|
||||
<button class="join-item btn btn-sm btn-primary">1</button>
|
||||
<button class="join-item btn btn-sm bg-base-200 border-base-300">2</button>
|
||||
<button class="join-item btn btn-sm bg-base-200 border-base-300">3</button>
|
||||
<button class="join-item btn btn-sm bg-base-200 border-base-300">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="text-center text-sm opacity-50 py-4 mt-auto">
|
||||
© 2026 - 5Panda
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-40">
|
||||
<label for="sidebar-drawer" class="drawer-overlay"></label>
|
||||
<aside class="bg-base-300 border-r border-base-300 w-64 min-h-full flex flex-col">
|
||||
<ul class="menu p-4 text-base-content flex-1 w-full">
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg bg-primary/10 text-primary font-medium h-full">
|
||||
<i class="fa-solid fa-chart-line w-5"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
||||
<i class="fa-solid fa-users w-5 text-primary"></i>
|
||||
Patients
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
||||
<i class="fa-solid fa-vial w-5 text-primary"></i>
|
||||
Samples
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
||||
<i class="fa-solid fa-microscope w-5 text-primary"></i>
|
||||
Results
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
||||
<i class="fa-solid fa-file-medical-alt w-5 text-primary"></i>
|
||||
Reports
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="mt-6 mb-2 min-h-0">
|
||||
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">System</p>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
||||
<i class="fa-solid fa-cog w-5 text-primary"></i>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-1 min-h-0">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
||||
<i class="fa-solid fa-users-cog w-5 text-primary"></i>
|
||||
Users
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="p-4 border-t border-base-300">
|
||||
<div class="bg-base-100/50 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs opacity-60">Storage</span>
|
||||
<span class="text-xs text-primary font-bold">68%</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full h-2" value="68" max="100"></progress>
|
||||
<p class="text-xs opacity-50 mt-2">6.8 GB of 10 GB used</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog class="modal modal-bottom sm:modal-middle" :class="{ 'modal-open': showModal }">
|
||||
<div class="modal-box border border-base-300 shadow-2xl bg-base-100">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2 text-base-content">
|
||||
<i class="fa-solid fa-vial text-primary"></i>
|
||||
Add New Sample
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Patient Name</span>
|
||||
</label>
|
||||
<input type="text" placeholder="Enter patient name" class="input input-bordered 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">
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Test Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 bg-base-200 border-base-300 text-base-content">
|
||||
<option selected disabled>Select test type</option>
|
||||
<option>Complete Blood Count (CBC)</option>
|
||||
<option>Lipid Panel</option>
|
||||
<option>Liver Function Test</option>
|
||||
<option>Kidney Function Test</option>
|
||||
<option>Thyroid Panel</option>
|
||||
<option>Urinalysis</option>
|
||||
<option>Blood Glucose</option>
|
||||
<option>Electrolytes Panel</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Collection Date</span>
|
||||
</label>
|
||||
<input type="date" class="input input-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 bg-base-200 border-base-300 text-base-content">
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-base-content opacity-80">Notes</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full h-24 focus:outline-none focus:ring-2 focus:ring-primary/50 placeholder:opacity-50 resize-none bg-base-200 border-base-300 text-base-content" placeholder="Additional notes or special instructions..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button class="btn btn-ghost opacity-70" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary gap-2 shadow-lg shadow-primary/20 font-medium">
|
||||
<i class="fa-solid fa-save"></i> Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop bg-black/60" @click="closeModal()"></form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('appData', () => ({
|
||||
showModal: false,
|
||||
themeConfig: {
|
||||
light: 'autumn',
|
||||
dark: 'dracula'
|
||||
},
|
||||
get currentTheme() {
|
||||
return localStorage.getItem('theme') || this.themeConfig.light;
|
||||
},
|
||||
set currentTheme(value) {
|
||||
localStorage.setItem('theme', value);
|
||||
document.documentElement.setAttribute('data-theme', value);
|
||||
},
|
||||
get isDark() {
|
||||
return this.currentTheme === this.themeConfig.dark;
|
||||
},
|
||||
init() {
|
||||
document.documentElement.setAttribute('data-theme', this.currentTheme);
|
||||
},
|
||||
toggleSidebar() {
|
||||
this.$refs.sidebarDrawer.checked = !this.$refs.sidebarDrawer.checked;
|
||||
},
|
||||
toggleTheme() {
|
||||
this.currentTheme = this.isDark ? this.themeConfig.light : this.themeConfig.dark;
|
||||
},
|
||||
openModal() {
|
||||
this.showModal = true;
|
||||
},
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user