add valueset getter to valueset : , valuesetKey :

This commit is contained in:
mahdahar 2026-01-20 13:20:37 +07:00
parent e96ffa1ca9
commit 823694e4a1
6 changed files with 148 additions and 17 deletions

126
CLAUDE.md Normal file
View File

@ -0,0 +1,126 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
CLQMS is a Clinical Laboratory Quality Management System built with CodeIgniter 4 (PHP 8.1+) providing a REST API backend for laboratory operations from patient registration through test resulting. The frontend uses Alpine.js views in `app/Views/v2/`.
## Development Commands
```bash
# Install dependencies
composer install
# Run all tests
composer test
php vendor/bin/phpunit
# Run single test file
php vendor/bin/phpunit tests/feature/Patients/PatientIndexTest.php
# Run single test method
php vendor/bin/phpunit tests/feature/Patients/PatientIndexTest.php --filter=testIndexWithoutParams
# Run tests with coverage
php vendor/bin/phpunit --coverage-html build/logs/html
# Run CLI commands
php spark help
php spark db:seed DBSeeder
```
## Architecture
### Directory Structure
- `app/Controllers/{Domain}/` - API controllers organized by domain (Patient, Organization, Specimen, Result, Test)
- `app/Models/{Domain}/` - Domain models
- `app/Libraries/Data/*.json` - Static lookup data (gender, order_priority, specimen_type, etc.)
- `app/Views/v2/` - Alpine.js frontend views
- `app/Database/Migrations/` - 10 consolidated migrations (2026-01-01-*)
- `tests/feature/` - API endpoint tests using `FeatureTestTrait`
### Key Patterns
**Controller Pattern:**
```php
class PatientController extends Controller {
use ResponseTrait;
protected $db;
protected $model;
public function __construct() {
$this->db = \Config\Database::connect();
$this->model = new PatientModel();
}
public function index() {
try {
$data = $this->model->findAll();
return $this->respond(['status' => 'success', 'data' => $data], 200);
} catch (\Exception $e) {
return $this->failServerError($e->getMessage());
}
}
}
```
**Model Pattern:** Models extend `BaseModel` which auto-normalizes dates to/from UTC via `beforeInsert`/`beforeUpdate` callbacks. Use `allowedFields` for mass assignment and `useSoftDeletes = true` with `deletedField = 'DelDate'` for soft deletes.
**API Response Format:**
```json
// Success
{"status": "success", "message": "...", "data": [...]}
// Error
{"status": "failed", "message": "..."}
```
### Lookups Library
Use `App\Libraries\Lookups` for static dropdown values (loaded from JSON files, cached):
```php
use App\Libraries\Lookups;
// Get dropdown-formatted data [{value: '1', label: 'Female'}, ...]
$gender = Lookups::get('gender');
// Get label by key
$label = Lookups::getLabel('gender', '1'); // 'Female'
// Clear cache after modifying lookup data
Lookups::clearCache();
```
For dynamic values managed at runtime, use the `/api/valueset*` endpoints instead.
### JWT Authentication
Most API endpoints require JWT auth via `AuthFilter`. Public endpoints include `/v2/login`, `/api/demo/*`, `/api/auth/*`.
## Database Conventions
| Element | Convention |
|---------|------------|
| Tables | snake_case (`patient`, `patvisit`) |
| Columns | PascalCase (`InternalPID`, `PatientID`) |
| Classes | PascalCase (`PatientController`, `BaseModel`) |
| Methods/Variables | camelCase (`getPatient()`, `$internalPID`) |
Soft deletes use `DelDate` field - never hard delete records.
## Key Routes
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/auth/login` | JWT authentication |
| GET/POST | `/api/patient` | Patient CRUD |
| GET/POST | `/api/patvisit` | Patient visits |
| POST | `/api/ordertest` | Create orders |
| POST | `/api/edge/results` | Instrument integration (tiny-edge middleware) |
## Important Notes
- All dates normalized to UTC automatically via `BaseModel`
- No comments in code unless explicitly requested
- Use transactions for multi-table operations
- Validate input before DB operations using CodeIgniter validation rules

View File

@ -158,12 +158,13 @@ class TestsController extends BaseController
$row['refnum'] = array_map(function ($r) { $row['refnum'] = array_map(function ($r) {
return [ return [
'RefNumID' => $r['RefNumID'], 'RefNumID' => $r['RefNumID'],
'NumRefType' => $r['NumRefType'], 'NumRefTypeKey' => $r['NumRefType'],
'NumRefTypeVValue' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']), 'NumRefType' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
'RangeTypeVValue' => ValueSet::getLabel('range_type', $r['RangeType']), 'RangeType' => ValueSet::getLabel('range_type', $r['RangeType']),
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']), 'SexKey' => $r['Sex'],
'LowSignVValue' => ValueSet::getLabel('math_sign', $r['LowSign']), 'Sex' => ValueSet::getLabel('gender', $r['Sex']),
'HighSignVValue' => ValueSet::getLabel('math_sign', $r['HighSign']), 'LowSign' => ValueSet::getLabel('math_sign', $r['LowSign']),
'HighSign' => ValueSet::getLabel('math_sign', $r['HighSign']),
'High' => $r['High'] !== null ? (int) $r['High'] : null, 'High' => $r['High'] !== null ? (int) $r['High'] : null,
'Flag' => $r['Flag'] 'Flag' => $r['Flag']
]; ];
@ -183,10 +184,10 @@ class TestsController extends BaseController
$row['reftxt'] = array_map(function ($r) { $row['reftxt'] = array_map(function ($r) {
return [ return [
'RefTxtID' => $r['RefTxtID'], 'RefTxtID' => $r['RefTxtID'],
'TxtRefType' => $r['TxtRefType'], 'TxtRefTypeKey' => $r['TxtRefType'],
'TxtRefTypeVValue' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']), 'TxtRefType' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
'Sex' => $r['Sex'], 'SexKey' => $r['Sex'],
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']), 'Sex' => ValueSet::getLabel('gender', $r['Sex']),
'AgeStart' => (int) $r['AgeStart'], 'AgeStart' => (int) $r['AgeStart'],
'AgeEnd' => (int) $r['AgeEnd'], 'AgeEnd' => (int) $r['AgeEnd'],
'RefTxt' => $r['RefTxt'], 'RefTxt' => $r['RefTxt'],

View File

@ -68,7 +68,9 @@ class ValueSet {
foreach ($data as &$row) { foreach ($data as &$row) {
foreach ($fieldMappings as $field => $lookupName) { foreach ($fieldMappings as $field => $lookupName) {
if (isset($row[$field]) && $row[$field] !== null) { if (isset($row[$field]) && $row[$field] !== null) {
$row[$field . 'Text'] = self::getLabel($lookupName, $row[$field]) ?? ''; $keyValue = $row[$field];
$row[$field . 'Key'] = $keyValue;
$row[$field] = self::getLabel($lookupName, $keyValue) ?? '';
} }
} }
} }

View File

@ -44,7 +44,7 @@ class PatientModel extends BaseModel {
$rows = $this->findAll(); $rows = $this->findAll();
$rows = ValueSet::transformLabels($rows, [ $rows = ValueSet::transformLabels($rows, [
'Sex' => 'gender', 'Sex' => 'sex',
]); ]);
return $rows; return $rows;
} }
@ -81,7 +81,7 @@ class PatientModel extends BaseModel {
unset($patient['Comment']); unset($patient['Comment']);
$patient = ValueSet::transformLabels([$patient], [ $patient = ValueSet::transformLabels([$patient], [
'Sex' => 'gender', 'Sex' => 'sex',
'Country' => 'country', 'Country' => 'country',
'Race' => 'race', 'Race' => 'race',
'Religion' => 'religion', 'Religion' => 'religion',

View File

@ -358,14 +358,16 @@ class ValueSetTest extends CIUnitTestCase
['Gender' => '1', 'Country' => 'ID'], ['Gender' => '1', 'Country' => 'ID'],
['Gender' => '2', 'Country' => 'US'] ['Gender' => '2', 'Country' => 'US']
]; ];
$result = ValueSet::transformLabels($data, [ $result = ValueSet::transformLabels($data, [
'Gender' => 'sex', 'Gender' => 'sex',
'Country' => 'country' 'Country' => 'country'
]); ]);
$this->assertEquals('Female', $result[0]['GenderText']); $this->assertEquals('Female', $result[0]['Gender']);
$this->assertEquals('Male', $result[1]['GenderText']); $this->assertEquals('1', $result[0]['GenderKey']);
$this->assertEquals('Male', $result[1]['Gender']);
$this->assertEquals('2', $result[1]['GenderKey']);
} }
public function testGetOptions() public function testGetOptions()