diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9d71752 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/app/Controllers/TestsController.php b/app/Controllers/TestsController.php index 28fa87d..4312f69 100644 --- a/app/Controllers/TestsController.php +++ b/app/Controllers/TestsController.php @@ -158,12 +158,13 @@ class TestsController extends BaseController $row['refnum'] = array_map(function ($r) { return [ 'RefNumID' => $r['RefNumID'], - 'NumRefType' => $r['NumRefType'], - 'NumRefTypeVValue' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']), - 'RangeTypeVValue' => ValueSet::getLabel('range_type', $r['RangeType']), - 'SexVValue' => ValueSet::getLabel('gender', $r['Sex']), - 'LowSignVValue' => ValueSet::getLabel('math_sign', $r['LowSign']), - 'HighSignVValue' => ValueSet::getLabel('math_sign', $r['HighSign']), + 'NumRefTypeKey' => $r['NumRefType'], + 'NumRefType' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']), + 'RangeType' => ValueSet::getLabel('range_type', $r['RangeType']), + 'SexKey' => $r['Sex'], + 'Sex' => ValueSet::getLabel('gender', $r['Sex']), + 'LowSign' => ValueSet::getLabel('math_sign', $r['LowSign']), + 'HighSign' => ValueSet::getLabel('math_sign', $r['HighSign']), 'High' => $r['High'] !== null ? (int) $r['High'] : null, 'Flag' => $r['Flag'] ]; @@ -183,10 +184,10 @@ class TestsController extends BaseController $row['reftxt'] = array_map(function ($r) { return [ 'RefTxtID' => $r['RefTxtID'], - 'TxtRefType' => $r['TxtRefType'], - 'TxtRefTypeVValue' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']), - 'Sex' => $r['Sex'], - 'SexVValue' => ValueSet::getLabel('gender', $r['Sex']), + 'TxtRefTypeKey' => $r['TxtRefType'], + 'TxtRefType' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']), + 'SexKey' => $r['Sex'], + 'Sex' => ValueSet::getLabel('gender', $r['Sex']), 'AgeStart' => (int) $r['AgeStart'], 'AgeEnd' => (int) $r['AgeEnd'], 'RefTxt' => $r['RefTxt'], diff --git a/app/Database/Migrations/2026-01-01-000001_CreateLookups.php b/app/Database/Migrations/2026-01-01-000001_CreateValueSet.php similarity index 100% rename from app/Database/Migrations/2026-01-01-000001_CreateLookups.php rename to app/Database/Migrations/2026-01-01-000001_CreateValueSet.php diff --git a/app/Libraries/ValueSet.php b/app/Libraries/ValueSet.php index e1a2315..787988d 100644 --- a/app/Libraries/ValueSet.php +++ b/app/Libraries/ValueSet.php @@ -68,7 +68,9 @@ class ValueSet { foreach ($data as &$row) { foreach ($fieldMappings as $field => $lookupName) { 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) ?? ''; } } } diff --git a/app/Models/Patient/PatientModel.php b/app/Models/Patient/PatientModel.php index 822df55..bd64106 100644 --- a/app/Models/Patient/PatientModel.php +++ b/app/Models/Patient/PatientModel.php @@ -44,7 +44,7 @@ class PatientModel extends BaseModel { $rows = $this->findAll(); $rows = ValueSet::transformLabels($rows, [ - 'Sex' => 'gender', + 'Sex' => 'sex', ]); return $rows; } @@ -81,7 +81,7 @@ class PatientModel extends BaseModel { unset($patient['Comment']); $patient = ValueSet::transformLabels([$patient], [ - 'Sex' => 'gender', + 'Sex' => 'sex', 'Country' => 'country', 'Race' => 'race', 'Religion' => 'religion', diff --git a/tests/unit/ValueSet/ValueSetTest.php b/tests/unit/ValueSet/ValueSetTest.php index b846f83..cdd99c7 100644 --- a/tests/unit/ValueSet/ValueSetTest.php +++ b/tests/unit/ValueSet/ValueSetTest.php @@ -358,14 +358,16 @@ class ValueSetTest extends CIUnitTestCase ['Gender' => '1', 'Country' => 'ID'], ['Gender' => '2', 'Country' => 'US'] ]; - + $result = ValueSet::transformLabels($data, [ 'Gender' => 'sex', 'Country' => 'country' ]); - - $this->assertEquals('Female', $result[0]['GenderText']); - $this->assertEquals('Male', $result[1]['GenderText']); + + $this->assertEquals('Female', $result[0]['Gender']); + $this->assertEquals('1', $result[0]['GenderKey']); + $this->assertEquals('Male', $result[1]['Gender']); + $this->assertEquals('2', $result[1]['GenderKey']); } public function testGetOptions()