tinyqc/app/Controllers/Api/EntryApiController.php
mahdahar 4ae2c75fdd feat: Add department filtering to Controls, Tests, and Entry pages
Implemented comprehensive department filtering across multiple pages in the QC
system to enable users to filter data by laboratory department.

## Backend Changes

**Models:**
- MasterControlsModel: Enhanced search() method to accept optional dept_id
  parameter, added LEFT JOIN with master_depts to include department info
- MasterTestsModel: Updated search() to support dept_id filtering with JOIN

**Controllers:**
- MasterControlsController: Modified index() to accept and pass dept_id parameter
- MasterTestsController: Modified index() to accept and pass dept_id parameter
- EntryApiController: Updated getControls() to filter by dept_id using model search,
  added debug logging for troubleshooting

## Frontend Changes

**Views Updated:**
1. Controls page (/master/control)
   - Added department dropdown with DaisyUI styling
   - Active filter badge and clear button
   - Fetch controls filtered by selected department
   - Added department field to control form dialog

2. Tests page (/master/test)
   - Added department dropdown with active state indication
   - Filter tests by department
   - Clear button to reset filter

3. Daily Entry page (/entry/daily)
   - Added department dropdown in filter section
   - Resets control selection when department changes
   - Fetches controls and tests filtered by department

4. Monthly Entry page (/entry/monthly)
   - Added department dropdown with month selector
   - Resets test selection when department changes
   - Fetches tests filtered by department

## Key Features

- Dropdown UI shows "All Departments" as default
- Selected department name displayed in dropdown button
- Clear button appears when filter is active
- Active department highlighted in dropdown menu
- Loading state while fetching departments
- Automatic reset of dependent selections when department changes
- Consistent UI pattern across all pages using DaisyUI components

## Bug Fixes

- Fixed syntax error in MasterControlsModel search() method
- Removed duplicate/corrupted code that was causing incorrect results
- Added proper deptName field to SELECT query in MasterControlsModel

## Important Note: Department IDs Required

**ACTION REQUIRED**: Existing controls and tests in the database must be assigned
to departments for the filter to work correctly.

To update existing records, run:
  UPDATE master_controls SET dept_id = 1 WHERE dept_id IS NULL;
  UPDATE master_tests SET dept_id = 1 WHERE dept_id IS NULL;

Replace '1' with a valid department ID from master_depts table.

Alternatively, edit each control/test through the UI to assign a department.

## Technical Details

- Alpine.js data binding for reactive department selection
- API expects 'dept_id' query parameter (snake_case)
- Internal state uses camelCase (deptId)
- Departments loaded on init via /api/master/depts
- Search requests include both keyword and dept_id parameters
2026-02-03 16:55:13 +07:00

433 lines
15 KiB
PHP

