feat: Add user authentication system and secure all routes
- Implement AuthController with login/logout functionality - Create UsersModel with bcrypt password hashing - Add AuthFilter to protect all application routes - Create login page with error handling - Add users database migration with email/username fields - Rename ResultComments to TestComments for consistency - Update all routes to require authentication filter - Enhance EntryApiController with comment deletion and better error handling - Update seeder to include demo users and improved test data - Fix BaseController to handle auth sessions properly - Update entry views (daily/monthly) with new API endpoints - Update layout with logout button and user info display - Refactor control test index view for better organization
This commit is contained in:
parent
ef6be6522e
commit
87ff4c8d85
@ -1,26 +1,31 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use CodeIgniter\Router\RouteCollection;
|
use CodeIgniter\Router\RouteCollection;
|
||||||
|
use App\Filters\AuthFilter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var RouteCollection $routes
|
* @var RouteCollection $routes
|
||||||
*/
|
*/
|
||||||
$routes->get('/', 'PageController::dashboard');
|
$routes->get('login', 'Auth\AuthController::login');
|
||||||
|
$routes->post('login', 'Auth\AuthController::processLogin');
|
||||||
|
$routes->get('logout', 'Auth\AuthController::logout');
|
||||||
|
|
||||||
$routes->get('/master/dept', 'PageController::masterDept');
|
$routes->get('/', 'PageController::dashboard', ['filter' => AuthFilter::class]);
|
||||||
$routes->get('/master/test', 'PageController::masterTest');
|
|
||||||
$routes->get('/master/control', 'PageController::masterControl');
|
|
||||||
$routes->get('/master/control-tests', 'PageController::controlTests');
|
|
||||||
$routes->get('/dept', 'PageController::dept');
|
|
||||||
$routes->get('/test', 'PageController::test');
|
|
||||||
$routes->get('/control', 'PageController::control');
|
|
||||||
$routes->get('/entry', 'PageController::entry');
|
|
||||||
$routes->get('/entry/daily', 'PageController::entryDaily');
|
|
||||||
$routes->get('/entry/monthly', 'PageController::entryMonthly');
|
|
||||||
$routes->get('/report', 'PageController::report');
|
|
||||||
$routes->get('/report/merged', 'PageController::reportMerged');
|
|
||||||
|
|
||||||
$routes->group('api', function ($routes) {
|
$routes->get('/master/dept', 'PageController::masterDept', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/master/test', 'PageController::masterTest', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/master/control', 'PageController::masterControl', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/master/control-tests', 'PageController::controlTests', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/dept', 'PageController::dept', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/test', 'PageController::test', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/control', 'PageController::control', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/entry', 'PageController::entry', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/entry/daily', 'PageController::entryDaily', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/entry/monthly', 'PageController::entryMonthly', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/report', 'PageController::report', ['filter' => AuthFilter::class]);
|
||||||
|
$routes->get('/report/merged', 'PageController::reportMerged', ['filter' => AuthFilter::class]);
|
||||||
|
|
||||||
|
$routes->group('api', ['filter' => AuthFilter::class], function ($routes) {
|
||||||
$routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent');
|
$routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent');
|
||||||
$routes->get('dept', 'Api\DeptApiController::index');
|
$routes->get('dept', 'Api\DeptApiController::index');
|
||||||
$routes->get('dept/(:num)', 'Api\DeptApiController::show/$1');
|
$routes->get('dept/(:num)', 'Api\DeptApiController::show/$1');
|
||||||
@ -50,7 +55,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->post('entry/comment', 'Api\EntryApiController::saveComment');
|
$routes->post('entry/comment', 'Api\EntryApiController::saveComment');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('api/master', function ($routes) {
|
$routes->group('api/master', ['filter' => AuthFilter::class], function ($routes) {
|
||||||
$routes->get('depts', 'Master\MasterDeptsController::index');
|
$routes->get('depts', 'Master\MasterDeptsController::index');
|
||||||
$routes->get('depts/(:num)', 'Master\MasterDeptsController::show/$1');
|
$routes->get('depts/(:num)', 'Master\MasterDeptsController::show/$1');
|
||||||
$routes->post('depts', 'Master\MasterDeptsController::create');
|
$routes->post('depts', 'Master\MasterDeptsController::create');
|
||||||
@ -70,7 +75,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->delete('tests/(:num)', 'Master\MasterTestsController::delete/$1');
|
$routes->delete('tests/(:num)', 'Master\MasterTestsController::delete/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('api/qc', function ($routes) {
|
$routes->group('api/qc', ['filter' => AuthFilter::class], function ($routes) {
|
||||||
$routes->get('control-tests', 'Qc\ControlTestsController::index');
|
$routes->get('control-tests', 'Qc\ControlTestsController::index');
|
||||||
$routes->get('control-tests/(:num)', 'Qc\ControlTestsController::show/$1');
|
$routes->get('control-tests/(:num)', 'Qc\ControlTestsController::show/$1');
|
||||||
$routes->post('control-tests', 'Qc\ControlTestsController::create');
|
$routes->post('control-tests', 'Qc\ControlTestsController::create');
|
||||||
@ -83,9 +88,9 @@ $routes->group('api/qc', function ($routes) {
|
|||||||
$routes->patch('results/(:num)', 'Qc\ResultsController::update/$1');
|
$routes->patch('results/(:num)', 'Qc\ResultsController::update/$1');
|
||||||
$routes->delete('results/(:num)', 'Qc\ResultsController::delete/$1');
|
$routes->delete('results/(:num)', 'Qc\ResultsController::delete/$1');
|
||||||
|
|
||||||
$routes->get('result-comments', 'Qc\ResultCommentsController::index');
|
$routes->get('test-comments', 'Qc\TestCommentsController::index');
|
||||||
$routes->get('result-comments/(:num)', 'Qc\ResultCommentsController::show/$1');
|
$routes->get('test-comments/(:num)', 'Qc\TestCommentsController::show/$1');
|
||||||
$routes->post('result-comments', 'Qc\ResultCommentsController::create');
|
$routes->post('test-comments', 'Qc\TestCommentsController::create');
|
||||||
$routes->patch('result-comments/(:num)', 'Qc\ResultCommentsController::update/$1');
|
$routes->patch('test-comments/(:num)', 'Qc\TestCommentsController::update/$1');
|
||||||
$routes->delete('result-comments/(:num)', 'Qc\ResultCommentsController::delete/$1');
|
$routes->delete('test-comments/(:num)', 'Qc\TestCommentsController::delete/$1');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use App\Models\Master\MasterControlsModel;
|
|||||||
use App\Models\Master\MasterTestsModel;
|
use App\Models\Master\MasterTestsModel;
|
||||||
use App\Models\Qc\ResultsModel;
|
use App\Models\Qc\ResultsModel;
|
||||||
use App\Models\Qc\ControlTestsModel;
|
use App\Models\Qc\ControlTestsModel;
|
||||||
use App\Models\Qc\ResultCommentsModel;
|
use App\Models\Qc\TestCommentsModel;
|
||||||
|
|
||||||
class EntryApiController extends BaseController
|
class EntryApiController extends BaseController
|
||||||
{
|
{
|
||||||
@ -26,7 +26,7 @@ class EntryApiController extends BaseController
|
|||||||
$this->testModel = new MasterTestsModel();
|
$this->testModel = new MasterTestsModel();
|
||||||
$this->resultModel = new ResultsModel();
|
$this->resultModel = new ResultsModel();
|
||||||
$this->controlTestModel = new ControlTestsModel();
|
$this->controlTestModel = new ControlTestsModel();
|
||||||
$this->commentModel = new ResultCommentsModel();
|
$this->commentModel = new TestCommentsModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,10 +134,29 @@ class EntryApiController extends BaseController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge tests with existing values
|
// Get all test IDs for this control
|
||||||
|
$testIds = array_column($tests, 'testId');
|
||||||
|
|
||||||
|
// Fetch comments separately from test_comments table (comments are per test + date)
|
||||||
|
$commentsData = [];
|
||||||
|
if (!empty($testIds)) {
|
||||||
|
$comments = $this->commentModel->getByTestAndDateRange($testIds, $date, $date);
|
||||||
|
foreach ($comments as $c) {
|
||||||
|
$commentsData[$c['testId']] = $c['commentText'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge tests with existing values and comments
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($tests as $t) {
|
foreach ($tests as $t) {
|
||||||
$existing = $resultsByTest[$t['testId']] ?? null;
|
$existing = $resultsByTest[$t['testId']] ?? null;
|
||||||
|
|
||||||
|
// Get comment from either the test_comments table or existing result
|
||||||
|
$comment = $commentsData[$t['testId']] ?? null;
|
||||||
|
if ($existing && isset($existing['resComment']) && $existing['resComment']) {
|
||||||
|
$comment = $existing['resComment']; // Existing result's comment takes precedence
|
||||||
|
}
|
||||||
|
|
||||||
$data[] = [
|
$data[] = [
|
||||||
'controlTestId' => $t['id'],
|
'controlTestId' => $t['id'],
|
||||||
'controlId' => $t['controlId'],
|
'controlId' => $t['controlId'],
|
||||||
@ -146,7 +165,8 @@ class EntryApiController extends BaseController
|
|||||||
'testUnit' => $t['testUnit'],
|
'testUnit' => $t['testUnit'],
|
||||||
'mean' => $t['mean'],
|
'mean' => $t['mean'],
|
||||||
'sd' => $t['sd'],
|
'sd' => $t['sd'],
|
||||||
'existingResult' => $existing
|
'existingResult' => $existing,
|
||||||
|
'comment' => $comment
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,8 +264,8 @@ class EntryApiController extends BaseController
|
|||||||
// Get existing results for this month
|
// Get existing results for this month
|
||||||
$results = $this->resultModel->getByMonth((int) $testId, $month);
|
$results = $this->resultModel->getByMonth((int) $testId, $month);
|
||||||
|
|
||||||
// Get comments for this test (via results)
|
// Get comments for this test (test_id + date based)
|
||||||
$comments = $this->commentModel->getByTest((int) $testId);
|
$comments = $this->commentModel->getByTestAndMonth((int) $testId, $month);
|
||||||
|
|
||||||
// Map results by control_id and day
|
// Map results by control_id and day
|
||||||
$resultsByControl = [];
|
$resultsByControl = [];
|
||||||
@ -253,15 +273,16 @@ class EntryApiController extends BaseController
|
|||||||
$day = (int) date('j', strtotime($r['resDate']));
|
$day = (int) date('j', strtotime($r['resDate']));
|
||||||
$resultsByControl[$r['controlId']][$day] = [
|
$resultsByControl[$r['controlId']][$day] = [
|
||||||
'resultId' => $r['id'],
|
'resultId' => $r['id'],
|
||||||
'resValue' => $r['resValue']
|
'resValue' => $r['resValue'],
|
||||||
|
'resDate' => $r['resDate']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map comments by result_id
|
// Map comments by date (comments are now per test + date)
|
||||||
$commentsByResultId = [];
|
$commentsByDate = [];
|
||||||
foreach ($comments as $c) {
|
foreach ($comments as $c) {
|
||||||
$commentsByResultId[$c['resultId']] = [
|
$commentsByDate[$c['commentDate']] = [
|
||||||
'commentId' => $c['resultCommentId'],
|
'commentId' => $c['testCommentId'],
|
||||||
'commentText' => $c['commentText']
|
'commentText' => $c['commentText']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -274,9 +295,10 @@ class EntryApiController extends BaseController
|
|||||||
|
|
||||||
foreach ($resultsByDay as $day => $val) {
|
foreach ($resultsByDay as $day => $val) {
|
||||||
$resultWithComment = $val;
|
$resultWithComment = $val;
|
||||||
// Add comment if exists for this result
|
// Add comment if exists for this date (comments are per test + date)
|
||||||
if (isset($commentsByResultId[$val['resultId']])) {
|
$resultDate = date('Y-m-d', strtotime($val['resDate']));
|
||||||
$resultWithComment['resComment'] = $commentsByResultId[$val['resultId']]['commentText'];
|
if (isset($commentsByDate[$resultDate])) {
|
||||||
|
$resultWithComment['resComment'] = $commentsByDate[$resultDate]['commentText'];
|
||||||
} else {
|
} else {
|
||||||
$resultWithComment['resComment'] = null;
|
$resultWithComment['resComment'] = null;
|
||||||
}
|
}
|
||||||
@ -406,7 +428,7 @@ class EntryApiController extends BaseController
|
|||||||
try {
|
try {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
$required = ['resultId', 'comment'];
|
$required = ['testId', 'date', 'comment'];
|
||||||
foreach ($required as $field) {
|
foreach ($required as $field) {
|
||||||
if (!isset($input[$field])) {
|
if (!isset($input[$field])) {
|
||||||
return $this->failValidationErrors([$field => 'Required']);
|
return $this->failValidationErrors([$field => 'Required']);
|
||||||
@ -414,7 +436,8 @@ class EntryApiController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$commentData = [
|
$commentData = [
|
||||||
'result_id' => $input['resultId'],
|
'test_id' => $input['testId'],
|
||||||
|
'comment_date' => $input['date'],
|
||||||
'comment_text' => trim($input['comment'])
|
'comment_text' => trim($input['comment'])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use App\Models\Master\MasterControlsModel;
|
|||||||
use App\Models\Master\MasterTestsModel;
|
use App\Models\Master\MasterTestsModel;
|
||||||
use App\Models\Qc\ControlTestsModel;
|
use App\Models\Qc\ControlTestsModel;
|
||||||
use App\Models\Qc\ResultsModel;
|
use App\Models\Qc\ResultsModel;
|
||||||
use App\Models\Qc\ResultCommentsModel;
|
use App\Models\Qc\TestCommentsModel;
|
||||||
|
|
||||||
class ReportApiController extends BaseController
|
class ReportApiController extends BaseController
|
||||||
{
|
{
|
||||||
@ -26,7 +26,7 @@ class ReportApiController extends BaseController
|
|||||||
$this->dictTestModel = new MasterTestsModel();
|
$this->dictTestModel = new MasterTestsModel();
|
||||||
$this->controlTestModel = new ControlTestsModel();
|
$this->controlTestModel = new ControlTestsModel();
|
||||||
$this->resultModel = new ResultsModel();
|
$this->resultModel = new ResultsModel();
|
||||||
$this->commentModel = new ResultCommentsModel();
|
$this->commentModel = new TestCommentsModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getReport()
|
public function getReport()
|
||||||
@ -47,7 +47,13 @@ class ReportApiController extends BaseController
|
|||||||
|
|
||||||
$controlTest = $this->controlTestModel->getByControlAndTest($control['controlId'], $test);
|
$controlTest = $this->controlTestModel->getByControlAndTest($control['controlId'], $test);
|
||||||
$results = $this->resultModel->getByControlAndMonth($control['controlId'], $test, $dates);
|
$results = $this->resultModel->getByControlAndMonth($control['controlId'], $test, $dates);
|
||||||
$comment = $this->commentModel->getByControlTestMonth($control['controlId'], $test, $dates);
|
// Get all comments for this test in the month (comments are now per test + date)
|
||||||
|
$allComments = $this->commentModel->getByTestAndMonth($test, $dates);
|
||||||
|
// Index comments by date for easy lookup
|
||||||
|
$commentsByDate = [];
|
||||||
|
foreach ($allComments as $c) {
|
||||||
|
$commentsByDate[$c['commentDate']] = $c['commentText'];
|
||||||
|
}
|
||||||
$testInfo = $this->dictTestModel->find($test);
|
$testInfo = $this->dictTestModel->find($test);
|
||||||
|
|
||||||
$outOfRangeCount = 0;
|
$outOfRangeCount = 0;
|
||||||
@ -97,7 +103,7 @@ class ReportApiController extends BaseController
|
|||||||
'results' => $processedResults,
|
'results' => $processedResults,
|
||||||
'values' => $values,
|
'values' => $values,
|
||||||
'test' => $testInfo,
|
'test' => $testInfo,
|
||||||
'comment' => $comment,
|
'comments' => $commentsByDate,
|
||||||
'outOfRange' => $outOfRangeCount
|
'outOfRange' => $outOfRangeCount
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
99
app/Controllers/Auth/AuthController.php
Normal file
99
app/Controllers/Auth/AuthController.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Auth;
|
||||||
|
|
||||||
|
use CodeIgniter\API\ResponseTrait;
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\Auth\UsersModel;
|
||||||
|
|
||||||
|
class AuthController extends BaseController
|
||||||
|
{
|
||||||
|
use ResponseTrait;
|
||||||
|
|
||||||
|
protected $model;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->model = new UsersModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
if ($this->session->get('isLoggedIn')) {
|
||||||
|
return redirect()->to('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processLogin()
|
||||||
|
{
|
||||||
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Invalid request'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $input['username'] ?? '';
|
||||||
|
$password = $input['password'] ?? '';
|
||||||
|
$remember = $input['remember'] ?? false;
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Username and password are required'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->model->findByUsername($username);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Invalid username or password'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password_verify($password, $user['password'])) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Invalid username or password'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->session->set([
|
||||||
|
'isLoggedIn' => true,
|
||||||
|
'userId' => $user['userId'],
|
||||||
|
'username' => $user['username']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($remember) {
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$this->model->setRememberToken($user['userId'], $token);
|
||||||
|
set_cookie('remember_token', $token, 60 * 60 * 24 * 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Login successful',
|
||||||
|
'redirect' => base_url('/')
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
$userId = $this->session->get('userId');
|
||||||
|
|
||||||
|
if ($userId) {
|
||||||
|
$this->model->setRememberToken($userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_cookie('remember_token');
|
||||||
|
$this->session->destroy();
|
||||||
|
|
||||||
|
return redirect()->to('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,11 +13,11 @@ abstract class BaseController extends Controller
|
|||||||
use ResponseTrait;
|
use ResponseTrait;
|
||||||
|
|
||||||
protected $session;
|
protected $session;
|
||||||
|
protected $helpers = ['form', 'url', 'cookie', 'json', 'stringcase'];
|
||||||
|
|
||||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
parent::initController($request, $response, $logger);
|
parent::initController($request, $response, $logger);
|
||||||
$this->session = \Config\Services::session();
|
$this->session = \Config\Services::session();
|
||||||
$this->helpers = ['form', 'url', 'json', 'stringcase'];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,16 +3,16 @@ namespace App\Controllers\Qc;
|
|||||||
|
|
||||||
use CodeIgniter\API\ResponseTrait;
|
use CodeIgniter\API\ResponseTrait;
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
use App\Models\Qc\ResultCommentsModel;
|
use App\Models\Qc\TestCommentsModel;
|
||||||
|
|
||||||
class ResultCommentsController extends BaseController {
|
class TestCommentsController extends BaseController {
|
||||||
use ResponseTrait;
|
use ResponseTrait;
|
||||||
|
|
||||||
protected $model;
|
protected $model;
|
||||||
protected $rules;
|
protected $rules;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->model = new ResultCommentsModel();
|
$this->model = new TestCommentsModel();
|
||||||
$this->rules = [];
|
$this->rules = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,23 +88,25 @@ class QualityControlSystem extends Migration
|
|||||||
$this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE');
|
$this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||||
$this->forge->createTable('results');
|
$this->forge->createTable('results');
|
||||||
|
|
||||||
// result_comments
|
// test_comments
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'test_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'result_id' => ['type' => 'INT', 'unsigned' => true],
|
'test_id' => ['type' => 'INT', 'unsigned' => true],
|
||||||
|
'comment_date' => ['type' => 'DATE', 'null' => true],
|
||||||
'comment_text' => ['type' => 'TEXT', 'null' => true],
|
'comment_text' => ['type' => 'TEXT', 'null' => true],
|
||||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('result_comment_id', true);
|
$this->forge->addKey('test_comment_id', true);
|
||||||
$this->forge->addForeignKey('result_id', 'results', 'result_id', 'CASCADE', 'CASCADE');
|
$this->forge->addUniqueKey(['test_id', 'comment_date']);
|
||||||
$this->forge->createTable('result_comments');
|
$this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'CASCADE', 'CASCADE');
|
||||||
|
$this->forge->createTable('test_comments');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down()
|
public function down()
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('result_comments', true);
|
$this->forge->dropTable('test_comments', true);
|
||||||
$this->forge->dropTable('results', true);
|
$this->forge->dropTable('results', true);
|
||||||
$this->forge->dropTable('control_tests', true);
|
$this->forge->dropTable('control_tests', true);
|
||||||
$this->forge->dropTable('master_tests', true);
|
$this->forge->dropTable('master_tests', true);
|
||||||
|
|||||||
33
app/Database/Migrations/2026-02-09-000001_Users.php
Normal file
33
app/Database/Migrations/2026-02-09-000001_Users.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class Users extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$this->forge->addField([
|
||||||
|
'user_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
|
'username' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
|
||||||
|
'password' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
|
||||||
|
'remember_token' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
|
||||||
|
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
]);
|
||||||
|
$this->forge->addKey('user_id', true);
|
||||||
|
$this->forge->addUniqueKey('username');
|
||||||
|
$this->forge->createTable('master_users');
|
||||||
|
|
||||||
|
// Insert default admin user
|
||||||
|
$password = password_hash('admin123', PASSWORD_DEFAULT);
|
||||||
|
$this->db->query("INSERT INTO master_users (username, password, created_at, updated_at) VALUES ('admin', '{$password}', NOW(), NOW())");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$this->forge->dropTable('master_users', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@ class CmodQcSeeder extends Seeder
|
|||||||
$this->seedTests();
|
$this->seedTests();
|
||||||
$this->seedControlTests();
|
$this->seedControlTests();
|
||||||
$this->seedResults();
|
$this->seedResults();
|
||||||
$this->seedResultComments();
|
$this->seedTestComments();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function seedDepts()
|
protected function seedDepts()
|
||||||
@ -36,10 +36,14 @@ class CmodQcSeeder extends Seeder
|
|||||||
['dept_id' => 2, 'control_name' => 'QC Low Hema', 'lot' => 'QC2024004', 'producer' => 'Streck', 'exp_date' => '2025-11-30'],
|
['dept_id' => 2, 'control_name' => 'QC Low Hema', 'lot' => 'QC2024004', 'producer' => 'Streck', 'exp_date' => '2025-11-30'],
|
||||||
['dept_id' => 3, 'control_name' => 'QC Normal Immuno', 'lot' => 'QC2024005', 'producer' => 'Roche', 'exp_date' => '2025-10-31'],
|
['dept_id' => 3, 'control_name' => 'QC Normal Immuno', 'lot' => 'QC2024005', 'producer' => 'Roche', 'exp_date' => '2025-10-31'],
|
||||||
['dept_id' => 4, 'control_name' => 'QC Normal Urine', 'lot' => 'QC2024006', 'producer' => 'Siemens', 'exp_date' => '2025-09-30'],
|
['dept_id' => 4, 'control_name' => 'QC Normal Urine', 'lot' => 'QC2024006', 'producer' => 'Siemens', 'exp_date' => '2025-09-30'],
|
||||||
// New controls for January 2026
|
// January 2026 controls
|
||||||
['dept_id' => 1, 'control_name' => 'Trulab N', 'lot' => 'TN2026001', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'],
|
['dept_id' => 1, 'control_name' => 'Trulab N', 'lot' => 'TN2026001', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'],
|
||||||
['dept_id' => 1, 'control_name' => 'Trulab P', 'lot' => 'TP2026001', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'],
|
['dept_id' => 1, 'control_name' => 'Trulab P', 'lot' => 'TP2026001', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'],
|
||||||
['dept_id' => 1, 'control_name' => 'Cholestest', 'lot' => 'CT2026001', 'producer' => 'Roche', 'exp_date' => '2026-12-31'],
|
['dept_id' => 1, 'control_name' => 'Cholestest', 'lot' => 'CT2026001', 'producer' => 'Roche', 'exp_date' => '2026-12-31'],
|
||||||
|
// February 2026 controls (new lots)
|
||||||
|
['dept_id' => 2, 'control_name' => 'QC Normal Hema', 'lot' => 'H202602', 'producer' => 'Streck', 'exp_date' => '2026-12-31'],
|
||||||
|
['dept_id' => 2, 'control_name' => 'QC Low Hema', 'lot' => 'HL202602', 'producer' => 'Streck', 'exp_date' => '2026-12-31'],
|
||||||
|
['dept_id' => 1, 'control_name' => 'Trulab N', 'lot' => 'TN202602', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'],
|
||||||
];
|
];
|
||||||
$this->db->table('master_controls')->insertBatch($controls);
|
$this->db->table('master_controls')->insertBatch($controls);
|
||||||
}
|
}
|
||||||
@ -87,6 +91,15 @@ class CmodQcSeeder extends Seeder
|
|||||||
['control_id' => 8, 'test_id' => 2, 'mean' => 2.4, 'sd' => 0.12], // Trulab P - Creatinine
|
['control_id' => 8, 'test_id' => 2, 'mean' => 2.4, 'sd' => 0.12], // Trulab P - Creatinine
|
||||||
['control_id' => 8, 'test_id' => 4, 'mean' => 195, 'sd' => 12], // Trulab P - Cholesterol
|
['control_id' => 8, 'test_id' => 4, 'mean' => 195, 'sd' => 12], // Trulab P - Cholesterol
|
||||||
['control_id' => 9, 'test_id' => 4, 'mean' => 180, 'sd' => 10], // Cholestest - Cholesterol
|
['control_id' => 9, 'test_id' => 4, 'mean' => 180, 'sd' => 10], // Cholestest - Cholesterol
|
||||||
|
// February 2026 control-tests
|
||||||
|
['control_id' => 10, 'test_id' => 5, 'mean' => 7.6, 'sd' => 0.5], // QC Normal Hema (Feb) - WBC
|
||||||
|
['control_id' => 10, 'test_id' => 6, 'mean' => 4.7, 'sd' => 0.2], // QC Normal Hema (Feb) - RBC
|
||||||
|
['control_id' => 10, 'test_id' => 7, 'mean' => 14.8, 'sd' => 0.4], // QC Normal Hema (Feb) - HGB
|
||||||
|
['control_id' => 11, 'test_id' => 5, 'mean' => 3.4, 'sd' => 0.25], // QC Low Hema (Feb) - WBC
|
||||||
|
['control_id' => 11, 'test_id' => 6, 'mean' => 2.4, 'sd' => 0.14], // QC Low Hema (Feb) - RBC
|
||||||
|
['control_id' => 12, 'test_id' => 1, 'mean' => 92, 'sd' => 4.2], // Trulab N (Feb) - Glucose
|
||||||
|
['control_id' => 12, 'test_id' => 2, 'mean' => 0.95, 'sd' => 0.05], // Trulab N (Feb) - Creatinine
|
||||||
|
['control_id' => 12, 'test_id' => 4, 'mean' => 148, 'sd' => 9], // Trulab N (Feb) - Cholesterol
|
||||||
];
|
];
|
||||||
$this->db->table('control_tests')->insertBatch($controlTests);
|
$this->db->table('control_tests')->insertBatch($controlTests);
|
||||||
}
|
}
|
||||||
@ -94,17 +107,19 @@ class CmodQcSeeder extends Seeder
|
|||||||
protected function seedResults()
|
protected function seedResults()
|
||||||
{
|
{
|
||||||
$faker = \Faker\Factory::create();
|
$faker = \Faker\Factory::create();
|
||||||
$resultDate = '2026-01-01';
|
|
||||||
$results = [];
|
$results = [];
|
||||||
|
|
||||||
$controlTests = $this->db->table('control_tests')->get()->getResultArray();
|
$controlTests = $this->db->table('control_tests')->get()->getResultArray();
|
||||||
$resultCount = 0;
|
$resultCount = 0;
|
||||||
|
$maxResults = 150; // Increased for more test data
|
||||||
|
|
||||||
|
// January 2026 results (days 1-31)
|
||||||
foreach ($controlTests as $ct) {
|
foreach ($controlTests as $ct) {
|
||||||
$numResults = $faker->numberBetween(3, 4);
|
$numResults = $faker->numberBetween(3, 5);
|
||||||
|
|
||||||
for ($i = 0; $i < $numResults && $resultCount < 50; $i++) {
|
for ($i = 0; $i < $numResults && $resultCount < $maxResults; $i++) {
|
||||||
$resDate = date('Y-m-d', strtotime($resultDate . ' +' . $faker->numberBetween(0, 20) . ' days'));
|
$day = $faker->numberBetween(1, 31);
|
||||||
|
$resDate = "2026-01-" . str_pad($day, 2, '0', STR_PAD_LEFT);
|
||||||
$value = $ct['mean'] + ($faker->randomFloat(2, -2.5, 2.5) * $ct['sd']);
|
$value = $ct['mean'] + ($faker->randomFloat(2, -2.5, 2.5) * $ct['sd']);
|
||||||
|
|
||||||
$results[] = [
|
$results[] = [
|
||||||
@ -118,60 +133,101 @@ class CmodQcSeeder extends Seeder
|
|||||||
$resultCount++;
|
$resultCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// February 2026 results (days 1-28)
|
||||||
|
foreach ($controlTests as $ct) {
|
||||||
|
$numResults = $faker->numberBetween(3, 5);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numResults && $resultCount < $maxResults; $i++) {
|
||||||
|
$day = $faker->numberBetween(1, 28);
|
||||||
|
$resDate = "2026-02-" . str_pad($day, 2, '0', STR_PAD_LEFT);
|
||||||
|
$value = $ct['mean'] + ($faker->randomFloat(2, -2.5, 2.5) * $ct['sd']);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'control_id' => $ct['control_id'],
|
||||||
|
'test_id' => $ct['test_id'],
|
||||||
|
'res_date' => $resDate,
|
||||||
|
'res_value' => round($value, 2),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
$resultCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->db->table('results')->insertBatch($results);
|
$this->db->table('results')->insertBatch($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function seedResultComments()
|
protected function seedTestComments()
|
||||||
{
|
{
|
||||||
// Get all results to associate comments with specific results
|
$faker = \Faker\Factory::create();
|
||||||
$results = $this->db->table('results')->get()->getResultArray();
|
$comments = [];
|
||||||
|
|
||||||
if (empty($results)) {
|
// Get all tests
|
||||||
return;
|
$tests = $this->db->table('master_tests')->get()->getResultArray();
|
||||||
}
|
|
||||||
|
|
||||||
// Map control_id + test_id to result_ids
|
// Comment templates for different test types
|
||||||
$resultMap = [];
|
$commentTemplates = [
|
||||||
foreach ($results as $result) {
|
'QC stable throughout the period',
|
||||||
$key = $result['control_id'] . '_' . $result['test_id'];
|
'Slight drift observed, monitoring continued',
|
||||||
$resultMap[$key][] = $result['result_id'];
|
'Calibration verified, values within range',
|
||||||
}
|
'Reagent lot changed, new QC run initiated',
|
||||||
|
'Instrument maintenance performed',
|
||||||
// Comments data with control_id + test_id for mapping
|
'Shift detected, corrective action taken',
|
||||||
$commentsData = [
|
'Control values consistent with previous lot',
|
||||||
['control_id' => 1, 'test_id' => 1, 'comment_text' => 'Slight drift observed, instrument recalibrated on 01/15'],
|
'Temperature check completed, within specs',
|
||||||
['control_id' => 2, 'test_id' => 4, 'comment_text' => 'High cholesterol values noted, lot change recommended'],
|
'New lot validated successfully',
|
||||||
['control_id' => 3, 'test_id' => 5, 'comment_text' => 'WBC controls stable throughout the month'],
|
'Periodic check satisfactory',
|
||||||
['control_id' => 4, 'test_id' => 6, 'comment_text' => 'RBC QC intermittent shift, probe cleaned'],
|
|
||||||
['control_id' => 5, 'test_id' => 8, 'comment_text' => 'TSH assay maintenance performed on 01/10'],
|
|
||||||
['control_id' => 6, 'test_id' => 10, 'comment_text' => 'Urine protein controls within range'],
|
|
||||||
['control_id' => 1, 'test_id' => 2, 'comment_text' => 'Creatinine QC stable, no issues'],
|
|
||||||
['control_id' => 2, 'test_id' => 1, 'comment_text' => 'Glucose high QC showed consistent elevation, reagent lot changed'],
|
|
||||||
['control_id' => 3, 'test_id' => 7, 'comment_text' => 'Hemoglobin QC performance acceptable'],
|
|
||||||
['control_id' => 5, 'test_id' => 9, 'comment_text' => 'Free T4 calibration curve verified'],
|
|
||||||
// New control comments for January 2026
|
|
||||||
['control_id' => 7, 'test_id' => 1, 'comment_text' => 'Trulab N Glucose stable throughout January'],
|
|
||||||
['control_id' => 7, 'test_id' => 2, 'comment_text' => 'Trulab N Creatinine within acceptable range'],
|
|
||||||
['control_id' => 7, 'test_id' => 4, 'comment_text' => 'Trulab N Cholesterol performance satisfactory'],
|
|
||||||
['control_id' => 8, 'test_id' => 1, 'comment_text' => 'Trulab P Glucose elevated, monitoring continued'],
|
|
||||||
['control_id' => 8, 'test_id' => 2, 'comment_text' => 'Trulab P Creatinine QC stable'],
|
|
||||||
['control_id' => 8, 'test_id' => 4, 'comment_text' => 'Trulab P Cholesterol consistent with expected values'],
|
|
||||||
['control_id' => 9, 'test_id' => 4, 'comment_text' => 'Cholestest performance verified, no issues'],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$comments = [];
|
// Generate comments for Jan 2026 (days 1-31)
|
||||||
foreach ($commentsData as $data) {
|
foreach ($tests as $test) {
|
||||||
$key = $data['control_id'] . '_' . $data['test_id'];
|
$numComments = $faker->numberBetween(3, 6);
|
||||||
if (isset($resultMap[$key]) && !empty($resultMap[$key])) {
|
$usedDates = [];
|
||||||
// Attach comment to the first matching result
|
|
||||||
|
for ($i = 0; $i < $numComments; $i++) {
|
||||||
|
// Pick a random date in January
|
||||||
|
do {
|
||||||
|
$day = $faker->numberBetween(1, 31);
|
||||||
|
$dateKey = "2026-01-" . str_pad($day, 2, '0', STR_PAD_LEFT);
|
||||||
|
} while (in_array($dateKey, $usedDates));
|
||||||
|
|
||||||
|
$usedDates[] = $dateKey;
|
||||||
|
|
||||||
$comments[] = [
|
$comments[] = [
|
||||||
'result_id' => $resultMap[$key][0],
|
'test_id' => $test['test_id'],
|
||||||
'comment_text' => $data['comment_text'],
|
'comment_date' => $dateKey,
|
||||||
|
'comment_text' => $faker->randomElement($commentTemplates),
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->db->table('result_comments')->insertBatch($comments);
|
|
||||||
|
// Generate comments for Feb 2026 (days 1-28)
|
||||||
|
foreach ($tests as $test) {
|
||||||
|
$numComments = $faker->numberBetween(2, 5);
|
||||||
|
$usedDates = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numComments; $i++) {
|
||||||
|
// Pick a random date in February
|
||||||
|
do {
|
||||||
|
$day = $faker->numberBetween(1, 28);
|
||||||
|
$dateKey = "2026-02-" . str_pad($day, 2, '0', STR_PAD_LEFT);
|
||||||
|
} while (in_array($dateKey, $usedDates));
|
||||||
|
|
||||||
|
$usedDates[] = $dateKey;
|
||||||
|
|
||||||
|
$comments[] = [
|
||||||
|
'test_id' => $test['test_id'],
|
||||||
|
'comment_date' => $dateKey,
|
||||||
|
'comment_text' => $faker->randomElement($commentTemplates) . ' (Feb)',
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('test_comments')->insertBatch($comments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
app/Filters/AuthFilter.php
Normal file
52
app/Filters/AuthFilter.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filters;
|
||||||
|
|
||||||
|
use CodeIgniter\Filters\FilterInterface;
|
||||||
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
use Config\Services;
|
||||||
|
use App\Models\Auth\UsersModel;
|
||||||
|
|
||||||
|
class AuthFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function before(RequestInterface $request, $arguments = null)
|
||||||
|
{
|
||||||
|
$session = Services::session();
|
||||||
|
$uri = service('uri');
|
||||||
|
$currentPath = $uri->getPath();
|
||||||
|
|
||||||
|
// Skip auth filter for login/logout routes
|
||||||
|
$excludedPaths = ['login', 'logout'];
|
||||||
|
if (in_array($currentPath, $excludedPaths)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is logged in
|
||||||
|
if (!$session->get('isLoggedIn')) {
|
||||||
|
// Check for remember token
|
||||||
|
$rememberToken = $_COOKIE['remember_token'] ?? null;
|
||||||
|
if ($rememberToken) {
|
||||||
|
$usersModel = new UsersModel();
|
||||||
|
$user = $usersModel->findByRememberToken($rememberToken);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
// Auto-login with remember token
|
||||||
|
$session->set([
|
||||||
|
'isLoggedIn' => true,
|
||||||
|
'userId' => $user['user_id'],
|
||||||
|
'username' => $user['username']
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Models/Auth/UsersModel.php
Normal file
38
app/Models/Auth/UsersModel.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Auth;
|
||||||
|
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
|
||||||
|
class UsersModel extends BaseModel
|
||||||
|
{
|
||||||
|
protected $table = 'master_users';
|
||||||
|
protected $primaryKey = 'user_id';
|
||||||
|
protected $allowedFields = ['username', 'password', 'remember_token'];
|
||||||
|
protected $useSoftDeletes = true;
|
||||||
|
protected $useTimestamps = true;
|
||||||
|
|
||||||
|
public function findByUsername($username)
|
||||||
|
{
|
||||||
|
return $this->where('username', $username)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyPassword($userId, $password)
|
||||||
|
{
|
||||||
|
$user = $this->find($userId);
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return password_verify($password, $user['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRememberToken($userId, $token)
|
||||||
|
{
|
||||||
|
return $this->update($userId, ['remember_token' => $token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRememberToken($token)
|
||||||
|
{
|
||||||
|
return $this->where('remember_token', $token)->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,152 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Models\Qc;
|
|
||||||
|
|
||||||
use App\Models\BaseModel;
|
|
||||||
|
|
||||||
class ResultCommentsModel extends BaseModel {
|
|
||||||
protected $table = 'result_comments';
|
|
||||||
protected $primaryKey = 'result_comment_id';
|
|
||||||
protected $allowedFields = [
|
|
||||||
'result_id',
|
|
||||||
'comment_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_text', $keyword)
|
|
||||||
->groupEnd()
|
|
||||||
->findAll();
|
|
||||||
}
|
|
||||||
return $this->findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get comments by result_id
|
|
||||||
*/
|
|
||||||
public function getByResult(int $resultId): ?array {
|
|
||||||
return $this->where('result_id', $resultId)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all comments for a control+test combination (via results)
|
|
||||||
*/
|
|
||||||
public function getByControlTest(int $controlId, int $testId): ?array {
|
|
||||||
// First get result IDs for this control+test
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$results = $db->table('results')
|
|
||||||
->select('result_id')
|
|
||||||
->where('control_id', $controlId)
|
|
||||||
->where('test_id', $testId)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
|
|
||||||
if (empty($results)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$resultIds = array_column($results, 'result_id');
|
|
||||||
return $this->whereIn('result_id', $resultIds)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->orderBy('created_at', 'DESC')
|
|
||||||
->findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all comments for a test (via results)
|
|
||||||
*/
|
|
||||||
public function getByTest(int $testId): array {
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$results = $db->table('results')
|
|
||||||
->select('result_id')
|
|
||||||
->where('test_id', $testId)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
|
|
||||||
if (empty($results)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$resultIds = array_column($results, 'result_id');
|
|
||||||
return $this->whereIn('result_id', $resultIds)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get comments by control, test, and month (for reports)
|
|
||||||
*/
|
|
||||||
public function getByControlTestMonth(int $controlId, int $testId, string $month): ?array {
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$results = $db->table('results')
|
|
||||||
->select('result_id')
|
|
||||||
->where('control_id', $controlId)
|
|
||||||
->where('test_id', $testId)
|
|
||||||
->where('res_date >=', $month . '-01')
|
|
||||||
->where('res_date <=', $month . '-31')
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
|
|
||||||
if (empty($results)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$resultIds = array_column($results, 'result_id');
|
|
||||||
$comments = $this->whereIn('result_id', $resultIds)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->orderBy('created_at', 'DESC')
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
return $comments ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get comments for multiple results
|
|
||||||
*/
|
|
||||||
public function getByResultIds(array $resultIds): array {
|
|
||||||
if (empty($resultIds)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return $this->whereIn('result_id', $resultIds)
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upsert comment for a result
|
|
||||||
*/
|
|
||||||
public function upsertComment(array $data): int {
|
|
||||||
if (!isset($data['result_id'])) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$existing = $this->where('result_id', $data['result_id'])
|
|
||||||
->where('deleted_at', null)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
if (empty($data['comment_text'])) {
|
|
||||||
// If text is empty, soft delete
|
|
||||||
$this->update($existing['result_comment_id'], ['deleted_at' => date('Y-m-d H:i:s')]);
|
|
||||||
return $existing['result_comment_id'];
|
|
||||||
}
|
|
||||||
$this->update($existing['result_comment_id'], $data);
|
|
||||||
return $existing['result_comment_id'];
|
|
||||||
} else {
|
|
||||||
if (empty($data['comment_text'])) {
|
|
||||||
return 0; // Don't insert empty comments
|
|
||||||
}
|
|
||||||
return $this->insert($data, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -39,9 +39,9 @@ class ResultsModel extends BaseModel {
|
|||||||
r.test_id as testId,
|
r.test_id as testId,
|
||||||
r.res_date as resDate,
|
r.res_date as resDate,
|
||||||
r.res_value as resValue,
|
r.res_value as resValue,
|
||||||
rc.comment_text as resComment
|
tc.comment_text as resComment
|
||||||
');
|
');
|
||||||
$builder->join('result_comments rc', 'rc.result_id = r.result_id AND rc.deleted_at IS NULL', 'left');
|
$builder->join('test_comments tc', 'tc.test_id = r.test_id AND tc.comment_date = r.res_date AND tc.deleted_at IS NULL', 'left');
|
||||||
$builder->where('r.res_date', $date);
|
$builder->where('r.res_date', $date);
|
||||||
$builder->where('r.control_id', $controlId);
|
$builder->where('r.control_id', $controlId);
|
||||||
$builder->where('r.deleted_at', null);
|
$builder->where('r.deleted_at', null);
|
||||||
@ -60,9 +60,9 @@ class ResultsModel extends BaseModel {
|
|||||||
r.test_id as testId,
|
r.test_id as testId,
|
||||||
r.res_date as resDate,
|
r.res_date as resDate,
|
||||||
r.res_value as resValue,
|
r.res_value as resValue,
|
||||||
rc.comment_text as resComment
|
tc.comment_text as resComment
|
||||||
');
|
');
|
||||||
$builder->join('result_comments rc', 'rc.result_id = r.result_id AND rc.deleted_at IS NULL', 'left');
|
$builder->join('test_comments tc', 'tc.test_id = r.test_id AND tc.comment_date = r.res_date AND tc.deleted_at IS NULL', 'left');
|
||||||
$builder->where('r.test_id', $testId);
|
$builder->where('r.test_id', $testId);
|
||||||
$builder->where('r.res_date >=', $month . '-01');
|
$builder->where('r.res_date >=', $month . '-01');
|
||||||
$builder->where('r.res_date <=', $month . '-31');
|
$builder->where('r.res_date <=', $month . '-31');
|
||||||
@ -81,9 +81,9 @@ class ResultsModel extends BaseModel {
|
|||||||
r.result_id as id,
|
r.result_id as id,
|
||||||
r.res_date as resDate,
|
r.res_date as resDate,
|
||||||
r.res_value as resValue,
|
r.res_value as resValue,
|
||||||
rc.comment_text as resComment
|
tc.comment_text as resComment
|
||||||
');
|
');
|
||||||
$builder->join('result_comments rc', 'rc.result_id = r.result_id AND rc.deleted_at IS NULL', 'left');
|
$builder->join('test_comments tc', 'tc.test_id = r.test_id AND tc.comment_date = r.res_date AND tc.deleted_at IS NULL', 'left');
|
||||||
$builder->where('r.control_id', $controlId);
|
$builder->where('r.control_id', $controlId);
|
||||||
$builder->where('r.test_id', $testId);
|
$builder->where('r.test_id', $testId);
|
||||||
$builder->where('r.res_date >=', $month . '-01');
|
$builder->where('r.res_date >=', $month . '-01');
|
||||||
|
|||||||
137
app/Models/Qc/TestCommentsModel.php
Normal file
137
app/Models/Qc/TestCommentsModel.php
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models\Qc;
|
||||||
|
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
|
||||||
|
class TestCommentsModel extends BaseModel {
|
||||||
|
protected $table = 'test_comments';
|
||||||
|
protected $primaryKey = 'test_comment_id';
|
||||||
|
protected $allowedFields = [
|
||||||
|
'test_id',
|
||||||
|
'comment_date',
|
||||||
|
'comment_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_text', $keyword)
|
||||||
|
->groupEnd()
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
return $this->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get comment by test_id and date
|
||||||
|
*/
|
||||||
|
public function getByTestAndDate(int $testId, string $date): ?array {
|
||||||
|
return $this->where('test_id', $testId)
|
||||||
|
->where('comment_date', $date)
|
||||||
|
->where('deleted_at', null)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all comments for a test
|
||||||
|
*/
|
||||||
|
public function getByTest(int $testId): array {
|
||||||
|
return $this->where('test_id', $testId)
|
||||||
|
->where('deleted_at', null)
|
||||||
|
->orderBy('comment_date', 'DESC')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get comments for test(s) within a date range
|
||||||
|
* @param int|array $testId Single test ID or array of test IDs
|
||||||
|
*/
|
||||||
|
public function getByTestAndDateRange($testId, string $startDate, string $endDate): array {
|
||||||
|
$builder = $this->where('comment_date >=', $startDate)
|
||||||
|
->where('comment_date <=', $endDate)
|
||||||
|
->where('deleted_at', null);
|
||||||
|
|
||||||
|
if (is_array($testId)) {
|
||||||
|
$builder->whereIn('test_id', $testId);
|
||||||
|
} else {
|
||||||
|
$builder->where('test_id', $testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->orderBy('comment_date', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get comments by month for a test
|
||||||
|
*/
|
||||||
|
public function getByTestAndMonth(int $testId, string $month): array {
|
||||||
|
return $this->where('test_id', $testId)
|
||||||
|
->where('comment_date >=', $month . '-01')
|
||||||
|
->where('comment_date <=', $month . '-31')
|
||||||
|
->where('deleted_at', null)
|
||||||
|
->orderBy('comment_date', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all comments for multiple tests
|
||||||
|
*/
|
||||||
|
public function getByTestIds(array $testIds): array {
|
||||||
|
if (empty($testIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return $this->whereIn('test_id', $testIds)
|
||||||
|
->where('deleted_at', null)
|
||||||
|
->orderBy('comment_date', 'DESC')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert comment for a test + date combination
|
||||||
|
*/
|
||||||
|
public function upsertComment(array $data): int {
|
||||||
|
if (!isset($data['test_id']) || !isset($data['comment_date'])) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->where('test_id', $data['test_id'])
|
||||||
|
->where('comment_date', $data['comment_date'])
|
||||||
|
->where('deleted_at', null)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
if (empty($data['comment_text'])) {
|
||||||
|
// If text is empty, soft delete
|
||||||
|
$this->update($existing['test_comment_id'], ['deleted_at' => date('Y-m-d H:i:s')]);
|
||||||
|
return $existing['test_comment_id'];
|
||||||
|
}
|
||||||
|
$this->update($existing['test_comment_id'], $data);
|
||||||
|
return $existing['test_comment_id'];
|
||||||
|
} else {
|
||||||
|
if (empty($data['comment_text'])) {
|
||||||
|
return 0; // Don't insert empty comments
|
||||||
|
}
|
||||||
|
return $this->insert($data, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete comment by test_id and date
|
||||||
|
*/
|
||||||
|
public function deleteByTestAndDate(int $testId, string $date): bool {
|
||||||
|
$existing = $this->where('test_id', $testId)
|
||||||
|
->where('comment_date', $date)
|
||||||
|
->where('deleted_at', null)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
return $this->delete($existing['test_comment_id']);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
171
app/Views/auth/login.php
Normal file
171
app/Views/auth/login.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!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>Login - TinyQC</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://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css">
|
||||||
|
<script>
|
||||||
|
const BASEURL = '<?= base_url(''); ?>';
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-base-200 min-h-screen flex items-center justify-center p-4" x-data="loginData()">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||||
|
<div class="card-body p-8">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary mb-4 shadow-lg shadow-primary/20">
|
||||||
|
<i class="fa-solid fa-flask text-white text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-base-content">TinyQC</h1>
|
||||||
|
<p class="text-sm text-base-content/60 mt-1">QC Management System</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
|
<label class="input input-bordered w-full flex items-center gap-2" :class="{ 'input-error': errors.username }">
|
||||||
|
<i class="fa-solid fa-user text-sm opacity-70"></i>
|
||||||
|
<input type="text" x-model="form.username" placeholder="Username" class="grow" required autofocus>
|
||||||
|
</label>
|
||||||
|
<span x-show="errors.username" x-text="errors.username" class="text-error text-xs mt-1"></span>
|
||||||
|
|
||||||
|
<label class="input input-bordered w-full flex items-center gap-2" :class="{ 'input-error': errors.password }">
|
||||||
|
<i class="fa-solid fa-lock text-sm opacity-70"></i>
|
||||||
|
<input :type="showPassword ? 'text' : 'password'" x-model="form.password" placeholder="Password" class="grow" required>
|
||||||
|
<button type="button" @click="showPassword = !showPassword" class="opacity-70 hover:opacity-100">
|
||||||
|
<i class="fa-solid" :class="showPassword ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<span x-show="errors.password" x-text="errors.password" class="text-error text-xs mt-1"></span>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="label cursor-pointer flex items-center gap-2">
|
||||||
|
<input type="checkbox" x-model="form.remember" class="checkbox checkbox-primary checkbox-sm">
|
||||||
|
<span class="label-text text-sm">Remember me</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="errorMessage" x-transition
|
||||||
|
class="alert alert-error alert-sm text-sm py-2">
|
||||||
|
<i class="fa-solid fa-circle-exclamation text-sm"></i>
|
||||||
|
<span x-text="errorMessage"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-full" :class="{ 'loading': loading }"
|
||||||
|
:disabled="loading">
|
||||||
|
<span x-show="!loading">
|
||||||
|
<i class="fa-solid fa-sign-in-alt text-sm mr-2"></i>Sign In
|
||||||
|
</span>
|
||||||
|
<span x-show="loading">Signing in...</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="divider text-xs text-base-content/50">or</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button @click="toggleTheme()" class="btn btn-ghost btn-sm">
|
||||||
|
<i class="fa-solid fa-sun text-lg text-warning" x-show="currentTheme === 'autumn'"></i>
|
||||||
|
<i class="fa-solid fa-moon text-lg text-neutral-content" x-show="currentTheme === 'dracula'"></i>
|
||||||
|
<span class="ml-2" x-text="currentTheme === 'autumn' ? 'Dark Mode' : 'Light Mode'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-sm text-base-content/50 mt-6">
|
||||||
|
© <?= date('Y') ?> TinyQC - PT.Summit
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('loginData', () => ({
|
||||||
|
loading: false,
|
||||||
|
showPassword: false,
|
||||||
|
errorMessage: '',
|
||||||
|
errors: {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
remember: false
|
||||||
|
},
|
||||||
|
get currentTheme() {
|
||||||
|
return localStorage.getItem('theme') || 'autumn';
|
||||||
|
},
|
||||||
|
set currentTheme(value) {
|
||||||
|
localStorage.setItem('theme', value);
|
||||||
|
document.documentElement.setAttribute('data-theme', value);
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
document.documentElement.setAttribute('data-theme', this.currentTheme);
|
||||||
|
},
|
||||||
|
toggleTheme() {
|
||||||
|
this.currentTheme = this.currentTheme === 'autumn' ? 'dracula' : 'autumn';
|
||||||
|
},
|
||||||
|
validateForm() {
|
||||||
|
this.errors = { username: '', password: '' };
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (!this.form.username.trim()) {
|
||||||
|
this.errors.username = 'Username is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.form.password) {
|
||||||
|
this.errors.password = 'Password is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
if (!this.validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BASEURL}login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.form.username,
|
||||||
|
password: this.form.password,
|
||||||
|
remember: this.form.remember
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirect || BASEURL;
|
||||||
|
} else {
|
||||||
|
this.errorMessage = data.message || 'Invalid username or password';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.errorMessage = 'An error occurred. Please try again.';
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -243,8 +243,10 @@ document.addEventListener('alpine:init', () => {
|
|||||||
if (test.existingResult && test.existingResult.resValue !== null) {
|
if (test.existingResult && test.existingResult.resValue !== null) {
|
||||||
this.resultsData[test.testId] = test.existingResult.resValue;
|
this.resultsData[test.testId] = test.existingResult.resValue;
|
||||||
}
|
}
|
||||||
if (test.existingResult && test.existingResult.resComment) {
|
// Get comment from either the new comment field or existing result
|
||||||
this.commentsData[test.testId] = test.existingResult.resComment;
|
const comment = test.comment || (test.existingResult && test.existingResult.resComment);
|
||||||
|
if (comment) {
|
||||||
|
this.commentsData[test.testId] = comment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -288,7 +290,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
for (const item of savedIds) {
|
for (const item of savedIds) {
|
||||||
const comment = this.commentsData[item.testId];
|
const comment = this.commentsData[item.testId];
|
||||||
if (comment) {
|
if (comment) {
|
||||||
await this.saveComment(item.resultId, comment);
|
await this.saveComment(item.testId, this.date, comment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,13 +311,14 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveComment(resultId, commentText) {
|
async saveComment(testId, date, commentText) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BASEURL}api/entry/comment`, {
|
const response = await fetch(`${BASEURL}api/entry/comment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resultId: resultId,
|
testId: testId,
|
||||||
|
date: date,
|
||||||
comment: commentText
|
comment: commentText
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@ -446,14 +446,12 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (json.status === 'success') {
|
if (json.status === 'success') {
|
||||||
// Save comments using the returned result ID map
|
// Save comments for changed entries
|
||||||
const resultIdMap = json.data.resultIdMap || {};
|
|
||||||
for (const key in this.commentsData) {
|
for (const key in this.commentsData) {
|
||||||
if (this.originalComments[key] !== this.commentsData[key]) {
|
if (this.originalComments[key] !== this.commentsData[key]) {
|
||||||
const resultId = resultIdMap[key];
|
const [controlId, day] = key.split('_');
|
||||||
if (resultId) {
|
const date = `${this.month}-${day.padStart(2, '0')}`;
|
||||||
await this.saveComment(resultId, this.commentsData[key]);
|
await this.saveComment(this.selectedTest, date, this.commentsData[key]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,13 +468,14 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveComment(resultId, commentText) {
|
async saveComment(testId, date, commentText) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BASEURL}api/entry/comment`, {
|
const response = await fetch(`${BASEURL}api/entry/comment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resultId: resultId,
|
testId: testId,
|
||||||
|
date: date,
|
||||||
comment: commentText
|
comment: commentText
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,22 +45,20 @@
|
|||||||
<i class="fa-solid fa-sun text-warning" x-show="currentTheme === themeConfig.light"></i>
|
<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>
|
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown dropdown-end" x-data="{ dropdownOpen: false }">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder"
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder">
|
||||||
@click="dropdownOpen = !dropdownOpen">
|
|
||||||
<div
|
<div
|
||||||
class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
|
class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
|
||||||
<span><?= $pageData['userInitials'] ?? 'DR' ?></span>
|
<span><?= $pageData['userInitials'] ?? strtoupper(substr(session()->get('username', 'U'), 0, 1)) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0"
|
<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"
|
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">
|
<li class="menu-title px-4 py-2">
|
||||||
<span
|
<span
|
||||||
class="text-base-content font-bold"><?= $pageData['userName'] ?? 'Lab User' ?></span>
|
class="text-base-content font-bold"><?= $pageData['userName'] ?? ucfirst(session()->get('username', 'User')) ?></span>
|
||||||
<span
|
<span
|
||||||
class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'Administrator' ?></span>
|
class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'User' ?></span>
|
||||||
</li>
|
</li>
|
||||||
<div class="divider my-0 h-px opacity-10"></div>
|
<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><a class="hover:bg-base-200"><i class="fa-solid fa-user text-primary"></i> Profile</a>
|
||||||
|
|||||||
@ -107,7 +107,7 @@
|
|||||||
x-text="formatNumber(item.sd)"></td>
|
x-text="formatNumber(item.sd)"></td>
|
||||||
<td class="text-right pr-4">
|
<td class="text-right pr-4">
|
||||||
<div
|
<div
|
||||||
class="flex justify-end gap-1 opacity-0 group-hover/row:opacity-100 transition-opacity">
|
class="flex justify-end gap-1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-square btn-ghost text-amber-600"
|
class="btn btn-xs btn-square btn-ghost text-amber-600"
|
||||||
@click="showForm(item.controlTestId)">
|
@click="showForm(item.controlTestId)">
|
||||||
|
|||||||
22
index.json
22
index.json
@ -134,9 +134,9 @@
|
|||||||
"methods": ["index", "show", "create", "update", "delete"]
|
"methods": ["index", "show", "create", "update", "delete"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ResultCommentsController",
|
"name": "TestCommentsController",
|
||||||
"namespace": "App\\Controllers\\Qc",
|
"namespace": "App\\Controllers\\Qc",
|
||||||
"file": "app/Controllers/Qc/ResultCommentsController.php",
|
"file": "app/Controllers/Qc/TestCommentsController.php",
|
||||||
"methods": ["index", "show", "create", "update", "delete"]
|
"methods": ["index", "show", "create", "update", "delete"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -186,11 +186,11 @@
|
|||||||
"pk": "control_test_id"
|
"pk": "control_test_id"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ResultCommentsModel",
|
"name": "TestCommentsModel",
|
||||||
"namespace": "App\\Models\\Qc",
|
"namespace": "App\\Models\\Qc",
|
||||||
"file": "app/Models/Qc/ResultCommentsModel.php",
|
"file": "app/Models/Qc/TestCommentsModel.php",
|
||||||
"table": "result_comments",
|
"table": "test_comments",
|
||||||
"pk": "result_comment_id"
|
"pk": "test_comment_id"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"apiEndpoints": [
|
"apiEndpoints": [
|
||||||
@ -219,11 +219,11 @@
|
|||||||
{ "method": "POST", "path": "/api/qc/results", "controller": "Qc\\ResultsController::create" },
|
{ "method": "POST", "path": "/api/qc/results", "controller": "Qc\\ResultsController::create" },
|
||||||
{ "method": "PATCH", "path": "/api/qc/results/(:num)", "controller": "Qc\\ResultsController::update/$1" },
|
{ "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": "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/test-comments", "controller": "Qc\\TestCommentsController::index" },
|
||||||
{ "method": "GET", "path": "/api/qc/result-comments/(:num)", "controller": "Qc\\ResultCommentsController::show/$1" },
|
{ "method": "GET", "path": "/api/qc/test-comments/(:num)", "controller": "Qc\\TestCommentsController::show/$1" },
|
||||||
{ "method": "POST", "path": "/api/qc/result-comments", "controller": "Qc\\ResultCommentsController::create" },
|
{ "method": "POST", "path": "/api/qc/test-comments", "controller": "Qc\\TestCommentsController::create" },
|
||||||
{ "method": "PATCH", "path": "/api/qc/result-comments/(:num)", "controller": "Qc\\ResultCommentsController::update/$1" },
|
{ "method": "PATCH", "path": "/api/qc/test-comments/(:num)", "controller": "Qc\\TestCommentsController::update/$1" },
|
||||||
{ "method": "DELETE", "path": "/api/qc/result-comments/(:num)", "controller": "Qc\\ResultCommentsController::delete/$1" }
|
{ "method": "DELETE", "path": "/api/qc/test-comments/(:num)", "controller": "Qc\\TestCommentsController::delete/$1" }
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{ "method": "GET", "path": "/", "controller": "PageController::dashboard" },
|
{ "method": "GET", "path": "/", "controller": "PageController::dashboard" },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user