<?php
namespace App\Controllers\Api;
use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use App\Models\Master\MasterControlsModel;
use App\Models\Master\MasterTestsModel;
use App\Models\Qc\ResultsModel;
use App\Models\Qc\ControlTestsModel;
use App\Models\Qc\ResultCommentsModel;
class EntryApiController extends BaseController
{
use ResponseTrait;
protected $controlModel;
protected $testModel;
protected $resultModel;
protected $controlTestModel;
protected $commentModel;
public function __construct()
{
$this->controlModel = new MasterControlsModel();
$this->testModel = new MasterTestsModel();
$this->resultModel = new ResultsModel();
$this->controlTestModel = new ControlTestsModel();
$this->commentModel = new ResultCommentsModel();
}
/**
* GET /api/entry/controls
* Get controls by dept (optional dept param)
* Optionally filter by date: only non-expired controls
*/
public function getControls()
{
try {
$keyword = $this->request->getGet('keyword');
$deptId = $this->request->getGet('dept_id');
$date = $this->request->getGet('date');
$controls = $this->controlModel->search($keyword, $deptId);
// Debug logging
log_message('debug', 'getControls: keyword=' . var_export($keyword, true) . ', deptId=' . var_export($deptId, true) . ', date=' . var_export($date, true) . ', found=' . count($controls));
// Filter expired controls if date provided
if ($date) {
$controls = array_filter($controls, function ($c) use ($date) {
return $c['expDate'] === null || $c['expDate'] >= $date;
});
}
// Convert to camelCase (BaseModel already returns camelCase)
$data = array_map(function ($c) {
return [
'id' => $c['controlId'],
'controlId' => $c['controlId'],
'controlName' => $c['controlName'],
'lot' => $c['lot'],
'producer' => $c['producer'],
'expDate' => $c['expDate'],
'deptName' => $c['deptName'] ?? null
];
}, $controls);
log_message('debug', 'getControls: returning ' . count($data) . ' controls');
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $data
], 200);
} catch (\Exception $e) {
log_message('error', 'getControls error: ' . $e->getMessage());
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
/**
* GET /api/entry/tests
* Get tests for a control (by control_id)
*/
public function getTests()
{
try {
$controlId = $this->request->getGet('control_id');
if (!$controlId) {
return $this->failValidationErrors(['control_id' => 'Required']);
}
$tests = $this->controlTestModel->getByControl((int) $controlId);
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $tests
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
/**
* GET /api/entry/daily
* Get existing results for date+control
*/
public function getDailyData()
{
try {
$date = $this->request->getGet('date');
$controlId = $this->request->getGet('control_id');
if (!$date || !$controlId) {
return $this->failValidationErrors(['date' => 'Required', 'control_id' => 'Required']);
}
// Get tests for this control
$tests = $this->controlTestModel->getByControl((int) $controlId);
// Get existing results for this date
$existingResults = $this->resultModel->getByDateAndControl($date, (int) $controlId);
// Map existing results by test_id
$resultsByTest = [];
foreach ($existingResults as $r) {
$resultsByTest[$r['testId']] = [
'resultId' => $r['id'],
'resValue' => $r['resValue'],
'resComment' => $r['resComment']
];
}
// Merge tests with existing values
$data = [];
foreach ($tests as $t) {
$existing = $resultsByTest[$t['testId']] ?? null;
$data[] = [
'controlTestId' => $t['id'],
'controlId' => $t['controlId'],
'testId' => $t['testId'],
'testName' => $t['testName'],
'testUnit' => $t['testUnit'],
'mean' => $t['mean'],
'sd' => $t['sd'],
'existingResult' => $existing
];
}
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $data
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
/**
* POST /api/entry/daily
* Save/update daily results (batch)
*/
public function saveDaily()
{
try {
$input = $this->request->getJSON(true);
if (!isset($input['date']) || !isset($input['results']) || !is_array($input['results'])) {
return $this->failValidationErrors(['Invalid input']);
}
$date = $input['date'];
$results = $input['results'];
$savedIds = [];
// Start transaction
$this->resultModel->db->transBegin();
foreach ($results as $r) {
if (!isset($r['controlId']) || !isset($r['testId']) || !isset($r['value'])) {
continue;
}
$data = [
'control_id' => $r['controlId'],
'test_id' => $r['testId'],
'res_date' => $date,
'res_value' => $r['value'] !== '' ? (float) $r['value'] : null
];
if ($data['res_value'] === null) {
continue; // Skip empty values
}
$resultId = $this->resultModel->upsertResult($data);
$savedIds[] = [
'testId' => $r['testId'],
'resultId' => $resultId
];
}
// Commit transaction
$this->resultModel->db->transCommit();
return $this->respond([
'status' => 'success',
'message' => 'Saved ' . count($savedIds) . ' results',
'data' => ['savedIds' => $savedIds]
], 200);
} catch (\Exception $e) {
// Rollback transaction on error
$this->resultModel->db->transRollback();
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
/**
* GET /api/entry/monthly
* Get monthly data by test
*/
public function getMonthlyData()
{
try {
$testId = $this->request->getGet('test_id');
$month = $this->request->getGet('month'); // YYYY-MM
if (!$testId || !$month) {
return $this->failValidationErrors(['test_id' => 'Required', 'month' => 'Required']);
}
// Get test details
$test = $this->testModel->find($testId);
if (!$test) {
return $this->failNotFound('Test not found');
}
// Get controls for this test with QC parameters (filter out expired)
$controls = $this->controlTestModel->getByTest((int) $testId, $month);
// Get existing results for this month
$results = $this->resultModel->getByMonth((int) $testId, $month);
// Get comments for this test (via results)
$comments = $this->commentModel->getByTest((int) $testId);
// Map results by control_id and day
$resultsByControl = [];
foreach ($results as $r) {
$day = (int) date('j', strtotime($r['resDate']));
$resultsByControl[$r['controlId']][$day] = [
'resultId' => $r['id'],
'resValue' => $r['resValue']
];
}
// Map comments by result_id
$commentsByResultId = [];
foreach ($comments as $c) {
$commentsByResultId[$c['resultId']] = [
'commentId' => $c['resultCommentId'],
'commentText' => $c['commentText']
];
}
// Build controls with results array[31]
$controlsWithData = [];
foreach ($controls as $c) {
$resultsByDay = $resultsByControl[$c['controlId']] ?? [];
$resultsArray = array_fill(1, 31, null);
foreach ($resultsByDay as $day => $val) {
$resultWithComment = $val;
// Add comment if exists for this result
if (isset($commentsByResultId[$val['resultId']])) {
$resultWithComment['resComment'] = $commentsByResultId[$val['resultId']]['commentText'];
} else {
$resultWithComment['resComment'] = null;
}
$resultsArray[$day] = $resultWithComment;
}
$controlsWithData[] = [
'controlTestId' => $c['id'],
'controlId' => $c['controlId'],
'controlName' => $c['controlName'],
'lot' => $c['lot'],
'producer' => $c['producer'],
'mean' => $c['mean'],
'sd' => $c['sd'],
'results' => $resultsArray
];
}
$data = [
'test' => [
'testId' => $test['testId'],
'testName' => $test['testName'],
'testUnit' => $test['testUnit']
],
'month' => $month,
'controls' => $controlsWithData
];
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $data
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
/**
* POST /api/entry/monthly
* Save monthly batch (results + comments)
*/
public function saveMonthly()
{
try {
$input = $this->request->getJSON(true);
if (!isset($input['testId']) || !isset($input['month']) || !isset($input['controls'])) {
return $this->failValidationErrors(['Invalid input']);
}
$testId = $input['testId'];
$month = $input['month'];
$controls = $input['controls'];
// Validate month has valid days
$daysInMonth = (int) date('t', strtotime($month . '-01'));
$savedCount = 0;
$resultIdMap = []; // Map controlId + day -> resultId
// Start transaction
$this->resultModel->db->transBegin();
foreach ($controls as $c) {
$controlId = $c['controlId'];
$results = $c['results'] ?? [];
// Save results with optional comments
foreach ($results as $day => $data) {
// Handle both old format (value only) and new format (value + comment)
if (is_array($data)) {
$value = $data['value'];
$commentText = $data['comment'] ?? null;
} else {
$value = $data;
$commentText = null;
}
if ($value === null || $value === '') {
continue;
}
// Validate day exists in month
if ($day < 1 || $day > $daysInMonth) {
continue;
}
$date = $month . '-' . str_pad($day, 2, '0', STR_PAD_LEFT);
$resultData = [
'control_id' => $controlId,
'test_id' => $testId,
'res_date' => $date,
'res_value' => (float) $value
];
$resultId = $this->resultModel->upsertResult($resultData);
$resultIdMap["{$controlId}_{$day}"] = $resultId;
$savedCount++;
}
}
// Commit transaction
$this->resultModel->db->transCommit();
return $this->respond([
'status' => 'success',
'message' => "Saved {$savedCount} results",
'data' => [
'savedCount' => $savedCount,
'resultIdMap' => $resultIdMap
]
], 200);
} catch (\Exception $e) {
// Rollback transaction on error
$this->resultModel->db->transRollback();
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
/**
* POST /api/entry/comment
* Save daily comment (single)
*/
public function saveComment()
{
try {
$input = $this->request->getJSON(true);
$required = ['resultId', 'comment'];
foreach ($required as $field) {
if (!isset($input[$field])) {
return $this->failValidationErrors([$field => 'Required']);
}
}
$commentData = [
'result_id' => $input['resultId'],
'comment_text' => trim($input['comment'])
];
$id = $this->commentModel->upsertComment($commentData);
return $this->respond([
'status' => 'success',
'message' => 'Comment saved',
'data' => ['id' => $id]
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
}