feat(valueset): refactor from ID-based to name-based lookups

Complete overhaul of the valueset system to use human-readable names
instead of numeric IDs for improved maintainability and API consistency.
- PatientController: Renamed 'Gender' field to 'Sex' in validation rules
- ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any)
- TestsController: Refactored to use ValueSet library instead of direct valueset queries
- Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods:
  - getOptions() - returns dropdown format [{value, label}]
  - getLabel(, ) - returns label for a value
  - transformLabels(, ) - batch transform records
  - get() and getRaw() for Lookups compatibility
- Added ValueSetApiController for public valueset API endpoints
- Added ValueSet refresh endpoint (POST /api/valueset/refresh)
- Added DemoOrderController for testing order creation without auth
- 2026-01-12-000001: Convert valueset references from VID to VValue
- 2026-01-12-000002: Rename patient.Gender column to Sex
- OrderTestController: Now uses OrderTestModel with proper model pattern
- TestsController: Uses ValueSet library for all lookup operations
- ValueSetController: Simplified to use name-based lookups
- Updated all organization (account/site/workstation) dialogs and index views
- Updated specimen container dialogs and index views
- Updated tests_index.php with ValueSet integration
- Updated patient dialog form and index views
- Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md)
- Consolidated lookups in Lookups.php (removed inline valueset constants)
- Updated all test files to match new field names
- 32 modified files, 17 new files, 2 deleted files
- Net: +661 insertions, -1443 deletions (significant cleanup)
This commit is contained in:
mahdahar 2026-01-12 16:53:41 +07:00
parent f11bde4d30
commit bb7df6b70c
98 changed files with 26950 additions and 1447 deletions

View File

@ -1,12 +0,0 @@
{
"custom_models": [
{
"model_display_name": "MiniMax-M2.1",
"model": "MiniMax-M2.1",
"base_url": "https://api.minimax.io/anthropic",
"api_key": "sk-cp-eMsvq_OqP6UiCBirrr3W6gZlG6-NXnIQeneGNpAJ8aWxywzNq5I9mibfQFBBy84C2Mm7jCqMtjKmbpnx6h02nz_D7xG6ETmBY4K6Nog454cYs_ZkYgMyG_g",
"provider": "anthropic",
"max_tokens": 64000
}
]
}

221
AGENTS.md Normal file
View File

@ -0,0 +1,221 @@
# CLQMS Backend - Agent Instructions
**Project:** Clinical Laboratory Quality Management System (CLQMS) Backend
**Framework:** CodeIgniter 4 (PHP 8.1+)
**Platform:** Windows - Use PowerShell or CMD for terminal commands
**Frontend:** Alpine.js (views/v2 directory)
## Build / Test 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 tests in verbose mode
php vendor/bin/phpunit --verbose
```
**Test Structure:**
- Feature tests: `tests/feature/` - API endpoint testing with `FeatureTestTrait`
- Unit tests: `tests/unit/` - Model/Logic testing
- Base test case: `Tests\Support\v2\MasterTestCase.php` - Provides JWT auth and helper methods
## Code Style Guidelines
### PHP Standards
- **PHP Version:** 8.1 minimum
- **PSR-4 Autoloading:** Follow namespace-to-path conventions (`App\Controllers\*`, `App\Models\*`)
- **Line endings:** Unix-style (LF) - configure editor accordingly
### Naming Conventions
| Element | Convention | Examples |
|---------|------------|----------|
| Classes | PascalCase | `PatientController`, `BaseModel` |
| Methods | camelCase | `getPatient()`, `createPatient()` |
| Variables | camelCase | `$internalPID`, `$patientData` |
| Constants | UPPER_SNAKE_CASE | `ORDER_PRIORITY`, `TEST_TYPE` |
| Table names | snake_case | `patient`, `pat_idt`, `valueset` |
| Column names | PascalCase (original DB) | `InternalPID`, `PatientID` |
### File Organization
```
app/
├── Controllers/{Domain}/
│ └── DomainController.php
├── Models/{Domain}/
│ └── DomainModel.php
├── Libraries/
│ └── Lookups.php
└── Views/v2/
```
### Imports and Namespaces
```php
<?php
namespace App\Controllers\Patient;
use CodeIgniter\Controller;
use CodeIgniter\API\ResponseTrait;
use App\Models\Patient\PatientModel;
```
- Use fully qualified class names or `use` statements
- Group imports logically
- Avoid unnecessary aliases
### Code Formatting
- **Indentation:** 4 spaces (not tabs)
- **Braces:** Allman style for classes/functions, K&R for control structures
- **Line length:** Soft limit 120 characters
- **Empty lines:** Single blank line between method definitions and logical groups
### Type Hints and Return Types
```php
// Required for new code
public function getPatient(int $internalPID): ?array
protected function createPatient(array $input): int
private function checkDbError(object $db, string $context): void
// Use nullable types for optional returns
public function findById(?int $id): ?array
```
### Controller Patterns
```php
class PatientController extends Controller {
use ResponseTrait;
protected $db;
protected $model;
protected $rules;
public function __construct() {
$this->db = \Config\Database::connect();
$this->model = new PatientModel();
$this->rules = [...]; // Validation rules
}
public function index() {
try {
$data = $this->model->findAll();
return $this->respond([...], 200);
} catch (\Exception $e) {
return $this->failServerError($e->getMessage());
}
}
}
```
### Model Patterns
```php
class PatientModel extends BaseModel {
protected $table = 'patient';
protected $primaryKey = 'InternalPID';
protected $allowedFields = [...];
protected $useSoftDeletes = true;
protected $deletedField = 'DelDate';
public function getPatients(array $filters = []): array {
// Query builder chain
$this->select('...');
$this->join(...);
if (!empty($filters['key'])) {
$this->where('key', $filters['key']);
}
return $this->findAll();
}
}
```
### Error Handling
- Controllers: Use try-catch with `failServerError()`, `failValidationErrors()`, `failNotFound()`
- Models: Throw `\Exception` with descriptive messages
- Database errors: Check `$db->error()` after operations
- Always validate input before DB operations
### Validation Rules
```php
protected $rules = [
'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
'EmailAddress' => 'permit_empty|valid_email|max_length[100]',
'Phone' => 'permit_empty|regex_match[/^\+?[0-9]{8,15}$/]',
];
```
### Date Handling
- All dates stored/retrieved in UTC via `BaseModel` callbacks
- Use `utc` helper functions: `convert_array_to_utc()`, `convert_array_to_utc_iso()`
- Format: ISO 8601 (`Y-m-d\TH:i:s\Z`) for API responses
### API Response Format
```php
// Success
return $this->respond([
'status' => 'success',
'message' => 'Data fetched successfully',
'data' => $rows
], 200);
// Created
return $this->respondCreated([
'status' => 'success',
'message' => 'Record created'
]);
// Error
return $this->failServerError('Something went wrong: ' . $e->getMessage());
```
### Database Transactions
```php
$db->transBegin();
try {
$this->insert($data);
$this->checkDbError($db, 'Insert operation');
$db->transCommit();
return $insertId;
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}
```
### Frontend Integration (Alpine.js)
- API calls use `BASEURL` global variable
- Include `credentials: 'include'` for authenticated requests
- Modals use `x-show` with `@click.self` backdrop close
### Lookups Library
Use `App\Libraries\Lookups` for all static lookup values - no database queries:
```php
use App\Libraries\Lookups;
// Frontend dropdown format
Lookups::get('gender'); // [{value: '1', label: 'Female'}, ...]
Lookups::get('test_type'); // [{value: 'TEST', label: 'Test'}, ...]
// Raw key-value pairs
Lookups::getRaw('gender'); // ['1' => 'Female', ...]
// All lookups
Lookups::getAll();
```
### Important Notes
- **Soft deletes:** Use `DelDate` field instead of hard delete
- **UTC timezone:** All dates normalized to UTC automatically
- **JWT auth:** API endpoints require Bearer token in `Authorization` header
- **No comments:** Do not add comments unless explicitly requested

174
CLAUDE.md
View File

@ -1,174 +0,0 @@
# CLQMS Backend - Claude Code Instructions
**Project:** Clinical Laboratory Quality Management System (CLQMS) Backend
**Framework:** CodeIgniter 4 (PHP)
**Platform:** Windows - Use PowerShell or CMD for terminal commands
**Frontend:** Alpine.js (views/v2 directory contains Alpine.js components)
### Views/V2 Structure
```
app/Views/v2/
├── layout/
│ └── main_layout.php # Main layout with sidebar, navbar, Alpine.js layout() component
├── auth/
│ └── login.php # Login page
├── dashboard/
│ └── dashboard_index.php # Dashboard view
├── patients/
│ ├── patients_index.php # Patient list with x-data="patients()" component
│ └── dialog_form.php # Patient form dialog
├── requests/
│ └── requests_index.php # Lab requests
├── settings/
│ └── settings_index.php # Settings page
└── master/
├── organization/ # Organization management (accounts, sites, disciplines, departments, workstations)
├── specimen/ # Specimen management (containers, preparations)
├── tests/ # Lab tests (tests_index, param_dialog, grp_dialog, calc_dialog)
└── valuesets/ # Value sets management
```
### Alpine.js Patterns
- **Global layout:** `layout()` function in `main_layout.php` handles sidebar state, theme toggle, and navigation
- **Page components:** Each page uses `x-data="componentName()"` (e.g., `x-data="patients()"`)
- **API calls:** Use `fetch()` with `BASEURL` global variable and `credentials: 'include'`
- **Dialogs:** Modals use `x-show` with `@click.self` backdrop click to close
- **TailwindCSS 4:** Loaded via CDN with custom CSS variables for theming
## Quick Reference
**Static Library:**
- [`Lookups`](app/Libraries/Lookups.php) - Static lookup constants (no database queries)
**Usage:**
```php
use App\Libraries\Lookups;
// Get formatted lookup [{value: 'KEY', label: 'Label'}, ...]
Lookups::get('gender');
// Get raw associative array ['KEY' => 'Label', ...]
Lookups::getRaw('gender');
// Get all lookups for frontend
Lookups::getAll();
```
## Agent Workflow for Valueset Queries
Use `Lookups` class for all lookup queries - no database queries needed.
### Step 1: Identify the Lookup Constant
**By Category Name:** Match VSName to constant name (e.g., "Gender" → `Lookups::GENDER`)
**By Reference:** Match VSDesc to constant name (e.g., `testdefsite.TestType``Lookups::TEST_TYPE`)
**By VSetDefID:** Map VSetDefID to constant (see Common Lookups table below)
### Step 2: Retrieve Values
```php
// Formatted for frontend dropdowns
Lookups::get('gender'); // [{value: '1', label: 'Female'}, ...]
// Raw key-value pairs
Lookups::getRaw('gender'); // ['1' => 'Female', '2' => 'Male', ...]
```
### Step 3: Return Results
**Response Format (formatted):**
```json
[
{ "value": "1", "label": "Female" },
{ "value": "2", "label": "Male" },
{ "value": "3", "label": "Unknown" }
]
```
## Common Lookups
| VSetDefID | Constant | Search Keywords |
|-----------|----------|-----------------|
| 1 | `WS_TYPE` | workstation, type |
| 2 | `ENABLE_DISABLE` | enable, disable |
| 3 | `GENDER` | gender, sex |
| 4 | `MARITAL_STATUS` | marital, status |
| 5 | `DEATH_INDICATOR` | death, indicator |
| 6 | `IDENTIFIER_TYPE` | identifier, type, KTP, passport |
| 7 | `OPERATION` | operation, CRUD |
| 8 | `DID_TYPE` | device, ID, AAID, IDFA |
| 9 | `REQUESTED_ENTITY` | requested, entity, patient, insurance |
| 10 | `ORDER_PRIORITY` | priority, order, stat, ASAP |
| 11 | `ORDER_STATUS` | status, order |
| 12 | `LOCATION_TYPE` | location, type |
| 13 | `ADDITIVE` | additive, heparin, EDTA |
| 14 | `CONTAINER_CLASS` | container, class |
| 15 | `SPECIMEN_TYPE` | specimen, type, blood, urine |
| 16 | `UNIT` | unit |
| 17 | `GENERATE_BY` | generate, by |
| 18 | `SPECIMEN_ACTIVITY` | specimen, activity |
| 19 | `ACTIVITY_RESULT` | activity, result |
| 20 | `SPECIMEN_STATUS` | specimen, status |
| 21 | `SPECIMEN_CONDITION` | specimen, condition |
| 22 | `SPECIMEN_ROLE` | specimen, role |
| 23 | `COLLECTION_METHOD` | collection, method |
| 24 | `BODY_SITE` | body, site |
| 25 | `CONTAINER_SIZE` | container, size |
| 26 | `FASTING_STATUS` | fasting, status |
| 27 | `TEST_TYPE` | test, type, testdefsite |
| 28 | `RESULT_UNIT` | result, unit |
| 29 | `FORMULA_LANGUAGE` | formula, language |
| 30 | `RACE` | race, ethnicity |
| 31 | `RELIGION` | religion |
| 32 | `ETHNIC` | ethnic |
| 33 | `COUNTRY` | country (loaded from external file) |
| 34 | `CONTAINER_CAP_COLOR` | container, cap, color |
| 35 | `TEST_ACTIVITY` | test, activity |
| 36 | `ADT_EVENT` | ADT, event |
| 37 | `SITE_TYPE` | site, type |
| 38 | `SITE_CLASS` | site, class |
| 39 | `ENTITY_TYPE` | entity, type |
| 40 | `AREA_CLASS` | area, class |
| 41 | `MATH_SIGN` | math, sign |
| 42 | `V_CATEGORY` | category |
| 43 | `RESULT_TYPE` | result, type |
| 44 | `REFERENCE_TYPE` | reference, type |
| 45 | `RANGE_TYPE` | range, type |
| 46 | `NUMERIC_REF_TYPE` | numeric, reference |
| 47 | `TEXT_REF_TYPE` | text, reference |
**Convenience Aliases:**
- `Lookups::PRIORITY` → alias for `ORDER_PRIORITY`
- `Lookups::TEST_STATUS` → Test status values
- `Lookups::REQUEST_STATUS` → alias for `SPECIMEN_STATUS`
- `Lookups::RESULT_STATUS` → Result status values
## Example Agent Conversations
**User:** "Show me Gender values"
**Agent:**
1. `Lookups::get('gender')` → Returns formatted array
2. Output: Female, Male, Unknown
**User:** "What values for testdefsite.TestType?"
**Agent:**
1. `Lookups::get('test_type')` → Returns formatted array
2. Output: TEST, PARAM, CALC, GROUP, TITLE
**User:** "Find specimen status options"
**Agent:**
1. `Lookups::get('specimen_status')` → Returns formatted array
2. Output: To be collected, Collected, In-transport, Arrived, etc.
---
## Commanding Officer Persona Mode
When the user addresses you as their commanding officer or in a starship context, respond accordingly:
- Address the officer respectfully ("Commander", "Captain", "Sir/Ma'am")
- Use military/space command terminology ("affirmative", "reporting", "orders", "status")
- Frame technical responses in mission-ops format ("Systems operational", "Data retrieved", "Report ready")
- Keep responses crisp and professional, befitting ship command
- Example: "Commander, the valueset data you requested is ready for review."
- Start something with basmalah and end with hamdalah

284
MVP_TODO.md Normal file
View File

@ -0,0 +1,284 @@
# CLQMS MVP Todo List
> Clinical Laboratory Quality Management System - Minimum Viable Product
## Quick Start: Create Order with Minimal Master Data
You **don't need** all master data finished to create an order. Here's what's actually required:
### Minimum Required (4 Tables)
```sql
-- 1. Patient (already exists in codebase)
-- Just need at least 1 patient
-- 2. Order Status Values (VSetID=11)
INSERT INTO valueset (VID, VSetID, VValue, VDesc, VOrder) VALUES
(1, 11, 'ORD', 'Ordered', 1),
(2, 11, 'SCH', 'Scheduled', 2),
(3, 11, 'ANA', 'Analysis', 3),
(4, 11, 'VER', 'Verified', 4),
(5, 11, 'REV', 'Reviewed', 5),
(6, 11, 'REP', 'Reported', 6);
-- 3. Priority Values (VSetID=10)
INSERT INTO valueset (VID, VSetID, VValue, VDesc, VOrder) VALUES
(1, 10, 'S', 'Stat', 1),
(2, 10, 'R', 'Routine', 2),
(3, 10, 'A', 'ASAP', 3);
-- 4. Counter for Order ID
INSERT INTO counter (CounterName, CounterValue) VALUES ('ORDER', 1);
-- Run seeder: php spark db:seed MinimalMasterDataSeeder
```
### API Endpoints (No Auth Required for Testing)
```bash
# Create demo order (auto-creates patient if needed)
POST /api/demo/order
{
"PatientID": "PT001",
"NameFirst": "John",
"NameLast": "Doe",
"Gender": "1",
"Birthdate": "1990-05-15",
"Priority": "R",
"OrderingProvider": "Dr. Smith"
}
# List orders
GET /api/demo/orders
# Create order (requires auth)
POST /api/ordertest
{
"InternalPID": 1,
"Priority": "R",
"OrderingProvider": "Dr. Smith"
}
# Update order status
POST /api/ordertest/status
{
"OrderID": "00250112000001",
"OrderStatus": "SCH"
}
```
## Core Workflow
Order → Collection → Reception → Preparation → Analysis → Verification → Review → Reporting
---
## Phase 1: Core Lab Workflow (Must Have)
### 1.1 Order Management
- [ ] Complete `OrderTestController` create/update/delete
- [ ] Implement order ID generation (LLYYMMDDXXXXX format)
- [ ] Implement order attachment handling (ordercom, orderatt tables)
- [ ] Add order status tracking (ORD→SCH→ANA→VER→REV→REP)
- [ ] Create order test mapping (testmap table)
- [ ] Add calculated test parameter auto-selection
### 1.2 Specimen Management
- [ ] Complete `SpecimenController` API
- [ ] Implement specimen ID generation (OrderID + SSS + C)
- [ ] Build specimen collection API (Collection status)
- [ ] Build specimen transport API (In-transport status)
- [ ] Build specimen reception API (Received/Rejected status)
- [ ] Build specimen preparation API (Centrifuge, Aliquot, Pre-treatment)
- [ ] Build specimen storage API (Stored status)
- [ ] Build specimen dispatching API (Dispatch status)
- [ ] Implement specimen condition tracking (HEM, ITC, LIP flags)
### 1.3 Result Management
- [ ] Complete `ResultController` with full CRUD
- [ ] Implement result entry API (numeric, text, valueset, range)
- [ ] Implement result verification workflow (Technical + Clinical)
- [ ] Add reference range validation (numeric, threshold, text, valueset)
- [ ] Implement critical value flagging (threshold-based)
- [ ] Implement result rerun with AspCnt tracking
- [ ] Add result report generation API
### 1.4 Patient Visit
- [ ] Complete `PatVisitController` create/read
- [ ] Implement patient visit to order linking
- [ ] Add admission/discharge/transfer (ADT) tracking
- [ ] Add diagnosis linking (patdiag table)
---
## Phase 2: Instrument Integration (Must Have)
### 2.1 Edge API
- [ ] Complete `EdgeController` results endpoint
- [ ] Implement edgeres table data handling
- [ ] Implement edgestatus tracking
- [ ] Implement edgeack acknowledgment
- [ ] Build instrument orders endpoint (/api/edge/orders)
- [ ] Build order acknowledgment endpoint (/api/edge/orders/:id/ack)
- [ ] Build status logging endpoint (/api/edge/status)
### 2.2 Test Mapping
- [ ] Implement test mapping CRUD (TestMapModel)
- [ ] Build instrument code to LQMS test mapping
- [ ] Add many-to-one mapping support (e.g., glucose variations)
---
## Phase 3: Quality Management (Should Have)
### 3.1 Quality Control
- [ ] Build QC result entry API
- [ ] Implement QC result storage (calres table)
- [ ] Add Levey-Jennings data preparation endpoints
- [ ] Implement QC validation (2SD auto-validation)
- [ ] Add Sigma score calculation endpoint
### 3.2 Calibration
- [ ] Build calibration result entry API
- [ ] Implement calibration factor tracking
- [ ] Add calibration history endpoint
- [ ] Implement calibration validation
### 3.3 Audit Trail
- [ ] Add audit logging middleware
- [ ] Implement data change tracking (what/who/when/how/where)
- [ ] Build audit query endpoint
- [ ] Add security log endpoints
---
## Phase 4: Master Data (Already Have - Need Completion)
### 4.1 Test Definitions ✅ Existing
- [ ] Test definitions (testdefsite)
- [ ] Technical specs (testdeftech)
- [ ] Calculated tests (testdefcal)
- [ ] Group tests (testdefgrp)
- [ ] Test parameters
### 4.2 Reference Ranges ✅ Existing
- [ ] Numeric ranges (refnum)
- [ ] Threshold ranges (refthold)
- [ ] Text ranges (reftxt)
- [ ] Value set ranges (refvset)
### 4.3 Organizations ✅ Existing
- [ ] Sites (SiteController)
- [ ] Departments (DepartmentController)
- [ ] Workstations (WorkstationController)
- [ ] Disciplines (DisciplineController)
### 4.4 Value Sets ✅ Existing
- [ ] Value set definitions (ValueSetDefController)
- [ ] Value set values (ValueSetController)
---
## Phase 5: Inventory & Billing (Nice to Have)
### 5.1 Inventory
- [ ] Build counter management API
- [ ] Implement product catalog endpoints
- [ ] Add reagent tracking
- [ ] Implement consumables usage logging
### 5.2 Billing
- [ ] Add billing account linking
- [ ] Implement tariff selection by service class
- [ ] Build billing export endpoint
---
## Priority Matrix
| Priority | Feature | Controller/Model | Status |
|----------|---------|-----------------|--------|
| P0 | Order CRUD | OrderTestController + OrderTestModel | ✅ Done |
| P0 | Specimen Status | SpecimenController | ⚠️ Needs API |
| P0 | Result Entry | ResultController | ❌ Empty |
| P0 | Result Verification | ResultController | ❌ Empty |
| P1 | Visit Management | PatVisitController | ⚠️ Partial |
| P1 | Instrument Integration | EdgeController | ⚠️ Partial |
| P1 | Reference Range Validation | RefNumModel + API | ⚠️ Need API |
| P2 | QC Results | New Controller | ❌ Not exist |
| P2 | Audit Trail | New Model | ❌ Not exist |
| P3 | Inventory | CounterController | ⚠️ Partial |
| P3 | Billing | New Controller | ❌ Not exist |
---
## Quick Test: Does Order Creation Work?
```bash
# Test 1: Create demo order (no auth required)
curl -X POST http://localhost:8080/api/demo/order \
-H "Content-Type: application/json" \
-d '{"NameFirst": "Test", "NameLast": "Patient"}'
# Expected Response:
{
"status": "success",
"message": "Demo order created successfully",
"data": {
"PatientID": "DEMO1736689600",
"InternalPID": 1,
"OrderID": "00250112000001",
"OrderStatus": "ORD"
}
}
# Test 2: Update order status
curl -X POST http://localhost:8080/api/ordertest/status \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"OrderID": "00250112000001", "OrderStatus": "SCH"}'
```
---
## Success Criteria
### Functional
- Patient registration works ✅
- Test ordering generates valid OrderID and SID
- Specimens track through collection → transport → reception → preparation → analysis
- Results can be entered with reference range validation
- Results verified through VER → REV → REP workflow
- Instruments can send results via Edge API
### Non-Functional
- JWT authentication required for all endpoints
- Soft delete (DelDate) on all transactions
- UTC timezone for all datetime fields
- Audit logging for data changes
- < 2s response time for standard queries
---
## Current Codebase Status
### Controllers (Need Work)
- ❌ OrderTestController - placeholder code, incomplete
- ❌ ResultController - only validates JWT
- ✅ PatientController - complete
- ✅ TestsController - complete
- ✅ PatVisitController - partial
### Models (Good)
- ✅ PatientModel - complete
- ✅ TestDef* models - complete
- ✅ Ref* models - complete
- ✅ ValueSet* models - complete
- ✅ SpecimenModel - exists, needs API
### Missing Controllers
- ❌ SpecimenController - need full implementation
- ❌ ResultController - need full implementation
- ❌ QualityControlController - not exist
- ❌ CalibrationController - not exist
- ❌ AuditController - not exist
- ❌ BillingController - not exist

View File

@ -149,11 +149,11 @@ $routes->group('api', function ($routes) {
// ValueSet // ValueSet
$routes->group('valueset', function ($routes) { $routes->group('valueset', function ($routes) {
$routes->get('/', 'ValueSet\ValueSetController::index'); $routes->get('/', 'ValueSet\ValueSetController::index');
$routes->get('(:num)', 'ValueSet\ValueSetController::show/$1'); $routes->get('(:any)', 'ValueSet\ValueSetController::showByName/$1');
$routes->get('valuesetdef/(:num)', 'ValueSet\ValueSetController::showByValueSetDef/$1');
$routes->post('/', 'ValueSet\ValueSetController::create'); $routes->post('/', 'ValueSet\ValueSetController::create');
$routes->patch('/', 'ValueSet\ValueSetController::update'); $routes->patch('/', 'ValueSet\ValueSetController::update');
$routes->delete('/', 'ValueSet\ValueSetController::delete'); $routes->delete('/', 'ValueSet\ValueSetController::delete');
$routes->post('refresh', 'ValueSet\ValueSetController::refresh');
}); });
$routes->group('valuesetdef', function ($routes) { $routes->group('valuesetdef', function ($routes) {
@ -164,6 +164,17 @@ $routes->group('api', function ($routes) {
$routes->delete('/', 'ValueSet\ValueSetDefController::delete'); $routes->delete('/', 'ValueSet\ValueSetDefController::delete');
}); });
$routes->group('valuesets', function ($routes) {
$routes->get('/', 'ValueSet\ValueSetApiController::index');
$routes->post('refresh', 'ValueSet\ValueSetApiController::refresh');
});
$routes->group('valueset', function ($routes) {
$routes->get('/', 'ValueSetApiController::index');
$routes->get('all', 'ValueSetApiController::all');
$routes->get('(:segment)', 'ValueSetApiController::index/$1');
});
// Counter // Counter
$routes->group('counter', function ($routes) { $routes->group('counter', function ($routes) {
$routes->get('/', 'CounterController::index'); $routes->get('/', 'CounterController::index');
@ -279,7 +290,23 @@ $routes->group('api', function ($routes) {
$routes->patch('/', 'TestsController::update'); $routes->patch('/', 'TestsController::update');
}); });
// Edge API - Integration with tiny-edge // Orders
$routes->group('ordertest', function ($routes) {
$routes->get('/', 'OrderTestController::index');
$routes->get('(:any)', 'OrderTestController::show/$1');
$routes->post('/', 'OrderTestController::create');
$routes->patch('/', 'OrderTestController::update');
$routes->delete('/', 'OrderTestController::delete');
$routes->post('status', 'OrderTestController::updateStatus');
});
// Demo/Test Routes (No Auth)
$routes->group('api/demo', function ($routes) {
$routes->post('order', 'Test\DemoOrderController::createDemoOrder');
$routes->get('orders', 'Test\DemoOrderController::listDemoOrders');
});
// Edge API - Integration with tiny-edge
$routes->group('edge', function ($routes) { $routes->group('edge', function ($routes) {
$routes->post('results', 'EdgeController::results'); $routes->post('results', 'EdgeController::results');
$routes->get('orders', 'EdgeController::orders'); $routes->get('orders', 'EdgeController::orders');

View File

@ -3,215 +3,190 @@ namespace App\Controllers;
use CodeIgniter\API\ResponseTrait; use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\Database\RawSql; use App\Models\OrderTest\OrderTestModel;
use App\Models\Patient\PatientModel;
use App\Models\PatVisit\PatVisitModel;
class OrderTestController extends Controller { class OrderTestController extends Controller {
use ResponseTrait; use ResponseTrait;
protected $db;
protected $model;
protected $patientModel;
protected $visitModel;
protected $rules;
public function __construct() { public function __construct() {
$this->db = \Config\Database::connect(); $this->db = \Config\Database::connect();
$this->rulesOrderTest = [ $this->model = new OrderTestModel();
'NameFirst' => 'required' $this->patientModel = new PatientModel();
$this->visitModel = new PatVisitModel();
$this->rules = [
'InternalPID' => 'required|is_natural'
]; ];
} }
public function index() { public function index() {
$rows = $this->db->table('ordertest')->select("*")->get()->getResultArray(); $internalPID = $this->request->getVar('InternalPID');
try {
if ($internalPID) {
$rows = $this->model->getOrdersByPatient($internalPID);
} else {
$rows = $this->db->table('ordertest')
->where('DelDate', null)
->orderBy('OrderDateTime', 'DESC')
->get()
->getResultArray();
}
if (empty($rows)) {
return $this->respond([ return $this->respond([
'status' => 'success', 'status' => 'success',
'message' => "no Data.", 'message' => 'Data fetched successfully',
'data' => [], 'data' => $rows
], 200); ], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
} }
return $this->respond([
'status' => 'success',
'message'=> "fetch success",
'data' => $rows,
], 200);
} }
public function show($OrderID = null) { public function show($orderID = null) {
$row=$this->db->table('ordertest')->select("*")->where('OrderID', $OrderID)->get()->getRowArray(); try {
$row = $this->model->getOrder($orderID);
if (empty($row)) { if (empty($row)) {
return $this->respond([
'status' => 'success',
'message' => 'Data not found.',
'data' => null
], 200);
}
return $this->respond([ return $this->respond([
'status' => 'success', 'status' => 'success',
'message' => "Data not found.", 'message' => 'Data fetched successfully',
'data' => null, 'data' => $row
], 200); ], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
} }
return $this->respond([
'status' => 'success',
'message'=> "Data fetched successfully",
'data' => $row,
], 200);
} }
public function create() { public function create() {
$input = $this->request->getJSON(true);
if (!$this->validateData($input, $this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
try { try {
$input = $this->request->getJSON(true); if (!$this->patientModel->find($input['InternalPID'])) {
return $this->failValidationErrors(['InternalPID' => 'Patient not found']);
// Prepare data
$dataOrderTest = $this->prepareOrderTestData($input);
$dataOrderCom = $this->prepareOrderComData($input);
$dataOrderAtt = $this->prepareOrderAttData($input);
if (!$this->validateData($dataLocation, $this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
} }
// Start transaction if (!empty($input['PatVisitID'])) {
$this->db->transStart(); $visit = $this->visitModel->find($input['PatVisitID']);
if (!$visit) {
// Insert location return $this->failValidationErrors(['PatVisitID' => 'Visit not found']);
$this->db->table('location')->insert($dataLocation); }
$newLocationID = $this->db->insertID();
// Insert address if available
if (!empty($dataLocationAddress)) {
$dataLocationAddress['LocationID'] = $newLocationID;
$this->db->table('locationaddress')->insert($dataLocationAddress);
} }
// Complete transaction $orderID = $this->model->createOrder($input);
$this->db->transComplete();
if ($this->db->transStatus() === false) {
$dbError = $this->db->error();
return $this->failServerError(
'Failed to create location data (transaction rolled back): ' . ($dbError['message'] ?? 'Unknown database error')
);
}
return $this->respondCreated([ return $this->respondCreated([
'status' => 'success', 'status' => 'success',
'message' => 'Location created successfully', 'message' => 'Order created successfully',
'data' => $dataLocation, 'data' => ['OrderID' => $orderID]
], 201); ], 201);
} catch (\Exception $e) {
} catch (\Throwable $e) {
// Ensure rollback if something goes wrong
if ($this->db->transStatus() !== false) {
$this->db->transRollback();
}
return $this->failServerError('Something went wrong: ' . $e->getMessage()); return $this->failServerError('Something went wrong: ' . $e->getMessage());
} }
} }
public function update() { public function update() {
$input = $this->request->getJSON(true);
if (empty($input['OrderID'])) {
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
}
try { try {
$input = $this->request->getJSON(true); $order = $this->model->getOrder($input['OrderID']);
if (!$order) {
// Prepare data return $this->failNotFound('Order not found');
$dataLocation = $this->prepareLocationData($input);
$dataLocationAddress = $this->prepareLocationAddressData($input);
if (!$this->validateData($dataLocation, $this->rules)) {
return $this->failValidationErrors( $this->validator->getErrors());
} }
// Start transaction $updateData = [];
$this->db->transStart(); if (isset($input['Priority'])) $updateData['Priority'] = $input['Priority'];
if (isset($input['OrderStatus'])) $updateData['OrderStatus'] = $input['OrderStatus'];
if (isset($input['OrderingProvider'])) $updateData['OrderingProvider'] = $input['OrderingProvider'];
if (isset($input['DepartmentID'])) $updateData['DepartmentID'] = $input['DepartmentID'];
if (isset($input['WorkstationID'])) $updateData['WorkstationID'] = $input['WorkstationID'];
// Insert location if (!empty($updateData)) {
$this->db->table('location')->where('LocationID', $dataLocation["LocationID"])->update($dataLocation); $this->model->update($input['OrderID'], $updateData);
// Insert address if available
if (!empty($dataLocationAddress)) {
$dataLocationAddress['LocationID'] = $input["LocationID"];
$this->db->table('locationaddress')->upsert($dataLocationAddress);
} }
// Complete transaction return $this->respond([
$this->db->transComplete(); 'status' => 'success',
'message' => 'Order updated successfully',
if ($this->db->transStatus() === false) { 'data' => $this->model->getOrder($input['OrderID'])
$dbError = $this->db->error(); ], 200);
return $this->failServerError( } catch (\Exception $e) {
'Failed to update location data (transaction rolled back): ' . ($dbError['message'] ?? 'Unknown database error')
);
}
return $this->respondCreated([
'status' => 'success',
'message' => 'Location updated successfully',
'data' => $dataLocation,
], 201);
} catch (\Throwable $e) {
// Ensure rollback if something goes wrong
if ($this->db->transStatus() !== false) {
$this->db->transRollback();
}
return $this->failServerError('Something went wrong: ' . $e->getMessage()); return $this->failServerError('Something went wrong: ' . $e->getMessage());
} }
} }
public function delete() { public function delete() {
$input = $this->request->getJSON(true);
$orderID = $input['OrderID'] ?? null;
if (empty($orderID)) {
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
}
try { try {
$input = $this->request->getJSON(true); $order = $this->model->getOrder($orderID);
$LocationID = $input["LocationID"]; if (!$order) {
if (!$LocationID) { return $this->failNotFound('Order not found');
return $this->failValidationError('LocationID is required.');
} }
$this->model->softDelete($orderID);
$location = $this->db->table('location')->where('LocationID', $LocationID)->get()->getRow();
if (!$location) {
return $this->failNotFound("LocationID with {$LocationID} not found.");
}
$this->db->table('location')->where('LocationID', $LocationID)->update(['DelDate' => NOW()]);
return $this->respondDeleted([ return $this->respondDeleted([
'status' => 'success', 'status' => 'success',
'message' => "Location with {$LocationID} deleted successfully." 'message' => 'Order deleted successfully'
]); ]);
} catch (\Exception $e) {
} catch (\Throwable $e) {
// Ensure rollback if something goes wrong
if ($this->db->transStatus() !== false) {
$this->db->transRollback();
}
return $this->failServerError('Something went wrong: ' . $e->getMessage()); return $this->failServerError('Something went wrong: ' . $e->getMessage());
} }
} }
private function prepareLocationData(array $input): array { public function updateStatus() {
$LinkTo = null; $input = $this->request->getJSON(true);
if (!empty($input['LinkTo'])) {
$ids = array_column($input['LinkTo'], 'InternalPID'); if (empty($input['OrderID']) || empty($input['OrderStatus'])) {
$LinkTo = implode(',', $ids); return $this->failValidationErrors(['error' => 'OrderID and OrderStatus are required']);
} }
$data = [ $validStatuses = ['ORD', 'SCH', 'ANA', 'VER', 'REV', 'REP'];
"LocCode" => $input['LocCode'] ?? null, if (!in_array($input['OrderStatus'], $validStatuses)) {
"Parent" => $input['Parent'] ?? null, return $this->failValidationErrors(['OrderStatus' => 'Invalid status. Valid: ' . implode(', ', $validStatuses)]);
"LocFull" => $input['LocFull'] ?? null, }
"Description" => $input['Description'] ?? null,
];
if(!empty($input["LocationID"])) { $data["LocationID"] = $input["LocationID"]; } try {
$order = $this->model->getOrder($input['OrderID']);
if (!$order) {
return $this->failNotFound('Order not found');
}
return $data; $this->model->updateStatus($input['OrderID'], $input['OrderStatus']);
}
private function prepareLocationAddressData(array $input): array { return $this->respond([
$data = [ 'status' => 'success',
"LocationID" => $input['LocationID'] ?? null, 'message' => 'Order status updated successfully',
"Street1" => $input['Street1'] ?? null, 'data' => $this->model->getOrder($input['OrderID'])
"Street2" => $input['Street2'] ?? null, ], 200);
"City" => $input['City'] ?? null, } catch (\Exception $e) {
"Province" => $input['Province'] ?? null, return $this->failServerError('Something went wrong: ' . $e->getMessage());
"PostCode" => $input['PostCode'] ?? null, }
"GeoLocationSystem" => $input['GeoLocationSystem'] ?? null,
"GeoLocationData" => $input['GeoLocationData'] ?? null,
];
return $data;
} }
} }

View File

@ -20,7 +20,7 @@ class PatientController extends Controller {
'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', 'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]', 'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]',
'Gender' => 'required', 'Sex' => 'required',
'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]', 'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
'NameMiddle' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]', 'NameMiddle' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',

View File

@ -0,0 +1,78 @@
<?php
namespace App\Controllers\Test;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Controller;
use App\Models\Patient\PatientModel;
use App\Models\OrderTest\OrderTestModel;
class DemoOrderController extends Controller {
use ResponseTrait;
protected $db;
protected $patientModel;
protected $orderModel;
public function __construct() {
$this->db = \Config\Database::connect();
$this->patientModel = new PatientModel();
$this->orderModel = new OrderTestModel();
}
public function createDemoOrder() {
$input = $this->request->getJSON(true);
$patientData = [
'PatientID' => $input['PatientID'] ?? 'DEMO' . time(),
'Gender' => $input['Gender'] ?? '1',
'NameFirst' => $input['NameFirst'] ?? 'Demo',
'NameLast' => $input['NameLast'] ?? 'Patient',
'Birthdate' => $input['Birthdate'] ?? '1990-01-01'
];
$patient = $this->patientModel->where('PatientID', $patientData['PatientID'])->findAll();
if (empty($patient)) {
$internalPID = $this->patientModel->createPatient($patientData);
} else {
$internalPID = $patient[0]['InternalPID'];
}
$orderData = [
'InternalPID' => $internalPID,
'PatVisitID' => $input['PatVisitID'] ?? null,
'Priority' => $input['Priority'] ?? 'R',
'OrderingProvider' => $input['OrderingProvider'] ?? 'Dr. Demo',
'DepartmentID' => $input['DepartmentID'] ?? 1,
];
$orderID = $this->orderModel->createOrder($orderData);
return $this->respond([
'status' => 'success',
'message' => 'Demo order created successfully',
'data' => [
'PatientID' => $patientData['PatientID'],
'InternalPID' => $internalPID,
'OrderID' => $orderID,
'OrderStatus' => 'ORD'
]
], 201);
}
public function listDemoOrders() {
$orders = $this->db->table('ordertest ot')
->select('ot.OrderID, ot.InternalPID, p.PatientID, ot.OrderDateTime, ot.Priority, ot.OrderStatus')
->join('patient p', 'p.InternalPID = ot.InternalPID')
->where('ot.DelDate', null)
->orderBy('ot.OrderDateTime', 'DESC')
->limit(50)
->get()
->getResultArray();
return $this->respond([
'status' => 'success',
'message' => 'Data fetched successfully',
'data' => $orders
], 200);
}
}

View File

@ -3,6 +3,7 @@ namespace App\Controllers;
use CodeIgniter\API\ResponseTrait; use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use App\Libraries\ValueSet;
class TestsController extends BaseController class TestsController extends BaseController
{ {
@ -15,18 +16,9 @@ class TestsController extends BaseController
protected $modelTech; protected $modelTech;
protected $modelGrp; protected $modelGrp;
protected $modelMap; protected $modelMap;
protected $modelValueSet;
protected $modelRefNum; protected $modelRefNum;
protected $modelRefTxt; protected $modelRefTxt;
// Valueset ID constants
const VALUESET_REF_TYPE = 44; // testdeftech.RefType
const VALUESET_RANGE_TYPE = 45; // refnum.RangeType
const VALUESET_NUM_REF_TYPE = 46; // refnum.NumRefType
const VALUESET_TXT_REF_TYPE = 47; // reftxt.TxtRefType
const VALUESET_SEX = 3; // Sex values
const VALUESET_MATH_SIGN = 41; // LowSign, HighSign
public function __construct() public function __construct()
{ {
$this->db = \Config\Database::connect(); $this->db = \Config\Database::connect();
@ -35,11 +27,9 @@ class TestsController extends BaseController
$this->modelTech = new \App\Models\Test\TestDefTechModel; $this->modelTech = new \App\Models\Test\TestDefTechModel;
$this->modelGrp = new \App\Models\Test\TestDefGrpModel; $this->modelGrp = new \App\Models\Test\TestDefGrpModel;
$this->modelMap = new \App\Models\Test\TestMapModel; $this->modelMap = new \App\Models\Test\TestMapModel;
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
$this->modelRefNum = new \App\Models\RefRange\RefNumModel; $this->modelRefNum = new \App\Models\RefRange\RefNumModel;
$this->modelRefTxt = new \App\Models\RefRange\RefTxtModel; $this->modelRefTxt = new \App\Models\RefRange\RefTxtModel;
// Validation rules for main test definition
$this->rules = [ $this->rules = [
'TestSiteCode' => 'required|min_length[3]|max_length[6]', 'TestSiteCode' => 'required|min_length[3]|max_length[6]',
'TestSiteName' => 'required', 'TestSiteName' => 'required',
@ -48,11 +38,6 @@ class TestsController extends BaseController
]; ];
} }
/**
* GET /v1/tests
* GET /v1/tests/site
* List all tests with optional filtering
*/
public function index() public function index()
{ {
$siteId = $this->request->getGet('SiteID'); $siteId = $this->request->getGet('SiteID');
@ -64,9 +49,7 @@ class TestsController extends BaseController
$builder = $this->db->table('testdefsite') $builder = $this->db->table('testdefsite')
->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType, ->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt, testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate, testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate")
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
->where('testdefsite.EndDate IS NULL'); ->where('testdefsite.EndDate IS NULL');
if ($siteId) { if ($siteId) {
@ -94,21 +77,20 @@ class TestsController extends BaseController
if (empty($rows)) { if (empty($rows)) {
return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => []], 200); return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => []], 200);
} }
$rows = ValueSet::transformLabels($rows, [
'TestType' => 'test_type',
]);
return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $rows], 200); return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $rows], 200);
} }
/**
* GET /v1/tests/{id}
* GET /v1/tests/site/{id}
* Get single test by ID with all related details
*/
public function show($id = null) public function show($id = null)
{ {
if (!$id) if (!$id)
return $this->failValidationErrors('TestSiteID is required'); return $this->failValidationErrors('TestSiteID is required');
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName") $row = $this->model->select("testdefsite.*")
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
->where("testdefsite.TestSiteID", $id) ->where("testdefsite.TestSiteID", $id)
->find($id); ->find($id);
@ -116,11 +98,13 @@ class TestsController extends BaseController
return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => null], 200); return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => null], 200);
} }
// Load related details based on TestType $row = ValueSet::transformLabels([$row], [
$typeCode = $row['TypeCode'] ?? ''; 'TestType' => 'test_type',
])[0];
$typeCode = $row['TestType'] ?? '';
if ($typeCode === 'CALC') { if ($typeCode === 'CALC') {
// Load calculation details
$row['testdefcal'] = $this->db->table('testdefcal') $row['testdefcal'] = $this->db->table('testdefcal')
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName') ->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left') ->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
@ -129,29 +113,27 @@ class TestsController extends BaseController
->where('testdefcal.EndDate IS NULL') ->where('testdefcal.EndDate IS NULL')
->get()->getResultArray(); ->get()->getResultArray();
// Load test mappings
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
} elseif ($typeCode === 'GROUP') { } elseif ($typeCode === 'GROUP') {
// Load group members with test details
$row['testdefgrp'] = $this->db->table('testdefgrp') $row['testdefgrp'] = $this->db->table('testdefgrp')
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode') ->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType')
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left') ->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
->join('valueset vs', 'vs.VID=t.TestType', 'left')
->where('testdefgrp.TestSiteID', $id) ->where('testdefgrp.TestSiteID', $id)
->where('testdefgrp.EndDate IS NULL') ->where('testdefgrp.EndDate IS NULL')
->orderBy('testdefgrp.TestGrpID', 'ASC') ->orderBy('testdefgrp.TestGrpID', 'ASC')
->get()->getResultArray(); ->get()->getResultArray();
// Load test mappings $row['testdefgrp'] = ValueSet::transformLabels($row['testdefgrp'], [
'TestType' => 'test_type',
]);
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
} elseif ($typeCode === 'TITLE') { } elseif ($typeCode === 'TITLE') {
// Load test mappings only for TITLE type
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
} else { } else {
// TEST or PARAM - load technical details
$row['testdeftech'] = $this->db->table('testdeftech') $row['testdeftech'] = $this->db->table('testdeftech')
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName') ->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left') ->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
@ -160,50 +142,38 @@ class TestsController extends BaseController
->where('testdeftech.EndDate IS NULL') ->where('testdeftech.EndDate IS NULL')
->get()->getResultArray(); ->get()->getResultArray();
// Load test mappings
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
// Load refnum/reftxt based on RefType
if (!empty($row['testdeftech'])) { if (!empty($row['testdeftech'])) {
$techData = $row['testdeftech'][0]; $techData = $row['testdeftech'][0];
$refType = (int) $techData['RefType']; $refType = $techData['RefType'];
// Load refnum for NMRC type (RefType = 1) if ($refType === '1') {
if ($refType === 1) {
$refnumData = $this->modelRefNum $refnumData = $this->modelRefNum
->where('TestSiteID', $id) ->where('TestSiteID', $id)
->where('EndDate IS NULL') ->where('EndDate IS NULL')
->orderBy('Display', 'ASC') ->orderBy('Display', 'ASC')
->findAll(); ->findAll();
// Add VValue for display
$row['refnum'] = array_map(function ($r) { $row['refnum'] = array_map(function ($r) {
return [ return [
'RefNumID' => $r['RefNumID'], 'RefNumID' => $r['RefNumID'],
'NumRefType' => (int) $r['NumRefType'], 'NumRefType' => $r['NumRefType'],
'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']), 'NumRefTypeVValue' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
'RangeType' => (int) $r['RangeType'], 'RangeTypeVValue' => ValueSet::getLabel('range_type', $r['RangeType']),
'RangeTypeVValue' => $this->getVValue(45, $r['RangeType']), 'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
'Sex' => (int) $r['Sex'], 'LowSignVValue' => ValueSet::getLabel('math_sign', $r['LowSign']),
'SexVValue' => $this->getVValue(3, $r['Sex']), 'HighSignVValue' => ValueSet::getLabel('math_sign', $r['HighSign']),
'AgeStart' => (int) $r['AgeStart'],
'AgeEnd' => (int) $r['AgeEnd'],
'LowSign' => $r['LowSign'] !== null ? (int) $r['LowSign'] : null,
'LowSignVValue' => $this->getVValue(41, $r['LowSign']),
'Low' => $r['Low'] !== null ? (int) $r['Low'] : null,
'HighSign' => $r['HighSign'] !== null ? (int) $r['HighSign'] : null,
'HighSignVValue' => $this->getVValue(41, $r['HighSign']),
'High' => $r['High'] !== null ? (int) $r['High'] : null, 'High' => $r['High'] !== null ? (int) $r['High'] : null,
'Flag' => $r['Flag'] 'Flag' => $r['Flag']
]; ];
}, $refnumData ?? []); }, $refnumData ?? []);
$row['numRefTypeOptions'] = $this->getValuesetOptions(46); $row['numRefTypeOptions'] = ValueSet::getOptions('numeric_ref_type');
$row['rangeTypeOptions'] = $this->getValuesetOptions(45); $row['rangeTypeOptions'] = ValueSet::getOptions('range_type');
} }
// Load reftxt for TEXT type (RefType = 2) if ($refType === '2') {
if ($refType === 2) {
$reftxtData = $this->modelRefTxt $reftxtData = $this->modelRefTxt
->where('TestSiteID', $id) ->where('TestSiteID', $id)
->where('EndDate IS NULL') ->where('EndDate IS NULL')
@ -213,10 +183,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' => (int) $r['TxtRefType'], 'TxtRefType' => $r['TxtRefType'],
'TxtRefTypeVValue' => $this->getVValue(47, $r['TxtRefType']), 'TxtRefTypeVValue' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
'Sex' => (int) $r['Sex'], 'Sex' => $r['Sex'],
'SexVValue' => $this->getVValue(3, $r['Sex']), 'SexVValue' => 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'],
@ -224,24 +194,18 @@ class TestsController extends BaseController
]; ];
}, $reftxtData ?? []); }, $reftxtData ?? []);
$row['txtRefTypeOptions'] = $this->getValuesetOptions(47); $row['txtRefTypeOptions'] = ValueSet::getOptions('text_ref_type');
} }
} }
} }
// Include valueset options for dropdowns $row['refTypeOptions'] = ValueSet::getOptions('reference_type');
$row['refTypeOptions'] = $this->getValuesetOptions(self::VALUESET_REF_TYPE); $row['sexOptions'] = ValueSet::getOptions('gender');
$row['sexOptions'] = $this->getValuesetOptions(self::VALUESET_SEX); $row['mathSignOptions'] = ValueSet::getOptions('math_sign');
$row['mathSignOptions'] = $this->getValuesetOptions(self::VALUESET_MATH_SIGN);
return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $row], 200); return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $row], 200);
} }
/**
* POST /v1/tests
* POST /v1/tests/site
* Create new test definition
*/
public function create() public function create()
{ {
$input = $this->request->getJSON(true); $input = $this->request->getJSON(true);
@ -253,7 +217,6 @@ class TestsController extends BaseController
$this->db->transStart(); $this->db->transStart();
try { try {
// 1. Insert into Main Table (testdefsite)
$testSiteData = [ $testSiteData = [
'SiteID' => $input['SiteID'], 'SiteID' => $input['SiteID'],
'TestSiteCode' => $input['TestSiteCode'], 'TestSiteCode' => $input['TestSiteCode'],
@ -275,7 +238,6 @@ class TestsController extends BaseController
throw new \Exception("Failed to insert main test definition"); throw new \Exception("Failed to insert main test definition");
} }
// 2. Handle Details based on TestType
$this->handleDetails($id, $input, 'insert'); $this->handleDetails($id, $input, 'insert');
$this->db->transComplete(); $this->db->transComplete();
@ -295,16 +257,10 @@ class TestsController extends BaseController
} }
} }
/**
* PUT/PATCH /v1/tests/{id}
* PUT/PATCH /v1/tests/site/{id}
* Update existing test definition
*/
public function update($id = null) public function update($id = null)
{ {
$input = $this->request->getJSON(true); $input = $this->request->getJSON(true);
// Determine ID
if (!$id && isset($input["TestSiteID"])) { if (!$id && isset($input["TestSiteID"])) {
$id = $input["TestSiteID"]; $id = $input["TestSiteID"];
} }
@ -312,7 +268,6 @@ class TestsController extends BaseController
return $this->failValidationErrors('TestSiteID is required.'); return $this->failValidationErrors('TestSiteID is required.');
} }
// Verify record exists
$existing = $this->model->find($id); $existing = $this->model->find($id);
if (!$existing) { if (!$existing) {
return $this->failNotFound('Test not found'); return $this->failNotFound('Test not found');
@ -321,7 +276,6 @@ class TestsController extends BaseController
$this->db->transStart(); $this->db->transStart();
try { try {
// 1. Update Main Table
$testSiteData = []; $testSiteData = [];
$allowedUpdateFields = [ $allowedUpdateFields = [
'TestSiteCode', 'TestSiteCode',
@ -348,7 +302,6 @@ class TestsController extends BaseController
$this->model->update($id, $testSiteData); $this->model->update($id, $testSiteData);
} }
// 2. Handle Details
$this->handleDetails($id, $input, 'update'); $this->handleDetails($id, $input, 'update');
$this->db->transComplete(); $this->db->transComplete();
@ -368,16 +321,10 @@ class TestsController extends BaseController
} }
} }
/**
* DELETE /v1/tests/{id}
* DELETE /v1/tests/site/{id}
* Soft delete test by setting EndDate
*/
public function delete($id = null) public function delete($id = null)
{ {
$input = $this->request->getJSON(true); $input = $this->request->getJSON(true);
// Determine ID
if (!$id && isset($input["TestSiteID"])) { if (!$id && isset($input["TestSiteID"])) {
$id = $input["TestSiteID"]; $id = $input["TestSiteID"];
} }
@ -385,13 +332,11 @@ class TestsController extends BaseController
return $this->failValidationErrors('TestSiteID is required.'); return $this->failValidationErrors('TestSiteID is required.');
} }
// Verify record exists
$existing = $this->model->find($id); $existing = $this->model->find($id);
if (!$existing) { if (!$existing) {
return $this->failNotFound('Test not found'); return $this->failNotFound('Test not found');
} }
// Check if already disabled
if (!empty($existing['EndDate'])) { if (!empty($existing['EndDate'])) {
return $this->failValidationErrors('Test is already disabled'); return $this->failValidationErrors('Test is already disabled');
} }
@ -401,15 +346,11 @@ class TestsController extends BaseController
try { try {
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
// 1. Soft delete main record
$this->model->update($id, ['EndDate' => $now]); $this->model->update($id, ['EndDate' => $now]);
// 2. Get TestType to handle related records
$testType = $existing['TestType']; $testType = $existing['TestType'];
$vs = $this->modelValueSet->find($testType); $typeCode = $testType;
$typeCode = $vs['VValue'] ?? '';
// 3. Soft delete related records based on TestType
if ($typeCode === 'CALC') { if ($typeCode === 'CALC') {
$this->db->table('testdefcal') $this->db->table('testdefcal')
->where('TestSiteID', $id) ->where('TestSiteID', $id)
@ -423,12 +364,10 @@ class TestsController extends BaseController
->where('TestSiteID', $id) ->where('TestSiteID', $id)
->update(['EndDate' => $now]); ->update(['EndDate' => $now]);
// Soft delete refnum and reftxt records
$this->modelRefNum->where('TestSiteID', $id)->set('EndDate', $now)->update(); $this->modelRefNum->where('TestSiteID', $id)->set('EndDate', $now)->update();
$this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update(); $this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update();
} }
// 4. Soft delete test mappings
$this->db->table('testmap') $this->db->table('testmap')
->where('TestSiteID', $id) ->where('TestSiteID', $id)
->update(['EndDate' => $now]); ->update(['EndDate' => $now]);
@ -450,41 +389,10 @@ class TestsController extends BaseController
} }
} }
/**
* Helper to get valueset options
*/
private function getValuesetOptions($vsetID)
{
return $this->db->table('valueset')
->select('VID as vid, VValue as vvalue, VDesc as vdesc')
->where('VSetID', $vsetID)
->orderBy('VOrder', 'ASC')
->get()->getResultArray();
}
/**
* Helper to get VValue from VID for display
*/
private function getVValue($vsetID, $vid)
{
if ($vid === null || $vid === '')
return null;
$row = $this->db->table('valueset')
->select('VValue as vvalue')
->where('VSetID', $vsetID)
->where('VID', (int) $vid)
->get()->getRowArray();
return $row ? $row['vvalue'] : null;
}
/**
* Helper to handle inserting/updating sub-tables based on TestType
*/
private function handleDetails($testSiteID, $input, $action) private function handleDetails($testSiteID, $input, $action)
{ {
$testTypeID = $input['TestType'] ?? null; $testTypeID = $input['TestType'] ?? null;
// If update and TestType not in payload, fetch from DB
if (!$testTypeID && $action === 'update') { if (!$testTypeID && $action === 'update') {
$existing = $this->model->find($testSiteID); $existing = $this->model->find($testSiteID);
$testTypeID = $existing['TestType'] ?? null; $testTypeID = $existing['TestType'] ?? null;
@ -493,11 +401,8 @@ class TestsController extends BaseController
if (!$testTypeID) if (!$testTypeID)
return; return;
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE) $typeCode = $testTypeID;
$vs = $this->modelValueSet->find($testTypeID);
$typeCode = $vs['VValue'] ?? '';
// Get details data from input
$details = $input['details'] ?? $input; $details = $input['details'] ?? $input;
$details['TestSiteID'] = $testSiteID; $details['TestSiteID'] = $testSiteID;
$details['SiteID'] = $input['SiteID'] ?? 1; $details['SiteID'] = $input['SiteID'] ?? 1;
@ -512,8 +417,6 @@ class TestsController extends BaseController
break; break;
case 'TITLE': case 'TITLE':
// TITLE type only has testdefsite, no additional details needed
// But we should save test mappings if provided
if (isset($input['testmap']) && is_array($input['testmap'])) { if (isset($input['testmap']) && is_array($input['testmap'])) {
$this->saveTestMap($testSiteID, $input['testmap'], $action); $this->saveTestMap($testSiteID, $input['testmap'], $action);
} }
@ -524,32 +427,25 @@ class TestsController extends BaseController
default: default:
$this->saveTechDetails($testSiteID, $details, $action, $typeCode); $this->saveTechDetails($testSiteID, $details, $action, $typeCode);
// Save refnum/reftxt for TEST/PARAM types
if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) { if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) {
$refType = (int) $details['RefType']; $refType = $details['RefType'];
// Save refnum for NMRC type (RefType = 1) if ($refType === '1' && isset($input['refnum']) && is_array($input['refnum'])) {
if ($refType === 1 && isset($input['refnum']) && is_array($input['refnum'])) {
$this->saveRefNumRanges($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1); $this->saveRefNumRanges($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1);
} }
// Save reftxt for TEXT type (RefType = 2) if ($refType === '2' && isset($input['reftxt']) && is_array($input['reftxt'])) {
if ($refType === 2 && isset($input['reftxt']) && is_array($input['reftxt'])) {
$this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, $input['SiteID'] ?? 1); $this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, $input['SiteID'] ?? 1);
} }
} }
break; break;
} }
// Save test mappings for TEST and CALC types as well
if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) { if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) {
$this->saveTestMap($testSiteID, $input['testmap'], $action); $this->saveTestMap($testSiteID, $input['testmap'], $action);
} }
} }
/**
* Save technical details for TEST and PARAM types
*/
private function saveTechDetails($testSiteID, $data, $action, $typeCode) private function saveTechDetails($testSiteID, $data, $action, $typeCode)
{ {
$techData = [ $techData = [
@ -586,9 +482,6 @@ class TestsController extends BaseController
} }
} }
/**
* Save refnum ranges for NMRC type
*/
private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID) private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID)
{ {
if ($action === 'update') { if ($action === 'update') {
@ -601,14 +494,14 @@ class TestsController extends BaseController
$this->modelRefNum->insert([ $this->modelRefNum->insert([
'TestSiteID' => $testSiteID, 'TestSiteID' => $testSiteID,
'SiteID' => $siteID, 'SiteID' => $siteID,
'NumRefType' => (int) $range['NumRefType'], 'NumRefType' => $range['NumRefType'],
'RangeType' => (int) $range['RangeType'], 'RangeType' => $range['RangeType'],
'Sex' => (int) $range['Sex'], 'Sex' => $range['Sex'],
'AgeStart' => (int) ($range['AgeStart'] ?? 0), 'AgeStart' => (int) ($range['AgeStart'] ?? 0),
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
'LowSign' => !empty($range['LowSign']) ? (int) $range['LowSign'] : null, 'LowSign' => !empty($range['LowSign']) ? $range['LowSign'] : null,
'Low' => !empty($range['Low']) ? (int) $range['Low'] : null, 'Low' => !empty($range['Low']) ? (int) $range['Low'] : null,
'HighSign' => !empty($range['HighSign']) ? (int) $range['HighSign'] : null, 'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null,
'High' => !empty($range['High']) ? (int) $range['High'] : null, 'High' => !empty($range['High']) ? (int) $range['High'] : null,
'Flag' => $range['Flag'] ?? null, 'Flag' => $range['Flag'] ?? null,
'Display' => $index, 'Display' => $index,
@ -617,9 +510,6 @@ class TestsController extends BaseController
} }
} }
/**
* Save reftxt ranges for TEXT type
*/
private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID) private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID)
{ {
if ($action === 'update') { if ($action === 'update') {
@ -632,8 +522,8 @@ class TestsController extends BaseController
$this->modelRefTxt->insert([ $this->modelRefTxt->insert([
'TestSiteID' => $testSiteID, 'TestSiteID' => $testSiteID,
'SiteID' => $siteID, 'SiteID' => $siteID,
'TxtRefType' => (int) $range['TxtRefType'], 'TxtRefType' => $range['TxtRefType'],
'Sex' => (int) $range['Sex'], 'Sex' => $range['Sex'],
'AgeStart' => (int) ($range['AgeStart'] ?? 0), 'AgeStart' => (int) ($range['AgeStart'] ?? 0),
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
'RefTxt' => $range['RefTxt'] ?? '', 'RefTxt' => $range['RefTxt'] ?? '',
@ -643,9 +533,6 @@ class TestsController extends BaseController
} }
} }
/**
* Save calculation details for CALC type
*/
private function saveCalcDetails($testSiteID, $data, $action) private function saveCalcDetails($testSiteID, $data, $action)
{ {
$calcData = [ $calcData = [
@ -678,19 +565,14 @@ class TestsController extends BaseController
} }
} }
/**
* Save group details for GROUP type
*/
private function saveGroupDetails($testSiteID, $data, $input, $action) private function saveGroupDetails($testSiteID, $data, $input, $action)
{ {
if ($action === 'update') { if ($action === 'update') {
// Soft delete existing members
$this->db->table('testdefgrp') $this->db->table('testdefgrp')
->where('TestSiteID', $testSiteID) ->where('TestSiteID', $testSiteID)
->update(['EndDate' => date('Y-m-d H:i:s')]); ->update(['EndDate' => date('Y-m-d H:i:s')]);
} }
// Get members from details or input
$members = $data['members'] ?? ($input['Members'] ?? []); $members = $data['members'] ?? ($input['Members'] ?? []);
if (is_array($members)) { if (is_array($members)) {
@ -706,13 +588,9 @@ class TestsController extends BaseController
} }
} }
/**
* Save test mappings
*/
private function saveTestMap($testSiteID, $mappings, $action) private function saveTestMap($testSiteID, $mappings, $action)
{ {
if ($action === 'update') { if ($action === 'update') {
// Soft delete existing mappings
$this->db->table('testmap') $this->db->table('testmap')
->where('TestSiteID', $testSiteID) ->where('TestSiteID', $testSiteID)
->update(['EndDate' => date('Y-m-d H:i:s')]); ->update(['EndDate' => date('Y-m-d H:i:s')]);

View File

@ -3,87 +3,80 @@ namespace App\Controllers\ValueSet;
use CodeIgniter\API\ResponseTrait; use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController; use App\Controllers\BaseController;
use App\Models\ValueSet\ValueSetModel; use App\Libraries\ValueSet;
class ValueSetController extends BaseController { class ValueSetController extends BaseController {
use ResponseTrait; use ResponseTrait;
protected $db;
protected $model;
protected $rules;
public function __construct() {
$this->db = \Config\Database::connect();
$this->model = new ValueSetModel;
$this->rules = [
'VSetID' => 'required',
'VValue' => 'required',
];
}
public function index() { public function index() {
$param = $this->request->getVar('param'); $param = $this->request->getVar('param');
$VSetID = $this->request->getVar('VSetID');
if ($param) {
$data = $this->model->getValueSets($param, $VSetID); $all = ValueSet::getAll();
$filtered = array_filter($all, function($item) use ($param) {
return stripos($item['VSName'] ?? '', $param) !== false ||
stripos($item['name'] ?? '', $param) !== false;
});
return $this->respond([
'status' => 'success',
'data' => array_values($filtered)
], 200);
}
return $this->respond([
'status' => 'success',
'data' => ValueSet::getAll()
], 200);
}
public function showByName(string $name = null) {
if (!$name) {
return $this->respond([
'status' => 'error',
'message' => 'Name is required'
], 400);
}
$data = ValueSet::get($name);
if (!$data) {
return $this->respond([
'status' => 'error',
'message' => "ValueSet '$name' not found"
], 404);
}
return $this->respond([ return $this->respond([
'status' => 'success', 'status' => 'success',
'message'=> "Data fetched successfully",
'data' => $data 'data' => $data
], 200); ], 200);
} }
public function show($VID = null) {
$row = $this->model->getValueSet($VID);
if (empty($row)) {
return $this->respond([ 'status' => 'success', 'message' => "ValueSet with ID $VID not found.", 'data' => null ], 200);
}
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row], 200);
}
public function showByValueSetDef($VSetID = null) {
$rows = $this->model->getValueSetByValueSetDef($VSetID);
if (empty($rows)) {
return $this->respond([ 'status' => 'success', 'message' => "ValueSet not found.", 'data' => [] ], 200);
}
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
}
public function create() { public function create() {
$input = $this->request->getJSON(true); return $this->respond([
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } 'status' => 'error',
try { 'message' => 'CRUD operations on value sets are disabled. Edit JSON files directly.'
$VID = $this->model->insert($input); ], 403);
return $this->respondCreated([ 'status' => 'success', 'message' => "data $VID created successfully" ]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
} }
public function update() { public function update() {
$input = $this->request->getJSON(true); return $this->respond([
$VID = $input["VID"]; 'status' => 'error',
if (!$VID) { return $this->failValidationErrors('VID is required.'); } 'message' => 'CRUD operations on value sets are disabled. Edit JSON files directly.'
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors() ); } ], 403);
try {
$this->model->update($VID,$input);
return $this->respondCreated([ 'status' => 'success', 'message' => "data $VID updated successfully" ]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
} }
public function delete() { public function delete() {
$input = $this->request->getJSON(true); return $this->respond([
$VID = $input["VID"]; 'status' => 'error',
if (!$VID) { return $this->failValidationErrors('VID is required.'); } 'message' => 'CRUD operations on value sets are disabled. Edit JSON files directly.'
try { ], 403);
$this->model->delete($VID); }
return $this->respondDeleted(['status' => 'success', 'message' => "Data $VID deleted successfully."]);
} catch (\Throwable $e) { public function refresh() {
return $this->failServerError('Something went wrong: ' . $e->getMessage()); ValueSet::clearCache();
} return $this->respond([
'status' => 'success',
'message' => 'Cache cleared'
], 200);
} }
} }

View File

@ -0,0 +1,38 @@
<?php
namespace App\Controllers;
use App\Libraries\ValueSet;
class ValueSetApiController extends \CodeIgniter\Controller
{
use \CodeIgniter\API\ResponseTrait;
public function index(string $lookupName)
{
$data = ValueSet::getOptions($lookupName);
return $this->respond([
'status' => 'success',
'data' => $data
], 200);
}
public function all()
{
$dir = APPPATH . 'Libraries/Data/valuesets/';
$files = glob($dir . '*.json');
$result = [];
foreach ($files as $file) {
$name = basename($file, '.json');
if ($name[0] === '_') continue;
$result[] = [
'name' => $name,
'options' => ValueSet::getOptions($name)
];
}
return $this->respond([
'status' => 'success',
'data' => $result
], 200);
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class ValuesetVidToVvalue extends Migration
{
public function up()
{
$this->forge->modifyColumn('patient', [
'Gender' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Race' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Religion' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Ethnic' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'MaritalStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'DeathIndicator' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('testdefsite', [
'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
]);
$this->forge->modifyColumn('containerdef', [
'Additive' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'ConClass' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Color' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('location', [
'LocType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('workstation', [
'Type' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Enable' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('site', [
'SiteTypeID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'SiteClassID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('account', [
'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('refnum', [
'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'NumRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'RangeType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'LowSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'HighSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('reftxt', [
'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'TxtRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
$this->forge->modifyColumn('orderstatus', [
'OrderStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
]);
}
public function down()
{
$this->forge->modifyColumn('patient', [
'Gender' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Race' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Religion' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Ethnic' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'MaritalStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'DeathIndicator' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('testdefsite', [
'TestType' => ['type' => 'INT', 'null' => false],
]);
$this->forge->modifyColumn('containerdef', [
'Additive' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'ConClass' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Color' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('location', [
'LocType' => ['type' => 'INT', 'null' => true],
]);
$this->forge->modifyColumn('workstation', [
'Type' => ['type' => 'TINYINT', 'null' => true],
'Enable' => ['type' => 'INT', 'null' => true],
]);
$this->forge->modifyColumn('site', [
'SiteTypeID' => ['type' => 'INT', 'null' => true],
'SiteClassID' => ['type' => 'INT', 'null' => true],
]);
$this->forge->modifyColumn('account', [
'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('refnum', [
'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'NumRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'RangeType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'LowSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'HighSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('reftxt', [
'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'TxtRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('orderstatus', [
'OrderStatus' => ['type' => 'INT', 'null' => false],
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RenamePatientGenderToSex extends Migration {
public function up() {
$this->forge->modifyColumn('patient', [
'Gender' => ['name' => 'Sex', 'type' => 'INT', 'constraint' => 11, 'null' => true],
]);
}
public function down() {
$this->forge->modifyColumn('patient', [
'Sex' => ['name' => 'Gender', 'type' => 'INT', 'constraint' => 11, 'null' => true],
]);
}
}

View File

@ -88,7 +88,7 @@ class PatientSeeder extends Seeder
'NameLast' => 'Patient', 'NameLast' => 'Patient',
'Suffix' => 'S.Kom', 'Suffix' => 'S.Kom',
'NameAlias' => 'DummyTest', 'NameAlias' => 'DummyTest',
'Gender' => 5, 'Sex' => 5,
'PlaceOfBirth' => 'Jakarta', 'PlaceOfBirth' => 'Jakarta',
'Birthdate' => '1990-05-15', 'Birthdate' => '1990-05-15',
'Street_1' => 'Jl. Sudirman No. 123', 'Street_1' => 'Jl. Sudirman No. 123',
@ -126,7 +126,7 @@ class PatientSeeder extends Seeder
'NameLast' => 'Doe', 'NameLast' => 'Doe',
'Suffix' => null, 'Suffix' => null,
'NameAlias' => 'JaneDoe', 'NameAlias' => 'JaneDoe',
'Gender' => 6, // Female 'Sex' => 6, // Female
'PlaceOfBirth' => 'Bandung', 'PlaceOfBirth' => 'Bandung',
'Birthdate' => '1985-10-20', 'Birthdate' => '1985-10-20',
'Street_1' => 'Jl. Asia Afrika No. 456', 'Street_1' => 'Jl. Asia Afrika No. 456',
@ -164,7 +164,7 @@ class PatientSeeder extends Seeder
'NameLast' => 'Wijaya', 'NameLast' => 'Wijaya',
'Suffix' => null, 'Suffix' => null,
'NameAlias' => 'BudiW', 'NameAlias' => 'BudiW',
'Gender' => 5, 'Sex' => 5,
'PlaceOfBirth' => 'Surabaya', 'PlaceOfBirth' => 'Surabaya',
'Birthdate' => '2000-01-01', 'Birthdate' => '2000-01-01',
'Street_1' => 'Jl. Pahlawan No. 789', 'Street_1' => 'Jl. Pahlawan No. 789',

View File

@ -0,0 +1,54 @@
{
"version": "1.0.0",
"generated": "2026-01-12",
"description": "Value Set Definitions - Static lookup values for CLQMS",
"valuesets": [
{"file": "ws_type.json", "VSetID": 1, "VSName": "Workstation Type"},
{"file": "enable_disable.json", "VSetID": 2, "VSName": "Enable/Disable"},
{"file": "gender.json", "VSetID": 3, "VSName": "Gender"},
{"file": "marital_status.json", "VSetID": 4, "VSName": "Marital Status"},
{"file": "death_indicator.json", "VSetID": 5, "VSName": "Death Indicator"},
{"file": "identifier_type.json", "VSetID": 6, "VSName": "Identifier Type"},
{"file": "operation.json", "VSetID": 7, "VSName": "Operation (CRUD)"},
{"file": "did_type.json", "VSetID": 8, "VSName": "DID Type"},
{"file": "requested_entity.json", "VSetID": 9, "VSName": "Requested Entity"},
{"file": "order_priority.json", "VSetID": 10, "VSName": "Order Priority"},
{"file": "order_status.json", "VSetID": 11, "VSName": "Order Status"},
{"file": "location_type.json", "VSetID": 12, "VSName": "Location Type"},
{"file": "additive.json", "VSetID": 13, "VSName": "Additive"},
{"file": "container_class.json", "VSetID": 14, "VSName": "Container Class"},
{"file": "specimen_type.json", "VSetID": 15, "VSName": "Specimen Type"},
{"file": "unit.json", "VSetID": 16, "VSName": "Unit"},
{"file": "generate_by.json", "VSetID": 17, "VSName": "Generate By"},
{"file": "specimen_activity.json", "VSetID": 18, "VSName": "Specimen Activity"},
{"file": "activity_result.json", "VSetID": 19, "VSName": "Activity Result"},
{"file": "specimen_status.json", "VSetID": 20, "VSName": "Specimen Status"},
{"file": "specimen_condition.json", "VSetID": 21, "VSName": "Specimen Condition"},
{"file": "specimen_role.json", "VSetID": 22, "VSName": "Specimen Role"},
{"file": "collection_method.json", "VSetID": 23, "VSName": "Collection Method"},
{"file": "body_site.json", "VSetID": 24, "VSName": "Body Site"},
{"file": "container_size.json", "VSetID": 25, "VSName": "Container Size"},
{"file": "fasting_status.json", "VSetID": 26, "VSName": "Fasting Status"},
{"file": "test_type.json", "VSetID": 27, "VSName": "Test Type"},
{"file": "result_unit.json", "VSetID": 28, "VSName": "Result Unit"},
{"file": "formula_language.json", "VSetID": 29, "VSName": "Formula Language"},
{"file": "race.json", "VSetID": 30, "VSName": "Race (Ethnicity)"},
{"file": "religion.json", "VSetID": 31, "VSName": "Religion"},
{"file": "ethnic.json", "VSetID": 32, "VSName": "Ethnic"},
{"file": "country.json", "VSetID": 33, "VSName": "Country"},
{"file": "container_cap_color.json", "VSetID": 34, "VSName": "Container Cap Color"},
{"file": "test_activity.json", "VSetID": 35, "VSName": "Test Activity"},
{"file": "adt_event.json", "VSetID": 36, "VSName": "ADT Event"},
{"file": "site_type.json", "VSetID": 37, "VSName": "Site Type"},
{"file": "site_class.json", "VSetID": 38, "VSName": "Site Class"},
{"file": "entity_type.json", "VSetID": 39, "VSName": "Entity Type"},
{"file": "area_class.json", "VSetID": 40, "VSName": "Area Class"},
{"file": "math_sign.json", "VSetID": 41, "VSName": "Math Sign"},
{"file": "v_category.json", "VSetID": 42, "VSName": "VCategory"},
{"file": "result_type.json", "VSetID": 43, "VSName": "Result Type"},
{"file": "reference_type.json", "VSetID": 44, "VSName": "Reference Type"},
{"file": "range_type.json", "VSetID": 45, "VSName": "Range Type"},
{"file": "numeric_ref_type.json", "VSetID": 46, "VSName": "Numeric Reference Type"},
{"file": "text_ref_type.json", "VSetID": 47, "VSName": "Text Reference Type"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 19,
"name": "activity_result",
"VSName": "Activity Result",
"VCategory": "User-defined",
"values": [
{"key": "0", "value": "Failed"},
{"key": "1", "value": "Success with note"},
{"key": "2", "value": "Success"}
]
}

View File

@ -0,0 +1,27 @@
{
"VSetID": 13,
"name": "additive",
"VSName": "Additive",
"VCategory": "User-defined",
"values": [
{"key": "Hep", "value": "Heparin ammonium"},
{"key": "Apro", "value": "Aprotinin"},
{"key": "HepCa", "value": "Heparin calcium"},
{"key": "H3BO3", "value": "Boric acid"},
{"key": "CaOxa", "value": "Calcium oxalate"},
{"key": "EDTA", "value": "EDTA"},
{"key": "Ede", "value": "Edetate"},
{"key": "HCl", "value": "Hydrochloric acid"},
{"key": "Hrdn", "value": "Hirudin"},
{"key": "EdeK", "value": "Edetate dipotassium"},
{"key": "EdeTri", "value": "Tripotassium edetate"},
{"key": "LiHep", "value": "Heparin lithium"},
{"key": "EdeNa", "value": "Edetate disodium"},
{"key": "NaCtrt", "value": "Sodium citrate"},
{"key": "NaHep", "value": "Heparin sodium"},
{"key": "NaF", "value": "Sodium fluoride"},
{"key": "Borax", "value": "Sodium tetraborate"},
{"key": "Mntl", "value": "Mannitol"},
{"key": "NaFrm", "value": "Sodium formate"}
]
}

View File

@ -0,0 +1,21 @@
{
"VSetID": 36,
"name": "adt_event",
"VSName": "ADT Event",
"VCategory": "User-defined",
"values": [
{"key": "A01", "value": "Admit"},
{"key": "A02", "value": "Transfer"},
{"key": "A03", "value": "Discharge"},
{"key": "A04", "value": "Register"},
{"key": "A08", "value": "Update patient information"},
{"key": "A11", "value": "Cancel admit"},
{"key": "A12", "value": "Cancel transfer"},
{"key": "A13", "value": "Cancel discharge"},
{"key": "A23", "value": "Delete patient record"},
{"key": "A24", "value": "Link patient information"},
{"key": "A37", "value": "Unlink patient information"},
{"key": "A54", "value": "Change attending doctor"},
{"key": "A61", "value": "Change consulting doctor"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 40,
"name": "area_class",
"VSName": "Area Class",
"VCategory": "User-defined",
"values": [
{"key": "PROP", "value": "Propinsi"},
{"key": "KAB", "value": "Kabupaten"},
{"key": "KOTA", "value": "Kota"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 24,
"name": "body_site",
"VSName": "Body Site",
"VCategory": "User-defined",
"values": [
{"key": "LA", "value": "Left Arm"},
{"key": "RA", "value": "Right Arm"},
{"key": "LF", "value": "Left Foot"},
{"key": "RF", "value": "Right Foot"}
]
}

View File

@ -0,0 +1,18 @@
{
"VSetID": 23,
"name": "collection_method",
"VSName": "Collection Method",
"VCategory": "User-defined",
"values": [
{"key": "pcntr", "value": "Puncture"},
{"key": "fprk", "value": "Finger-prick sampling"},
{"key": "ucct", "value": "Urine specimen collection, clean catch"},
{"key": "utcl", "value": "Timed urine collection"},
{"key": "ucth", "value": "Urine specimen collection, catheterized"},
{"key": "scgh", "value": "Collection of coughed sputum"},
{"key": "bpsy", "value": "Biopsy"},
{"key": "aspn", "value": "Aspiration"},
{"key": "excs", "value": "Excision"},
{"key": "scrp", "value": "Scraping"}
]
}

View File

@ -0,0 +1,16 @@
{
"VSetID": 34,
"name": "container_cap_color",
"VSName": "Container Cap Color",
"VCategory": "User-defined",
"values": [
{"key": "PRPL", "value": "Purple"},
{"key": "RED", "value": "Red"},
{"key": "YLLW", "value": "Yellow"},
{"key": "GRN", "value": "Green"},
{"key": "PINK", "value": "Pink"},
{"key": "LBLU", "value": "Light Blue"},
{"key": "RBLU", "value": "Royal Blue"},
{"key": "GRAY", "value": "Gray"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 14,
"name": "container_class",
"VSName": "Container Class",
"VCategory": "User-defined",
"values": [
{"key": "Pri", "value": "Primary"},
{"key": "Sec", "value": "Secondary"},
{"key": "Ter", "value": "Tertiary"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 25,
"name": "container_size",
"VSName": "Container Size",
"VCategory": "User-defined",
"values": [
{"key": "5ml", "value": "5 mL"},
{"key": "7ml", "value": "7 mL"},
{"key": "10ml", "value": "10 mL"},
{"key": "1l", "value": "1 L"}
]
}

View File

@ -0,0 +1,26 @@
{
"VSetID": 33,
"name": "country",
"VSName": "Country",
"VCategory": "User-defined",
"values": [
{"key": "ID", "value": "Indonesia"},
{"key": "US", "value": "United States"},
{"key": "MY", "value": "Malaysia"},
{"key": "SG", "value": "Singapore"},
{"key": "AU", "value": "Australia"},
{"key": "JP", "value": "Japan"},
{"key": "KR", "value": "South Korea"},
{"key": "CN", "value": "China"},
{"key": "IN", "value": "India"},
{"key": "TH", "value": "Thailand"},
{"key": "PH", "value": "Philippines"},
{"key": "VN", "value": "Vietnam"},
{"key": "GB", "value": "United Kingdom"},
{"key": "DE", "value": "Germany"},
{"key": "FR", "value": "France"},
{"key": "NL", "value": "Netherlands"},
{"key": "CA", "value": "Canada"},
{"key": "NZ", "value": "New Zealand"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 5,
"name": "death_indicator",
"VSName": "Death Indicator",
"VCategory": "System",
"values": [
{"key": "Y", "value": "Death"},
{"key": "N", "value": "Life"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 8,
"name": "did_type",
"VSName": "DID Type",
"VCategory": "System",
"values": [
{"key": "WDID", "value": "Windows Device ID"},
{"key": "AAID", "value": "Android AAID"},
{"key": "IDFA", "value": "iOS IDFA"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 2,
"name": "enable_disable",
"VSName": "Enable/Disable",
"VCategory": "System",
"values": [
{"key": "0", "value": "Disabled"},
{"key": "1", "value": "Enabled"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 39,
"name": "entity_type",
"VSName": "Entity Type",
"VCategory": "System",
"values": [
{"key": "HIS", "value": "HIS"},
{"key": "SITE", "value": "Site"},
{"key": "WST", "value": "Workstation"},
{"key": "INST", "value": "Equipment/Instrument"}
]
}

View File

@ -0,0 +1,16 @@
{
"VSetID": 32,
"name": "ethnic",
"VSName": "Ethnic",
"VCategory": "User-defined",
"values": [
{"key": "PPMLN", "value": "Papua Melanezoid"},
{"key": "NGRID", "value": "Negroid"},
{"key": "WDOID", "value": "Weddoid"},
{"key": "MMPM", "value": "Melayu Mongoloid_Proto Melayu"},
{"key": "MMDM", "value": "Melayu Mongoloid_Deutro Melayu"},
{"key": "TNGHA", "value": "Tionghoa"},
{"key": "INDIA", "value": "India"},
{"key": "ARAB", "value": "Arab"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 26,
"name": "fasting_status",
"VSName": "Fasting Status",
"VCategory": "User-defined",
"values": [
{"key": "F", "value": "Fasting"},
{"key": "NF", "value": "Not Fasting"},
{"key": "NG", "value": "Not Given"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 29,
"name": "formula_language",
"VSName": "Formula Language",
"VCategory": "User-defined",
"values": [
{"key": "Phyton", "value": "Phyton"},
{"key": "CQL", "value": "Clinical Quality Language"},
{"key": "FHIRP", "value": "FHIRPath"},
{"key": "SQL", "value": "SQL"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 3,
"name": "gender",
"VSName": "Gender",
"VCategory": "User-defined",
"values": [
{"key": "1", "value": "Female"},
{"key": "2", "value": "Male"},
{"key": "3", "value": "Unknown"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 17,
"name": "generate_by",
"VSName": "Generate By",
"VCategory": "System",
"values": [
{"key": "order", "value": "Generate by order"},
{"key": "user", "value": "Generate by user"}
]
}

View File

@ -0,0 +1,13 @@
{
"VSetID": 6,
"name": "identifier_type",
"VSName": "Identifier Type",
"VCategory": "User-defined",
"values": [
{"key": "KTP", "value": "Kartu Tanda Penduduk"},
{"key": "PASS", "value": "Passport"},
{"key": "SSN", "value": "Social Security Number"},
{"key": "SIM", "value": "Surat Izin Mengemudi"},
{"key": "KTAS", "value": "Kartu Izin Tinggal Terbatas"}
]
}

View File

@ -0,0 +1,16 @@
{
"VSetID": 12,
"name": "location_type",
"VSName": "Location Type",
"VCategory": "User-defined",
"values": [
{"key": "FCLT", "value": "Facility"},
{"key": "BLDG", "value": "Building"},
{"key": "FLOR", "value": "Floor"},
{"key": "POC", "value": "Point of Care"},
{"key": "ROOM", "value": "Room"},
{"key": "BED", "value": "Bed"},
{"key": "MOBL", "value": "Mobile"},
{"key": "REMT", "value": "Remote"}
]
}

View File

@ -0,0 +1,16 @@
{
"VSetID": 4,
"name": "marital_status",
"VSName": "Marital Status",
"VCategory": "User-defined",
"values": [
{"key": "A", "value": "Separated"},
{"key": "D", "value": "Divorced"},
{"key": "M", "value": "Married"},
{"key": "S", "value": "Single"},
{"key": "W", "value": "Widowed"},
{"key": "B", "value": "Unmarried"},
{"key": "U", "value": "Unknown"},
{"key": "O", "value": "Other"}
]
}

View File

@ -0,0 +1,13 @@
{
"VSetID": 41,
"name": "math_sign",
"VSName": "Math Sign",
"VCategory": "User-defined",
"values": [
{"key": "=", "value": "Equal"},
{"key": "<", "value": "Less than"},
{"key": ">", "value": "Greater than"},
{"key": "<=", "value": "Less than or equal to"},
{"key": ">=", "value": "Greater than or equal to"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 46,
"name": "numeric_ref_type",
"VSName": "Numeric Reference Type",
"VCategory": "User-defined",
"values": [
{"key": "RANGE", "value": "Range"},
{"key": "THOLD", "value": "Threshold"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 7,
"name": "operation",
"VSName": "Operation (CRUD)",
"VCategory": "System",
"values": [
{"key": "Create", "value": "Create record"},
{"key": "Read", "value": "Read record/field"},
{"key": "Update", "value": "Update record/field"},
{"key": "Delete", "value": "Delete record/field"}
]
}

View File

@ -0,0 +1,15 @@
{
"VSetID": 10,
"name": "order_priority",
"VSName": "Order Priority",
"VCategory": "User-defined",
"values": [
{"key": "S", "value": "Stat"},
{"key": "A", "value": "ASAP"},
{"key": "R", "value": "Routine"},
{"key": "P", "value": "Preop"},
{"key": "C", "value": "Callback"},
{"key": "T", "value": "Timing critical"},
{"key": "PRN", "value": "As needed"}
]
}

View File

@ -0,0 +1,20 @@
{
"VSetID": 11,
"name": "order_status",
"VSName": "Order Status",
"VCategory": "User-defined",
"values": [
{"key": "A", "value": "Some, not all results available"},
{"key": "CA", "value": "Order is cancelled"},
{"key": "CM", "value": "Order is completed"},
{"key": "DC", "value": "Order was discontinued"},
{"key": "ER", "value": "Error, order not found"},
{"key": "HD", "value": "Order on hold"},
{"key": "IP", "value": "In process, unspecified"},
{"key": "RP", "value": "Order has been replaced"},
{"key": "SC", "value": "In process, scheduled"},
{"key": "CL", "value": "Closed"},
{"key": "AC", "value": "Archived"},
{"key": "DL", "value": "Deleted"}
]
}

View File

@ -0,0 +1,15 @@
{
"VSetID": 10,
"name": "priority",
"VSName": "Priority",
"VCategory": "User-defined",
"values": [
{"key": "S", "value": "Stat"},
{"key": "A", "value": "ASAP"},
{"key": "R", "value": "Routine"},
{"key": "P", "value": "Preop"},
{"key": "C", "value": "Callback"},
{"key": "T", "value": "Timing critical"},
{"key": "PRN", "value": "As needed"}
]
}

View File

@ -0,0 +1,39 @@
{
"VSetID": 30,
"name": "race",
"VSName": "Race (Ethnicity)",
"VCategory": "User-defined",
"values": [
{"key": "JAWA", "value": "Jawa"},
{"key": "SUNDA", "value": "Sunda"},
{"key": "BATAK", "value": "Batak"},
{"key": "SULOR", "value": "Suku asal Sulawesi lainnya"},
{"key": "MDRA", "value": "Madura"},
{"key": "BTWI", "value": "Betawi"},
{"key": "MNG", "value": "Minangkabau"},
{"key": "BUGIS", "value": "Bugis"},
{"key": "MLYU", "value": "Melayu"},
{"key": "SUMSL", "value": "Suku asal Sumatera Selatan"},
{"key": "BTNOR", "value": "Suku asal Banten"},
{"key": "NTTOR", "value": "Suku asal Nusa Tenggara Timur"},
{"key": "BNJAR", "value": "Banjar"},
{"key": "ACEH", "value": "Aceh"},
{"key": "BALI", "value": "Bali"},
{"key": "SASAK", "value": "Sasak"},
{"key": "DAYAK", "value": "Dayak"},
{"key": "TNGHA", "value": "Tionghoa"},
{"key": "PPAOR", "value": "Suku asal Papua"},
{"key": "MKSSR", "value": "Makassar"},
{"key": "SUMOR", "value": "Suku asal Sumatera lainnya"},
{"key": "MLKOR", "value": "Suku asal Maluku"},
{"key": "KLMOR", "value": "Suku asal Kalimantan lainnya"},
{"key": "CRBON", "value": "Cirebon"},
{"key": "JBIOR", "value": "Suku asal Jambi"},
{"key": "LPGOR", "value": "Suku Lampung"},
{"key": "NTBOR", "value": "Suku asal Nusa Tenggara Barat lainnya"},
{"key": "GRTLO", "value": "Gorontalo"},
{"key": "MNHSA", "value": "Minahasa"},
{"key": "NIAS", "value": "Nias"},
{"key": "FORGN", "value": "Asing/luar negeri"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 45,
"name": "range_type",
"VSName": "Range Type",
"VCategory": "User-defined",
"values": [
{"key": "REF", "value": "Reference Range"},
{"key": "CRTC", "value": "Critical Range"},
{"key": "VAL", "value": "Validation Range"},
{"key": "RERUN", "value": "Rerun Range"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 44,
"name": "reference_type",
"VSName": "Reference Type",
"VCategory": "User-defined",
"values": [
{"key": "NMRC", "value": "Numeric"},
{"key": "TEXT", "value": "Text"}
]
}

View File

@ -0,0 +1,15 @@
{
"VSetID": 31,
"name": "religion",
"VSName": "Religion",
"VCategory": "User-defined",
"values": [
{"key": "ISLAM", "value": "Islam"},
{"key": "KRSTN", "value": "Kristen"},
{"key": "KTLIK", "value": "Katolik"},
{"key": "HINDU", "value": "Hindu"},
{"key": "BUDHA", "value": "Budha"},
{"key": "KHCU", "value": "Khong Hu Cu"},
{"key": "OTHER", "value": "Lainnya"}
]
}

View File

@ -0,0 +1,28 @@
{
"VSetID": 20,
"name": "request_status",
"VSName": "Request Status",
"VCategory": "User-defined",
"values": [
{"key": "STC", "value": "To be collected"},
{"key": "SCFld", "value": "Collection failed"},
{"key": "SCtd", "value": "Collected"},
{"key": "STran", "value": "In-transport"},
{"key": "STFld", "value": "Transport failed"},
{"key": "SArrv", "value": "Arrived"},
{"key": "SRejc", "value": "Rejected"},
{"key": "SRcvd", "value": "Received"},
{"key": "SPAna", "value": "Pre-analytical"},
{"key": "SPAF", "value": "Pre-analytical failed"},
{"key": "STA", "value": "To be analyze"},
{"key": "SAFld", "value": "Analytical failed"},
{"key": "SAna", "value": "Analytical"},
{"key": "STS", "value": "To be stored"},
{"key": "SSFld", "value": "Store failed"},
{"key": "SStrd", "value": "Stored"},
{"key": "SExp", "value": "Expired"},
{"key": "STD", "value": "To be destroyed"},
{"key": "SDFld", "value": "Failed to destroy"},
{"key": "SDstd", "value": "Destroyed"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 9,
"name": "requested_entity",
"VSName": "Requested Entity",
"VCategory": "System",
"values": [
{"key": "PAT", "value": "Patient"},
{"key": "ISN", "value": "Insurance"},
{"key": "ACC", "value": "Account"},
{"key": "DOC", "value": "Doctor"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 99,
"name": "result_status",
"VSName": "Result Status",
"VCategory": "User-defined",
"values": [
{"key": "PRELIMINARY", "value": "Preliminary"},
{"key": "FINAL", "value": "Final"},
{"key": "CORRECTED", "value": "Corrected"},
{"key": "CANCELLED", "value": "Cancelled"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 43,
"name": "result_type",
"VSName": "Result Type",
"VCategory": "User-defined",
"values": [
{"key": "NMRIC", "value": "Numeric"},
{"key": "RANGE", "value": "Range"},
{"key": "TEXT", "value": "Text"},
{"key": "VSET", "value": "Value set"}
]
}

View File

@ -0,0 +1,18 @@
{
"VSetID": 28,
"name": "result_unit",
"VSName": "Result Unit",
"VCategory": "User-defined",
"values": [
{"key": "g/dL", "value": "g/dL"},
{"key": "g/L", "value": "g/L"},
{"key": "mg/dL", "value": "mg/dL"},
{"key": "mg/L", "value": "mg/L"},
{"key": "L/L", "value": "L/L"},
{"key": "x106/mL", "value": "x106/mL"},
{"key": "x1012/L", "value": "x1012/L"},
{"key": "fL", "value": "fL"},
{"key": "pg", "value": "pg"},
{"key": "x109/L", "value": "x109/L"}
]
}

View File

@ -0,0 +1,14 @@
{
"VSetID": 38,
"name": "site_class",
"VSName": "Site Class",
"VCategory": "User-defined",
"values": [
{"key": "A", "value": "Kelas A"},
{"key": "B", "value": "Kelas B"},
{"key": "C", "value": "Kelas C"},
{"key": "D", "value": "Kelas D"},
{"key": "Utm", "value": "Utama"},
{"key": "Ptm", "value": "Pratama"}
]
}

View File

@ -0,0 +1,14 @@
{
"VSetID": 37,
"name": "site_type",
"VSName": "Site Type",
"VCategory": "User-defined",
"values": [
{"key": "GH", "value": "Government Hospital"},
{"key": "PH", "value": "Private Hospital"},
{"key": "GHL", "value": "Government Hospital Lab"},
{"key": "PHL", "value": "Private Hospital Lab"},
{"key": "GL", "value": "Government Lab"},
{"key": "PL", "value": "Private Lab"}
]
}

View File

@ -0,0 +1,15 @@
{
"VSetID": 18,
"name": "specimen_activity",
"VSName": "Specimen Activity",
"VCategory": "User-defined",
"values": [
{"key": "SColl", "value": "Collection"},
{"key": "STran", "value": "Transport"},
{"key": "SRec", "value": "Reception"},
{"key": "SPrep", "value": "Preparation"},
{"key": "SAlqt", "value": "Aliquot"},
{"key": "SDisp", "value": "Dispatching"},
{"key": "SDest", "value": "Destruction"}
]
}

View File

@ -0,0 +1,19 @@
{
"VSetID": 21,
"name": "specimen_condition",
"VSName": "Specimen Condition",
"VCategory": "User-defined",
"values": [
{"key": "HEM", "value": "Hemolyzed"},
{"key": "ITC", "value": "Icteric"},
{"key": "LIP", "value": "Lipemic"},
{"key": "CFU", "value": "Centrifuged"},
{"key": "ROOM", "value": "Room temperature"},
{"key": "COOL", "value": "Cool"},
{"key": "FROZ", "value": "Frozen"},
{"key": "CLOT", "value": "Clotted"},
{"key": "AUT", "value": "Autolyzed"},
{"key": "CON", "value": "Contaminated"},
{"key": "LIVE", "value": "Live"}
]
}

View File

@ -0,0 +1,17 @@
{
"VSetID": 22,
"name": "specimen_role",
"VSName": "Specimen Role",
"VCategory": "User-defined",
"values": [
{"key": "P", "value": "Patient"},
{"key": "B", "value": "Blind Sample"},
{"key": "Q", "value": "Control specimen"},
{"key": "E", "value": "Electronic QC"},
{"key": "F", "value": "Filler Organization Proficiency"},
{"key": "O", "value": "Operator Proficiency"},
{"key": "C", "value": "Calibrator"},
{"key": "R", "value": "Replicate"},
{"key": "V", "value": "Verifying Calibrator"}
]
}

View File

@ -0,0 +1,28 @@
{
"VSetID": 20,
"name": "specimen_status",
"VSName": "Specimen Status",
"VCategory": "User-defined",
"values": [
{"key": "STC", "value": "To be collected"},
{"key": "SCFld", "value": "Collection failed"},
{"key": "SCtd", "value": "Collected"},
{"key": "STran", "value": "In-transport"},
{"key": "STFld", "value": "Transport failed"},
{"key": "SArrv", "value": "Arrived"},
{"key": "SRejc", "value": "Rejected"},
{"key": "SRcvd", "value": "Received"},
{"key": "SPAna", "value": "Pre-analytical"},
{"key": "SPAF", "value": "Pre-analytical failed"},
{"key": "STA", "value": "To be analyze"},
{"key": "SAFld", "value": "Analytical failed"},
{"key": "SAna", "value": "Analytical"},
{"key": "STS", "value": "To be stored"},
{"key": "SSFld", "value": "Store failed"},
{"key": "SStrd", "value": "Stored"},
{"key": "SExp", "value": "Expired"},
{"key": "STD", "value": "To be destroyed"},
{"key": "SDFld", "value": "Failed to destroy"},
{"key": "SDstd", "value": "Destroyed"}
]
}

View File

@ -0,0 +1,23 @@
{
"VSetID": 15,
"name": "specimen_type",
"VSName": "Specimen Type",
"VCategory": "User-defined",
"values": [
{"key": "BLD", "value": "Whole blood"},
{"key": "BLDA", "value": "Blood arterial"},
{"key": "BLDCO", "value": "Cord blood"},
{"key": "FBLOOD", "value": "Blood, Fetal"},
{"key": "CSF", "value": "Cerebral spinal fluid"},
{"key": "WB", "value": "Blood, Whole"},
{"key": "BBL", "value": "Blood bag"},
{"key": "SER", "value": "Serum"},
{"key": "PLAS", "value": "Plasma"},
{"key": "PLB", "value": "Plasma bag"},
{"key": "MUCOS", "value": "Mucosa"},
{"key": "MUCUS", "value": "Mucus"},
{"key": "UR", "value": "Urine"},
{"key": "RANDU", "value": "Urine, Random"},
{"key": "URINM", "value": "Urine, Midstream"}
]
}

View File

@ -0,0 +1,13 @@
{
"VSetID": 35,
"name": "test_activity",
"VSName": "Test Activity",
"VCategory": "User-defined",
"values": [
{"key": "ORD", "value": "Order"},
{"key": "ANA", "value": "Analyse"},
{"key": "VER", "value": "Result Verification/Technical Validation"},
{"key": "REV", "value": "Clinical Review/Clinical Validation"},
{"key": "REP", "value": "Reporting"}
]
}

View File

@ -0,0 +1,12 @@
{
"VSetID": 99,
"name": "test_status",
"VSName": "Test Status",
"VCategory": "User-defined",
"values": [
{"key": "PENDING", "value": "Waiting for Results"},
{"key": "IN_PROCESS", "value": "Analyzing"},
{"key": "VERIFIED", "value": "Verified & Signed"},
{"key": "REJECTED", "value": "Sample Rejected"}
]
}

View File

@ -0,0 +1,13 @@
{
"VSetID": 27,
"name": "test_type",
"VSName": "Test Type",
"VCategory": "User-defined",
"values": [
{"key": "TEST", "value": "Test"},
{"key": "PARAM", "value": "Parameter"},
{"key": "CALC", "value": "Calculated Test"},
{"key": "GROUP", "value": "Group Test"},
{"key": "TITLE", "value": "Title"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 47,
"name": "text_ref_type",
"VSName": "Text Reference Type",
"VCategory": "User-defined",
"values": [
{"key": "VSET", "value": "Value Set"},
{"key": "TEXT", "value": "Text"}
]
}

View File

@ -0,0 +1,11 @@
{
"VSetID": 16,
"name": "unit",
"VSName": "Unit",
"VCategory": "User-defined",
"values": [
{"key": "L", "value": "Liter"},
{"key": "mL", "value": "Mili Liter"},
{"key": "Pcs", "value": "Pieces"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 42,
"name": "v_category",
"VSName": "VCategory",
"VCategory": "System",
"values": [
{"key": "0", "value": "System"},
{"key": "1", "value": "User-defined"}
]
}

View File

@ -0,0 +1,10 @@
{
"VSetID": 1,
"name": "ws_type",
"VSName": "Workstation Type",
"VCategory": "System",
"values": [
{"key": "0", "value": "Primary"},
{"key": "1", "value": "Secondary"}
]
}

View File

@ -1,649 +1,5 @@
<?php <?php
namespace App\Libraries; namespace App\Libraries;
/** class Lookups extends ValueSet {
* Static Lookup Values
* All predefined lookup values stored as constants for easy access.
* No database queries - all data is hardcoded.
* Based on valuesetdef and valueset tables.
*/
class Lookups {
// VSetID 1: Workstation Type
const WS_TYPE = [
'0' => 'Primary',
'1' => 'Secondary'
];
// VSetID 2: Enable/Disable
const ENABLE_DISABLE = [
'0' => 'Disabled',
'1' => 'Enabled'
];
// VSetID 3: Gender
const GENDER = [
'1' => 'Female',
'2' => 'Male',
'3' => 'Unknown'
];
// VSetID 4: Marital Status
const MARITAL_STATUS = [
'A' => 'Separated',
'D' => 'Divorced',
'M' => 'Married',
'S' => 'Single',
'W' => 'Widowed',
'B' => 'Unmarried',
'U' => 'Unknown',
'O' => 'Other'
];
// VSetID 5: Death Indicator
const DEATH_INDICATOR = [
'Y' => 'Death',
'N' => 'Life'
];
// VSetID 6: Identifier Type
const IDENTIFIER_TYPE = [
'KTP' => 'Kartu Tanda Penduduk',
'PASS' => 'Passport',
'SSN' => 'Social Security Number',
'SIM' => 'Surat Izin Mengemudi',
'KTAS' => 'Kartu Izin Tinggal Terbatas'
];
// VSetID 7: Operation (CRUD)
const OPERATION = [
'Create' => 'Create record',
'Read' => 'Read record/field',
'Update' => 'Update record/field',
'Delete' => 'Delete record/field'
];
// VSetID 8: DID Type
const DID_TYPE = [
'WDID' => 'Windows Device ID',
'AAID' => 'Android AAID',
'IDFA' => 'iOS IDFA'
];
// VSetID 9: Requested Entity
const REQUESTED_ENTITY = [
'PAT' => 'Patient',
'ISN' => 'Insurance',
'ACC' => 'Account',
'DOC' => 'Doctor'
];
// VSetID 10: Order Priority
const ORDER_PRIORITY = [
'S' => 'Stat',
'A' => 'ASAP',
'R' => 'Routine',
'P' => 'Preop',
'C' => 'Callback',
'T' => 'Timing critical',
'PRN' => 'As needed'
];
// VSetID 11: Order Status
const ORDER_STATUS = [
'A' => 'Some, not all results available',
'CA' => 'Order is cancelled',
'CM' => 'Order is completed',
'DC' => 'Order was discontinued',
'ER' => 'Error, order not found',
'HD' => 'Order on hold',
'IP' => 'In process, unspecified',
'RP' => 'Order has been replaced',
'SC' => 'In process, scheduled',
'CL' => 'Closed',
'AC' => 'Archived',
'DL' => 'Deleted'
];
// VSetID 12: Location Type
const LOCATION_TYPE = [
'FCLT' => 'Facility',
'BLDG' => 'Building',
'FLOR' => 'Floor',
'POC' => 'Point of Care',
'ROOM' => 'Room',
'BED' => 'Bed',
'MOBL' => 'Mobile',
'REMT' => 'Remote'
];
// VSetID 13: Additive
const ADDITIVE = [
'Hep' => 'Heparin ammonium',
'Apro' => 'Aprotinin',
'HepCa' => 'Heparin calcium',
'H3BO3' => 'Boric acid',
'CaOxa' => 'Calcium oxalate',
'EDTA' => 'EDTA',
'Ede' => 'Edetate',
'HCl' => 'Hydrochloric acid',
'Hrdn' => 'Hirudin',
'EdeK' => 'Edetate dipotassium',
'EdeTri' => 'Tripotassium edetate',
'LiHep' => 'Heparin lithium',
'EdeNa' => 'Edetate disodium',
'NaCtrt' => 'Sodium citrate',
'NaHep' => 'Heparin sodium',
'NaF' => 'Sodium fluoride',
'Borax' => 'Sodium tetraborate',
'Mntl' => 'Mannitol',
'NaFrm' => 'Sodium formate'
];
// VSetID 14: Container Class
const CONTAINER_CLASS = [
'Pri' => 'Primary',
'Sec' => 'Secondary',
'Ter' => 'Tertiary'
];
// VSetID 15: Specimen Type
const SPECIMEN_TYPE = [
'BLD' => 'Whole blood',
'BLDA' => 'Blood arterial',
'BLDCO' => 'Cord blood',
'FBLOOD' => 'Blood, Fetal',
'CSF' => 'Cerebral spinal fluid',
'WB' => 'Blood, Whole',
'BBL' => 'Blood bag',
'SER' => 'Serum',
'PLAS' => 'Plasma',
'PLB' => 'Plasma bag',
'MUCOS' => 'Mucosa',
'MUCUS' => 'Mucus',
'UR' => 'Urine',
'RANDU' => 'Urine, Random',
'URINM' => 'Urine, Midstream'
];
// VSetID 16: Unit
const UNIT = [
'L' => 'Liter',
'mL' => 'Mili Liter',
'Pcs' => 'Pieces'
];
// VSetID 17: Generate By
const GENERATE_BY = [
'order' => 'Generate by order',
'user' => 'Generate by user'
];
// VSetID 18: Specimen Activity
const SPECIMEN_ACTIVITY = [
'SColl' => 'Collection',
'STran' => 'Transport',
'SRec' => 'Reception',
'SPrep' => 'Preparation',
'SAlqt' => 'Aliquot',
'SDisp' => 'Dispatching',
'SDest' => 'Destruction'
];
// VSetID 19: Activity Result
const ACTIVITY_RESULT = [
'0' => 'Failed',
'1' => 'Success with note',
'2' => 'Success'
];
// VSetID 20: Specimen Status
const SPECIMEN_STATUS = [
'STC' => 'To be collected',
'SCFld' => 'Collection failed',
'SCtd' => 'Collected',
'STran' => 'In-transport',
'STFld' => 'Transport failed',
'SArrv' => 'Arrived',
'SRejc' => 'Rejected',
'SRcvd' => 'Received',
'SPAna' => 'Pre-analytical',
'SPAF' => 'Pre-analytical failed',
'STA' => 'To be analyze',
'SAFld' => 'Analytical failed',
'SAna' => 'Analytical',
'STS' => 'To be stored',
'SSFld' => 'Store failed',
'SStrd' => 'Stored',
'SExp' => 'Expired',
'STD' => 'To be destroyed',
'SDFld' => 'Failed to destroy',
'SDstd' => 'Destroyed'
];
// VSetID 21: Specimen Condition
const SPECIMEN_CONDITION = [
'HEM' => 'Hemolyzed',
'ITC' => 'Icteric',
'LIP' => 'Lipemic',
'CFU' => 'Centrifuged',
'ROOM' => 'Room temperature',
'COOL' => 'Cool',
'FROZ' => 'Frozen',
'CLOT' => 'Clotted',
'AUT' => 'Autolyzed',
'CON' => 'Contaminated',
'LIVE' => 'Live'
];
// VSetID 22: Specimen Role
const SPECIMEN_ROLE = [
'P' => 'Patient',
'B' => 'Blind Sample',
'Q' => 'Control specimen',
'E' => 'Electronic QC',
'F' => 'Filler Organization Proficiency',
'O' => 'Operator Proficiency',
'C' => 'Calibrator',
'R' => 'Replicate',
'V' => 'Verifying Calibrator'
];
// VSetID 23: Collection Method
const COLLECTION_METHOD = [
'pcntr' => 'Puncture',
'fprk' => 'Finger-prick sampling',
'ucct' => 'Urine specimen collection, clean catch',
'utcl' => 'Timed urine collection',
'ucth' => 'Urine specimen collection, catheterized',
'scgh' => 'Collection of coughed sputum',
'bpsy' => 'Biopsy',
'aspn' => 'Aspiration',
'excs' => 'Excision',
'scrp' => 'Scraping'
];
// VSetID 24: Body Site
const BODY_SITE = [
'LA' => 'Left Arm',
'RA' => 'Right Arm',
'LF' => 'Left Foot',
'RF' => 'Right Foot'
];
// VSetID 25: Container Size
const CONTAINER_SIZE = [
'5ml' => '5 mL',
'7ml' => '7 mL',
'10ml' => '10 mL',
'1l' => '1 L'
];
// VSetID 26: Fasting Status
const FASTING_STATUS = [
'F' => 'Fasting',
'NF' => 'Not Fasting',
'NG' => 'Not Given'
];
// VSetID 27: Test Type
const TEST_TYPE = [
'TEST' => 'Test',
'PARAM' => 'Parameter',
'CALC' => 'Calculated Test',
'GROUP' => 'Group Test',
'TITLE' => 'Title'
];
// VSetID 28: Result Unit
const RESULT_UNIT = [
'g/dL' => 'g/dL',
'g/L' => 'g/L',
'mg/dL' => 'mg/dL',
'mg/L' => 'mg/L',
'L/L' => 'L/L',
'x106/mL' => 'x106/mL',
'x1012/L' => 'x1012/L',
'fL' => 'fL',
'pg' => 'pg',
'x109/L' => 'x109/L'
];
// VSetID 29: Formula Language
const FORMULA_LANGUAGE = [
'Phyton' => 'Phyton',
'CQL' => 'Clinical Quality Language',
'FHIRP' => 'FHIRPath',
'SQL' => 'SQL'
];
// VSetID 30: Race (Ethnicity)
const RACE = [
'JAWA' => 'Jawa',
'SUNDA' => 'Sunda',
'BATAK' => 'Batak',
'SULOR' => 'Suku asal Sulawesi lainnya',
'MDRA' => 'Madura',
'BTWI' => 'Betawi',
'MNG' => 'Minangkabau',
'BUGIS' => 'Bugis',
'MLYU' => 'Melayu',
'SUMSL' => 'Suku asal Sumatera Selatan',
'BTNOR' => 'Suku asal Banten',
'NTTOR' => 'Suku asal Nusa Tenggara Timur',
'BNJAR' => 'Banjar',
'ACEH' => 'Aceh',
'BALI' => 'Bali',
'SASAK' => 'Sasak',
'DAYAK' => 'Dayak',
'TNGHA' => 'Tionghoa',
'PPAOR' => 'Suku asal Papua',
'MKSSR' => 'Makassar',
'SUMOR' => 'Suku asal Sumatera lainnya',
'MLKOR' => 'Suku asal Maluku',
'KLMOR' => 'Suku asal Kalimantan lainnya',
'CRBON' => 'Cirebon',
'JBIOR' => 'Suku asal Jambi',
'LPGOR' => 'Suku Lampung',
'NTBOR' => 'Suku asal Nusa Tenggara Barat lainnya',
'GRTLO' => 'Gorontalo',
'MNHSA' => 'Minahasa',
'NIAS' => 'Nias',
'FORGN' => 'Asing/luar negeri'
];
// VSetID 31: Religion
const RELIGION = [
'ISLAM' => 'Islam',
'KRSTN' => 'Kristen',
'KTLIK' => 'Katolik',
'HINDU' => 'Hindu',
'BUDHA' => 'Budha',
'KHCU' => 'Khong Hu Cu',
'OTHER' => 'Lainnya'
];
// VSetID 32: Ethnic
const ETHNIC = [
'PPMLN' => 'Papua Melanezoid',
'NGRID' => 'Negroid',
'WDOID' => 'Weddoid',
'MMPM' => 'Melayu Mongoloid_Proto Melayu',
'MMDM' => 'Melayu Mongoloid_Deutro Melayu',
'TNGHA' => 'Tionghoa',
'INDIA' => 'India',
'ARAB' => 'Arab'
];
// VSetID 33: Country (ISO 2-letter codes - ISO 3166-1 alpha-2)
const COUNTRY = null; // Loaded from external file
/**
* Get COUNTRY data from external file (lazy load)
*/
private static function loadCountryData(): array {
$file = APPPATH . 'Libraries/Data/Countries.php';
if (is_file($file)) {
return require $file;
}
return [];
}
/**
* Get formatted country list
*/
public static function getCountry(): array {
return self::format(self::loadCountryData());
}
// VSetID 34: Container Cap Color
const CONTAINER_CAP_COLOR = [
'PRPL' => 'Purple',
'RED' => 'Red',
'YLLW' => 'Yellow',
'GRN' => 'Green',
'PINK' => 'Pink',
'LBLU' => 'Light Blue',
'RBLU' => 'Royal Blue',
'GRAY' => 'Gray'
];
// VSetID 35: Test Activity
const TEST_ACTIVITY = [
'ORD' => 'Order',
'ANA' => 'Analyse',
'VER' => 'Result Verification/Technical Validation',
'REV' => 'Clinical Review/Clinical Validation',
'REP' => 'Reporting'
];
// VSetID 36: ADT Event
const ADT_EVENT = [
'A01' => 'Admit',
'A02' => 'Transfer',
'A03' => 'Discharge',
'A04' => 'Register',
'A08' => 'Update patient information',
'A11' => 'Cancel admit',
'A12' => 'Cancel transfer',
'A13' => 'Cancel discharge',
'A23' => 'Delete patient record',
'A24' => 'Link patient information',
'A37' => 'Unlink patient information',
'A54' => 'Change attending doctor',
'A61' => 'Change consulting doctor'
];
// VSetID 37: Site Type
const SITE_TYPE = [
'GH' => 'Government Hospital',
'PH' => 'Private Hospital',
'GHL' => 'Government Hospital Lab',
'PHL' => 'Private Hospital Lab',
'GL' => 'Government Lab',
'PL' => 'Private Lab'
];
// VSetID 38: Site Class
const SITE_CLASS = [
'A' => 'Kelas A',
'B' => 'Kelas B',
'C' => 'Kelas C',
'D' => 'Kelas D',
'Utm' => 'Utama',
'Ptm' => 'Pratama'
];
// VSetID 39: Entity Type
const ENTITY_TYPE = [
'HIS' => 'HIS',
'SITE' => 'Site',
'WST' => 'Workstation',
'INST' => 'Equipment/Instrument'
];
// VSetID 40: Area Class
const AREA_CLASS = [
'PROP' => 'Propinsi',
'KAB' => 'Kabupaten',
'KOTA' => 'Kota'
];
// VSetID 41: Math Sign
const MATH_SIGN = [
'=' => 'Equal',
'<' => 'Less than',
'>' => 'Greater than',
'<=' => 'Less than or equal to',
'>=' => 'Greater than or equal to'
];
// VSetID 42: VCategory
const V_CATEGORY = [
'0' => 'System',
'1' => 'User-defined'
];
// VSetID 43: Result Type
const RESULT_TYPE = [
'NMRIC' => 'Numeric',
'RANGE' => 'Range',
'TEXT' => 'Text',
'VSET' => 'Value set'
];
// VSetID 44: Reference Type
const REFERENCE_TYPE = [
'NMRC' => 'Numeric',
'TEXT' => 'Text'
];
// VSetID 45: Range Type
const RANGE_TYPE = [
'REF' => 'Reference Range',
'CRTC' => 'Critical Range',
'VAL' => 'Validation Range',
'RERUN' => 'Rerun Range'
];
// VSetID 46: Numeric Reference Type
const NUMERIC_REF_TYPE = [
'RANGE' => 'Range',
'THOLD' => 'Threshold'
];
// VSetID 47: Text Reference Type
const TEXT_REF_TYPE = [
'VSET' => 'Value Set',
'TEXT' => 'Text'
];
// Convenience constants (aliases for common use cases)
const PRIORITY = self::ORDER_PRIORITY;
const TEST_STATUS = [
'PENDING' => 'Waiting for Results',
'IN_PROCESS' => 'Analyzing',
'VERIFIED' => 'Verified & Signed',
'REJECTED' => 'Sample Rejected'
];
const REQUEST_STATUS = self::SPECIMEN_STATUS;
const RESULT_STATUS = [
'PRELIMINARY' => 'Preliminary',
'FINAL' => 'Final',
'CORRECTED' => 'Corrected',
'CANCELLED' => 'Cancelled'
];
/**
* Get all lookups formatted for frontend
* @return array
*/
public static function getAll(): array {
return [
'ws_type' => self::format(self::WS_TYPE),
'enable_disable' => self::format(self::ENABLE_DISABLE),
'gender' => self::format(self::GENDER),
'marital_status' => self::format(self::MARITAL_STATUS),
'death_indicator' => self::format(self::DEATH_INDICATOR),
'identifier_type' => self::format(self::IDENTIFIER_TYPE),
'operation' => self::format(self::OPERATION),
'did_type' => self::format(self::DID_TYPE),
'requested_entity' => self::format(self::REQUESTED_ENTITY),
'order_priority' => self::format(self::ORDER_PRIORITY),
'order_status' => self::format(self::ORDER_STATUS),
'location_type' => self::format(self::LOCATION_TYPE),
'additive' => self::format(self::ADDITIVE),
'container_class' => self::format(self::CONTAINER_CLASS),
'specimen_type' => self::format(self::SPECIMEN_TYPE),
'unit' => self::format(self::UNIT),
'generate_by' => self::format(self::GENERATE_BY),
'specimen_activity' => self::format(self::SPECIMEN_ACTIVITY),
'activity_result' => self::format(self::ACTIVITY_RESULT),
'specimen_status' => self::format(self::SPECIMEN_STATUS),
'specimen_condition' => self::format(self::SPECIMEN_CONDITION),
'specimen_role' => self::format(self::SPECIMEN_ROLE),
'collection_method' => self::format(self::COLLECTION_METHOD),
'body_site' => self::format(self::BODY_SITE),
'container_size' => self::format(self::CONTAINER_SIZE),
'fasting_status' => self::format(self::FASTING_STATUS),
'test_type' => self::format(self::TEST_TYPE),
'result_unit' => self::format(self::RESULT_UNIT),
'formula_language' => self::format(self::FORMULA_LANGUAGE),
'race' => self::format(self::RACE),
'religion' => self::format(self::RELIGION),
'ethnic' => self::format(self::ETHNIC),
'country' => self::getCountry(),
'container_cap_color' => self::format(self::CONTAINER_CAP_COLOR),
'test_activity' => self::format(self::TEST_ACTIVITY),
'adt_event' => self::format(self::ADT_EVENT),
'site_type' => self::format(self::SITE_TYPE),
'site_class' => self::format(self::SITE_CLASS),
'entity_type' => self::format(self::ENTITY_TYPE),
'area_class' => self::format(self::AREA_CLASS),
'math_sign' => self::format(self::MATH_SIGN),
'v_category' => self::format(self::V_CATEGORY),
'result_type' => self::format(self::RESULT_TYPE),
'reference_type' => self::format(self::REFERENCE_TYPE),
'range_type' => self::format(self::RANGE_TYPE),
'numeric_ref_type' => self::format(self::NUMERIC_REF_TYPE),
'text_ref_type' => self::format(self::TEXT_REF_TYPE)
];
}
/**
* Format associative array as [{value: 'KEY', label: 'Label'}, ...]
*/
private static function format(array $array): array {
$result = [];
foreach ($array as $key => $label) {
$result[] = ['value' => (string) $key, 'label' => (string) $label];
}
return $result;
}
/**
* Get single lookup by constant name (case-insensitive)
* @param string $name Constant name (e.g., 'gender', 'PRIORITY')
* @return array|null
*/
public static function get(string $name): ?array {
$const = strtoupper($name);
// Special case for COUNTRY (loaded from external file)
if ($const === 'COUNTRY') {
return self::getCountry();
}
if (defined("self::$const")) {
return self::format(constant("self::$const"));
}
return null;
}
/**
* Get raw constant array (not formatted)
* @param string $name
* @return array|null
*/
public static function getRaw(string $name): ?array {
$const = strtoupper($name);
// Special case for COUNTRY (loaded from external file)
if ($const === 'COUNTRY') {
return self::loadCountryData();
}
if (defined("self::$const")) {
return constant("self::$const");
}
return null;
}
/**
* Get label by key from a lookup
* @param string $lookup Lookup name
* @param string $key Key to find
* @return string|null
*/
public static function getLabel(string $lookup, string $key): ?string {
$raw = self::getRaw($lookup);
return $raw[$key] ?? null;
}
} }

108
app/Libraries/ValueSet.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace App\Libraries;
use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Config\BaseConfig;
class ValueSet {
private static $cache = null;
private static string $dataPath = APPPATH . 'Libraries/Data/valuesets/';
private static string $cacheKey = 'valueset_all';
private static function getCacheHandler() {
if (self::$cache === null) {
$config = config('Cache');
self::$cache = CacheFactory::getHandler($config);
}
return self::$cache;
}
public static function get(string $name): ?array {
$all = self::getAll();
$values = $all[$name]['values'] ?? null;
if ($values === null) return null;
return self::format($values);
}
public static function getRaw(string $name): ?array {
$all = self::getAll();
return $all[$name]['values'] ?? null;
}
public static function getAll(): array {
$handler = self::getCacheHandler();
$data = $handler->get(self::$cacheKey);
if ($data !== null) {
return $data;
}
$data = self::bundleAll();
$handler->save(self::$cacheKey, $data, 0);
return $data;
}
public static function getLabel(string $lookupName, string $key): ?string {
$raw = self::getRaw($lookupName);
if ($raw === null) return null;
foreach ($raw as $item) {
if (($item['key'] ?? $item['value'] ?? null) === $key) {
return $item['value'] ?? $item['label'] ?? null;
}
}
return null;
}
public static function getOptions(string $lookupName): array {
$raw = self::getRaw($lookupName);
if ($raw === null) return [];
return array_map(function ($item) {
return [
'key' => $item['key'] ?? '',
'value' => $item['value'] ?? $item['label'] ?? '',
];
}, $raw);
}
public static function transformLabels(array $data, array $fieldMappings): array {
foreach ($data as &$row) {
foreach ($fieldMappings as $field => $lookupName) {
if (isset($row[$field]) && $row[$field] !== null) {
$row[$field . 'Text'] = self::getLabel($lookupName, $row[$field]) ?? '';
}
}
}
return $data;
}
public static function clearCache(): bool {
$handler = self::getCacheHandler();
return $handler->delete(self::$cacheKey);
}
private static function bundleAll(): array {
$result = [];
foreach (glob(self::$dataPath . '*.json') as $file) {
$name = pathinfo($file, PATHINFO_FILENAME);
if ($name[0] === '_') continue;
$data = self::loadFile($file);
if ($data) {
$result[$name] = $data;
}
}
return $result;
}
private static function loadFile(string $path): ?array {
if (!is_file($path)) return null;
$content = file_get_contents($path);
return json_decode($content, true);
}
private static function format(array $values): array {
return array_map(fn($v) => [
'value' => (string) ($v['key'] ?? ''),
'label' => (string) ($v['value'] ?? $v['label'] ?? '')
], $values);
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
namespace App\Models\Location; namespace App\Models\Location;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class LocationModel extends BaseModel { class LocationModel extends BaseModel {
protected $table = 'location'; protected $table = 'location';
@ -14,24 +15,31 @@ class LocationModel extends BaseModel {
protected $deletedField = 'EndDate'; protected $deletedField = 'EndDate';
public function getLocations($LocCode, $LocName) { public function getLocations($LocCode, $LocName) {
$sql = $this->select("LocationID, LocCode, Parent, LocFull, LocType, v.VDesc as LocTypeText") $sql = $this->select("LocationID, LocCode, Parent, LocFull, LocType");
->join("valueset v", "v.VID=location.loctype", 'left');
if($LocName != '') { $sql->like('LocFull', $LocName, 'both'); } if($LocName != '') { $sql->like('LocFull', $LocName, 'both'); }
if($LocCode != '') { $sql->like('LocCode', $LocCode, 'both'); } if($LocCode != '') { $sql->like('LocCode', $LocCode, 'both'); }
$rows = $sql->findAll(); $rows = $sql->findAll();
$rows = ValueSet::transformLabels($rows, [
'LocType' => 'location_type',
]);
return $rows; return $rows;
} }
public function getLocation($LocationID) { public function getLocation($LocationID) {
//'Street1', 'Street2', 'City', 'Province', 'PostCode', 'GeoLocationSystem', 'GeoLocationData',
$row = $this->select("location.*, la.Street1, la.Street2, la.PostCode, la.GeoLocationSystem, la.GeoLocationData, $row = $this->select("location.*, la.Street1, la.Street2, la.PostCode, la.GeoLocationSystem, la.GeoLocationData,
v.VDesc as LocTypeText, prop.AreaGeoID as ProvinceID, prop.AreaName as Province, city.AreaGeoID as CityID, city.AreaName as City, site.SiteID, site.SiteName") prop.AreaGeoID as ProvinceID, prop.AreaName as Province, city.AreaGeoID as CityID, city.AreaName as City, site.SiteID, site.SiteName")
->join("locationaddress la", "location.LocationID=la.LocationID", "left") ->join("locationaddress la", "location.LocationID=la.LocationID", "left")
->join("valueset v", "v.VID=location.loctype", "left")
->join("areageo prop", "la.Province=prop.AreaGeoID", "left") ->join("areageo prop", "la.Province=prop.AreaGeoID", "left")
->join("areageo city", "la.City=city.AreaGeoID", "left") ->join("areageo city", "la.City=city.AreaGeoID", "left")
->join("site", "site.SiteID=location.SiteID", "left") ->join("site", "site.SiteID=location.SiteID", "left")
->where('location.LocationID', (int) $LocationID)->first(); ->where('location.LocationID', (int) $LocationID)->first();
if (!$row) return null;
$row = ValueSet::transformLabels([$row], [
'LocType' => 'location_type',
])[0];
return $row; return $row;
} }
@ -83,4 +91,3 @@ class LocationModel extends BaseModel {
} }
} }
} }

View File

@ -0,0 +1,102 @@
<?php
namespace App\Models\OrderTest;
use App\Models\BaseModel;
class OrderTestModel extends BaseModel {
protected $table = 'ordertest';
protected $primaryKey = 'OrderID';
protected $allowedFields = [
'OrderID',
'InternalPID',
'PatVisitID',
'OrderDateTime',
'Priority',
'OrderStatus',
'OrderedBy',
'OrderingProvider',
'SiteID',
'SourceSiteID',
'DepartmentID',
'WorkstationID',
'BillingAccount',
'DelDate'
];
public function generateOrderID(string $siteCode = '00'): string {
$date = new \DateTime();
$year = $date->format('y');
$month = $date->format('m');
$day = $date->format('d');
$counter = $this->db->table('counter')
->where('CounterName', 'ORDER')
->get()
->getRow();
if (!$counter) {
$this->db->table('counter')->insert([
'CounterName' => 'ORDER',
'CounterValue' => 1
]);
$seq = 1;
} else {
$seq = $counter->CounterValue + 1;
$this->db->table('counter')
->where('CounterName', 'ORDER')
->update(['CounterValue' => $seq]);
}
$seqStr = str_pad($seq, 5, '0', STR_PAD_LEFT);
return $siteCode . $year . $month . $day . $seqStr;
}
public function createOrder(array $data): string {
$orderID = $data['OrderID'] ?? $this->generateOrderID();
$orderData = [
'OrderID' => $orderID,
'InternalPID' => $data['InternalPID'],
'PatVisitID' => $data['PatVisitID'] ?? null,
'OrderDateTime' => $data['OrderDateTime'] ?? date('Y-m-d H:i:s'),
'Priority' => $data['Priority'] ?? 'R',
'OrderStatus' => $data['OrderStatus'] ?? 'ORD',
'OrderedBy' => $data['OrderedBy'] ?? null,
'OrderingProvider' => $data['OrderingProvider'] ?? null,
'SiteID' => $data['SiteID'] ?? 1,
'SourceSiteID' => $data['SourceSiteID'] ?? 1,
'DepartmentID' => $data['DepartmentID'] ?? null,
'WorkstationID' => $data['WorkstationID'] ?? null,
'BillingAccount' => $data['BillingAccount'] ?? null,
'CreateDate' => date('Y-m-d H:i:s')
];
$this->insert($orderData);
return $orderID;
}
public function getOrder(string $orderID): ?array {
return $this->select('*')
->where('OrderID', $orderID)
->where('DelDate', null)
->get()
->getRowArray();
}
public function getOrdersByPatient(int $internalPID): array {
return $this->select('*')
->where('InternalPID', $internalPID)
->where('DelDate', null)
->orderBy('OrderDateTime', 'DESC')
->get()
->getResultArray();
}
public function updateStatus(string $orderID, string $status): bool {
return $this->where('OrderID', $orderID)->update(['OrderStatus' => $status]);
}
public function softDelete(string $orderID): bool {
return $this->where('OrderID', $orderID)->update(['DelDate' => date('Y-m-d H:i:s')]);
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
namespace App\Models\Organization; namespace App\Models\Organization;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class AccountModel extends BaseModel { class AccountModel extends BaseModel {
protected $table = 'account'; protected $table = 'account';
@ -14,7 +15,7 @@ class AccountModel extends BaseModel {
protected $updatedField = ''; protected $updatedField = '';
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $deletedField = 'EndDate'; protected $deletedField = 'EndDate';
public function getAccounts($filter=[]) { public function getAccounts($filter=[]) {
$builder = $this->select('account.AccountID, account.AccountName, account.Parent, pa.AccountName as ParentName, account.Initial') $builder = $this->select('account.AccountID, account.AccountName, account.Parent, pa.AccountName as ParentName, account.Initial')
->join('account pa', 'pa.AccountID=account.Parent', 'left'); ->join('account pa', 'pa.AccountID=account.Parent', 'left');
@ -32,15 +33,20 @@ class AccountModel extends BaseModel {
public function getAccount($AccountID) { public function getAccount($AccountID) {
$row = $this->select('account.*, pa.AccountName as ParentName, areageo.AreaName, areageo.AreaGeoID, $row = $this->select('account.*, pa.AccountName as ParentName, areageo.AreaName, areageo.AreaGeoID,
city.AreaName as CityName, city.AreaGeoID as City, prov.AreaName as ProvName, prov.AreaGeoID as Prov, city.AreaName as CityName, city.AreaGeoID as City, prov.AreaName as ProvName, prov.AreaGeoID as Prov')
country.VValue as CountryName, country.VID as country')
->join('account pa', 'pa.AccountID=account.Parent', 'left') ->join('account pa', 'pa.AccountID=account.Parent', 'left')
->join('areageo', 'areageo.AreaCode=account.AreaCode', 'left') ->join('areageo', 'areageo.AreaCode=account.AreaCode', 'left')
->join('areageo city', 'city.AreaGeoID=account.City', 'left') ->join('areageo city', 'city.AreaGeoID=account.City', 'left')
->join('areageo prov', 'prov.AreaGeoID=account.Province', 'left') ->join('areageo prov', 'prov.AreaGeoID=account.Province', 'left')
->join('valueset country', 'country.VID=account.Country', 'left')
->where('account.AccountID', $AccountID) ->where('account.AccountID', $AccountID)
->first(); ->first();
if (!$row) return null;
$row = ValueSet::transformLabels([$row], [
'Country' => 'account_Country',
])[0];
return $row; return $row;
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace App\Models\Organization; namespace App\Models\Organization;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class SiteModel extends BaseModel { class SiteModel extends BaseModel {
protected $table = 'site'; protected $table = 'site';
@ -13,7 +14,7 @@ class SiteModel extends BaseModel {
protected $updatedField = ''; protected $updatedField = '';
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $deletedField = 'EndDate'; protected $deletedField = 'EndDate';
public function getSites($filter) { public function getSites($filter) {
$builder = $this->select('site.SiteID, site.SiteCode, site.SiteName, s1.SiteName as ParentName, account.AccountName') $builder = $this->select('site.SiteID, site.SiteCode, site.SiteName, s1.SiteName as ParentName, account.AccountName')
->join('account', 'account.AccountID=site.AccountID', 'left') ->join('account', 'account.AccountID=site.AccountID', 'left')
@ -32,13 +33,19 @@ class SiteModel extends BaseModel {
} }
public function getSite($SiteID) { public function getSite($SiteID) {
$row = $this->select('site.*, account.AccountName, s1.SiteName as ParentName, sitetype.VValue as SiteType, siteclass.VValue as SiteClass') $row = $this->select('site.*, account.AccountName, s1.SiteName as ParentName')
->join('account', 'account.AccountID=site.AccountID', 'left') ->join('account', 'account.AccountID=site.AccountID', 'left')
->join('site s1', 's1.SiteID=site.Parent', 'left') ->join('site s1', 's1.SiteID=site.Parent', 'left')
->join('valueset sitetype', 'site.SiteTypeID=sitetype.VID', 'left')
->join('valueset siteclass', 'site.SiteClassID=siteclass.VID', 'left')
->where('site.SiteID', $SiteID) ->where('site.SiteID', $SiteID)
->first(); ->first();
if (!$row) return null;
$row = ValueSet::transformLabels([$row], [
'SiteTypeID' => 'site_type',
'SiteClassID' => 'site_class',
])[0];
return $row; return $row;
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace App\Models\Organization; namespace App\Models\Organization;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class WorkstationModel extends BaseModel { class WorkstationModel extends BaseModel {
protected $table = 'workstation'; protected $table = 'workstation';
@ -13,12 +14,12 @@ class WorkstationModel extends BaseModel {
protected $updatedField = ''; protected $updatedField = '';
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $deletedField = 'EndDate'; protected $deletedField = 'EndDate';
public function getWorkstations($filter = []) { public function getWorkstations($filter = []) {
$this->select('workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName') $this->select('workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName')
->join('workstation wst1', 'workstation.LinkTo=wst1.WorkstationID', 'left') ->join('workstation wst1', 'workstation.LinkTo=wst1.WorkstationID', 'left')
->join('department', 'department.DepartmentID=workstation.DepartmentID', 'left'); ->join('department', 'department.DepartmentID=workstation.DepartmentID', 'left');
if (!empty($filter['WorkstationCode'])) { if (!empty($filter['WorkstationCode'])) {
$this->like('workstation.WorkstationCode', $filter['WorkstationCode'], 'both'); $this->like('workstation.WorkstationCode', $filter['WorkstationCode'], 'both');
} }
@ -30,13 +31,19 @@ class WorkstationModel extends BaseModel {
} }
public function getWorkstation($WorkstationID) { public function getWorkstation($WorkstationID) {
$row = $this->select("workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName,enable.VDesc as EnableName, wstype.VDesc as TypeName") $row = $this->select("workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName")
->join('workstation wst1', 'workstation.LinkTo=wst1.WorkstationID', 'left') ->join('workstation wst1', 'workstation.LinkTo=wst1.WorkstationID', 'left')
->join('department', 'department.DepartmentID=workstation.DepartmentID', 'left') ->join('department', 'department.DepartmentID=workstation.DepartmentID', 'left')
->join('valueset wstype', 'wstype.VID=workstation.Type', 'left')
->join('valueset enable', 'enable.VID=workstation.Enable', 'left')
->where('workstation.WorkstationID', $WorkstationID) ->where('workstation.WorkstationID', $WorkstationID)
->first(); ->first();
if (!$row) return null;
$row = ValueSet::transformLabels([$row], [
'Type' => 'ws_type',
'Enable' => 'enable_disable',
])[0];
return $row; return $row;
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Models\Patient; namespace App\Models\Patient;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
use App\Models\Patient\PatAttModel; use App\Models\Patient\PatAttModel;
use App\Models\Patient\PatComModel; use App\Models\Patient\PatComModel;
@ -10,9 +11,9 @@ use App\Models\Patient\PatIdtModel;
class PatientModel extends BaseModel { class PatientModel extends BaseModel {
protected $table = 'patient'; protected $table = 'patient';
protected $primaryKey = 'InternalPID'; protected $primaryKey = 'InternalPID';
protected $allowedFields = ['PatientID', 'AlternatePID', 'Prefix', 'NameFirst', 'NameMiddle', 'NameMaiden', 'NameLast', 'Suffix', 'NameAlias', 'Gender', 'Birthdate', 'PlaceOfBirth', 'Street_1', 'Street_2', 'Street_3', protected $allowedFields = ['PatientID', 'AlternatePID', 'Prefix', 'NameFirst', 'NameMiddle', 'NameMaiden', 'NameLast', 'Suffix', 'NameAlias', 'Sex', 'Birthdate', 'PlaceOfBirth', 'Street_1', 'Street_2', 'Street_3',
'City', 'Province', 'ZIP', 'EmailAddress1', 'EmailAddress2', 'Phone', 'MobilePhone', 'Custodian', 'AccountNumber', 'Country', 'Race', 'MaritalStatus', 'Religion', 'Ethnic', 'Citizenship', 'City', 'Province', 'ZIP', 'EmailAddress1', 'EmailAddress2', 'Phone', 'MobilePhone', 'Custodian', 'AccountNumber', 'Country', 'Race', 'MaritalStatus', 'Religion', 'Ethnic', 'Citizenship',
'DeathIndicator', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ]; 'DeathIndicator', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ];
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'CreateDate'; protected $createdField = 'CreateDate';
@ -23,8 +24,7 @@ class PatientModel extends BaseModel {
public function getPatients($filters = []) { public function getPatients($filters = []) {
$qname = "CONCAT_WS(' ', IFNULL(Prefix,''), IFNULL(NameFirst,''), IFNULL(NameMiddle,''), IFNULL(NameLast,''), IFNULL(NameMaiden,''), IFNULL(Suffix,''))"; $qname = "CONCAT_WS(' ', IFNULL(Prefix,''), IFNULL(NameFirst,''), IFNULL(NameMiddle,''), IFNULL(NameLast,''), IFNULL(NameMaiden,''), IFNULL(Suffix,''))";
$this->select("InternalPID, PatientID, $qname as FullName, vs.VDesc as Gender, Birthdate, EmailAddress1 as Email, MobilePhone"); $this->select("InternalPID, PatientID, $qname as FullName, Sex, Birthdate, EmailAddress1 as Email, MobilePhone");
$this->join('valueset vs', 'vs.vid = Gender', 'left');
if (!empty($filters['Name'])) { if (!empty($filters['Name'])) {
$this->like($qname, $filters['Name'], 'both'); $this->like($qname, $filters['Name'], 'both');
@ -42,26 +42,16 @@ class PatientModel extends BaseModel {
$this->where('Birthdate', $filters['Birthdate']); $this->where('Birthdate', $filters['Birthdate']);
} }
return $this->findAll(); $rows = $this->findAll();
$rows = ValueSet::transformLabels($rows, [
'Sex' => 'gender',
]);
return $rows;
} }
public function getPatient($InternalPID) { public function getPatient($InternalPID) {
$rows = $this->select(" $rows = $this->select("
patient.*, patient.*,
country.VDesc as Country,
country.VID as CountryVID,
race.VDesc as Race,
race.VID as RaceVID,
religion.VDesc as Religion,
religion.VID as ReligionVID,
ethnic.VDesc as Ethnic,
ethnic.VID as EthnicVID,
gender.VDesc as Gender,
gender.VID as GenderVID,
deathindicator.VDesc as DeathIndicator,
deathindicator.VID as DeathIndicatorVID,
maritalstatus.VDesc as MaritalStatus,
maritalstatus.VID as MaritalStatusVID,
patcom.Comment as Comment, patcom.Comment as Comment,
patidt.IdentifierType, patidt.IdentifierType,
patidt.Identifier, patidt.Identifier,
@ -72,13 +62,6 @@ class PatientModel extends BaseModel {
areageo2.AreaName as City areageo2.AreaName as City
") ")
->join('valueset country', 'country.VID = patient.Country', 'left')
->join('valueset race', 'race.VID = patient.Race', 'left')
->join('valueset religion', 'religion.VID = patient.Religion', 'left')
->join('valueset ethnic', 'ethnic.VID = patient.Ethnic', 'left')
->join('valueset gender', 'gender.VID = patient.Gender', 'left')
->join('valueset deathindicator', 'deathindicator.VID = patient.DeathIndicator', 'left')
->join('valueset maritalstatus', 'maritalstatus.VID = patient.MaritalStatus', 'left')
->join('patcom', 'patcom.InternalPID = patient.InternalPID', 'left') ->join('patcom', 'patcom.InternalPID = patient.InternalPID', 'left')
->join('patidt', 'patidt.InternalPID = patient.InternalPID', 'left') ->join('patidt', 'patidt.InternalPID = patient.InternalPID', 'left')
->join('patatt', 'patatt.InternalPID = patient.InternalPID and patatt.DelDate is null', 'left') ->join('patatt', 'patatt.InternalPID = patient.InternalPID and patatt.DelDate is null', 'left')
@ -97,7 +80,16 @@ class PatientModel extends BaseModel {
unset($patient['Identifier']); unset($patient['Identifier']);
unset($patient['Comment']); unset($patient['Comment']);
// Default nested structures $patient = ValueSet::transformLabels([$patient], [
'Sex' => 'gender',
'Country' => 'country',
'Race' => 'race',
'Religion' => 'religion',
'Ethnic' => 'ethnic',
'DeathIndicator' => 'death_indicator',
'MaritalStatus' => 'marital_status',
])[0];
$patient['PatIdt'] = null; $patient['PatIdt'] = null;
$patient['PatAtt'] = []; $patient['PatAtt'] = [];
@ -133,24 +125,20 @@ class PatientModel extends BaseModel {
try { try {
// Insert Data ke Tabel Patient, get ID dan cek apa ada error
$this->insert($input); $this->insert($input);
$newInternalPID = $this->getInsertID(); $newInternalPID = $this->getInsertID();
$this->checkDbError($db, 'Insert patient'); $this->checkDbError($db, 'Insert patient');
// Insert Data ke Tabel PatIdt
if (!empty($input['PatIdt'])) { if (!empty($input['PatIdt'])) {
$modelPatIdt->createPatIdt($input['PatIdt'], $newInternalPID); $modelPatIdt->createPatIdt($input['PatIdt'], $newInternalPID);
$this->checkDbError($db, 'Insert PatIdt'); $this->checkDbError($db, 'Insert PatIdt');
} }
// Insert Data ke Tabel PatCom
if (!empty($input['PatCom'])) { if (!empty($input['PatCom'])) {
$modelPatCom->createPatCom($input['PatCom'], $newInternalPID); $modelPatCom->createPatCom($input['PatCom'], $newInternalPID);
$this->checkDbError($db, 'Insert PatCom'); $this->checkDbError($db, 'Insert PatCom');
} }
// Insert Data ke Tabel PatAtt
if (!empty($input['PatAtt'])) { if (!empty($input['PatAtt'])) {
$modelPatAtt->createPatAtt($input['PatAtt'], $newInternalPID); $modelPatAtt->createPatAtt($input['PatAtt'], $newInternalPID);
$this->checkDbError($db, 'Insert PatAtt'); $this->checkDbError($db, 'Insert PatAtt');
@ -178,12 +166,10 @@ class PatientModel extends BaseModel {
try { try {
// Update Patient
$InternalPID = $input['InternalPID']; $InternalPID = $input['InternalPID'];
$this->where('InternalPID',$InternalPID)->set($input)->update(); $this->where('InternalPID',$InternalPID)->set($input)->update();
$this->checkDbError($db, 'Update patient'); $this->checkDbError($db, 'Update patient');
// Update Patidt
if (!empty($input['PatIdt'])) { if (!empty($input['PatIdt'])) {
$modelPatIdt->updatePatIdt($input['PatIdt'], $InternalPID); $modelPatIdt->updatePatIdt($input['PatIdt'], $InternalPID);
$this->checkDbError($db, 'Update patIdt'); $this->checkDbError($db, 'Update patIdt');
@ -192,7 +178,6 @@ class PatientModel extends BaseModel {
$this->checkDbError($db, 'Update patidt'); $this->checkDbError($db, 'Update patidt');
} }
// Update Patcom
if (!empty($input['PatCom'])) { if (!empty($input['PatCom'])) {
$modelPatCom->updatePatCom($input['PatCom'], $InternalPID); $modelPatCom->updatePatCom($input['PatCom'], $InternalPID);
$this->checkDbError($db, 'Update PatCom'); $this->checkDbError($db, 'Update PatCom');
@ -201,7 +186,6 @@ class PatientModel extends BaseModel {
$this->checkDbError($db, 'Update patcom'); $this->checkDbError($db, 'Update patcom');
} }
// Update Patatt
if (!empty($input['PatAtt'])) { if (!empty($input['PatAtt'])) {
$modelPatAtt->updatePatAtt($input['PatAtt'], $InternalPID); $modelPatAtt->updatePatAtt($input['PatAtt'], $InternalPID);
$this->checkDbError($db, 'Update PatAtt'); $this->checkDbError($db, 'Update PatAtt');
@ -254,11 +238,9 @@ class PatientModel extends BaseModel {
->first() ?: null; ->first() ?: null;
} }
// Conversion to (Years Months Days) - For Age
private function calculateAgeFromBirthdate($birthdate, $deathdate) { private function calculateAgeFromBirthdate($birthdate, $deathdate) {
$dob = new \DateTime($birthdate); $dob = new \DateTime($birthdate);
// Cek DeathTime
if ($deathdate == null) { if ($deathdate == null) {
$today = new \DateTime(); $today = new \DateTime();
} else { } else {
@ -281,19 +263,17 @@ class PatientModel extends BaseModel {
return $formattedAge; return $formattedAge;
} }
// Conversion Time to Format Y-m-d\TH:i:s\Z
private function formattedDate(?string $dateString): ?string { private function formattedDate(?string $dateString): ?string {
try { try {
if (empty($dateString)) {return null;} if (empty($dateString)) {return null;}
$dt = new \DateTime($dateString, new \DateTimeZone("UTC")); $dt = new \DateTime($dateString, new \DateTimeZone("UTC"));
return $dt->format('Y-m-d\TH:i:s\Z'); // ISO 8601 UTC return $dt->format('Y-m-d\TH:i:s\Z');
} catch (\Exception $e) { } catch (\Exception $e) {
return null; return null;
} }
} }
// Conversion Time to Format j M Y - For BirthdateConversion
private function formatedDateForDisplay($dateString) { private function formatedDateForDisplay($dateString) {
$date = \DateTime::createFromFormat('Y-m-d H:i', $dateString); $date = \DateTime::createFromFormat('Y-m-d H:i', $dateString);
@ -308,7 +288,6 @@ class PatientModel extends BaseModel {
return $date->format('j M Y'); return $date->format('j M Y');
} }
// Check Error and Send Spesific Messages
private function checkDbError($db, string $context) { private function checkDbError($db, string $context) {
$error = $db->error(); $error = $db->error();
if (!empty($error['code'])) { if (!empty($error['code'])) {
@ -318,17 +297,14 @@ class PatientModel extends BaseModel {
} }
} }
// Preventif 0000-00-00
private function isValidDateTime($datetime) { private function isValidDateTime($datetime) {
if (empty($datetime) || $datetime=="") {return null; } if (empty($datetime) || $datetime=="") {return null; }
try { try {
// Kalau input hanya Y-m-d (tanpa jam)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $datetime)) { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $datetime)) {
$dt = \DateTime::createFromFormat('Y-m-d', $datetime); $dt = \DateTime::createFromFormat('Y-m-d', $datetime);
return $dt ? $dt->format('Y-m-d') : null; // hanya tanggal return $dt ? $dt->format('Y-m-d') : null;
} }
// Selain itu (ISO 8601 atau datetime lain), format ke Y-m-d H:i:s
$dt = new \DateTime($datetime); $dt = new \DateTime($datetime);
return $dt->format('Y-m-d H:i:s'); return $dt->format('Y-m-d H:i:s');

View File

@ -2,6 +2,7 @@
namespace App\Models\Specimen; namespace App\Models\Specimen;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class ContainerDefModel extends BaseModel { class ContainerDefModel extends BaseModel {
protected $table = 'containerdef'; protected $table = 'containerdef';
@ -16,10 +17,7 @@ class ContainerDefModel extends BaseModel {
public function getContainers($filter = []) { public function getContainers($filter = []) {
$builder = $this->select('containerdef.*, vscol.VValue as ColorTxt, vscla.VValue as ConClassTxt, vsadd.VValue as AdditiveTxt') $builder = $this->select('containerdef.*');
->join('valueset vscol', 'vscol.VID=containerdef.Color', 'left')
->join('valueset vscla', 'vscla.VID=containerdef.ConClass', 'left')
->join('valueset vsadd', 'vsadd.VID=containerdef.Additive', 'left');
if (!empty($filter['ConCode'])) { if (!empty($filter['ConCode'])) {
$builder->like('containerdef.ConCode', $filter['ConCode'], 'both'); $builder->like('containerdef.ConCode', $filter['ConCode'], 'both');
@ -29,15 +27,26 @@ class ContainerDefModel extends BaseModel {
} }
$rows = $builder->findAll(); $rows = $builder->findAll();
$rows = ValueSet::transformLabels($rows, [
'Color' => 'container_cap_color',
'ConClass' => 'container_class',
'Additive' => 'additive',
]);
return $rows; return $rows;
} }
public function getContainer($ConDefID) { public function getContainer($ConDefID) {
$row = $this->select('containerdef.*, vscol.VValue as ColorTxt, vscla.VValue as ConClassTxt, vsadd.VValue as AdditiveTxt') $row = $this->select('containerdef.*')
->join('valueset vscol', 'vscol.VID=containerdef.Color', 'left')
->join('valueset vscla', 'vscla.VID=containerdef.ConClass', 'left')
->join('valueset vsadd', 'vsadd.VID=containerdef.Additive', 'left')
->where('ConDefID', $ConDefID)->first(); ->where('ConDefID', $ConDefID)->first();
if (!$row) return null;
$row = ValueSet::transformLabels([$row], [
'Color' => 'container_cap_color',
'ConClass' => 'container_class',
'Additive' => 'additive',
])[0];
return $row; return $row;
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Models\Test; namespace App\Models\Test;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class TestDefGrpModel extends BaseModel { class TestDefGrpModel extends BaseModel {
protected $table = 'testdefgrp'; protected $table = 'testdefgrp';
@ -13,32 +14,31 @@ class TestDefGrpModel extends BaseModel {
'CreateDate', 'CreateDate',
'EndDate' 'EndDate'
]; ];
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'CreateDate'; protected $createdField = 'CreateDate';
protected $updatedField = ''; protected $updatedField = '';
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $deletedField = "EndDate"; protected $deletedField = "EndDate";
/**
* Get group members for a test group
*/
public function getGroupMembers($testSiteID) { public function getGroupMembers($testSiteID) {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
return $db->table('testdefgrp') $rows = $db->table('testdefgrp')
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode') ->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType')
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left') ->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
->join('valueset vs', 'vs.VID=t.TestType', 'left')
->where('testdefgrp.TestSiteID', $testSiteID) ->where('testdefgrp.TestSiteID', $testSiteID)
->where('testdefgrp.EndDate IS NULL') ->where('testdefgrp.EndDate IS NULL')
->orderBy('testdefgrp.TestGrpID', 'ASC') ->orderBy('testdefgrp.TestGrpID', 'ASC')
->get()->getResultArray(); ->get()->getResultArray();
$rows = ValueSet::transformLabels($rows, [
'TestType' => 'test_type',
]);
return $rows;
} }
/**
* Get all groups that contain a specific test
*/
public function getGroupsContainingTest($memberTestSiteID) { public function getGroupsContainingTest($memberTestSiteID) {
return $this->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName') return $this->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName')
->join('testdefsite t', 't.TestSiteID=testdefgrp.TestSiteID', 'left') ->join('testdefsite t', 't.TestSiteID=testdefgrp.TestSiteID', 'left')

View File

@ -3,6 +3,7 @@
namespace App\Models\Test; namespace App\Models\Test;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Libraries\ValueSet;
class TestDefSiteModel extends BaseModel { class TestDefSiteModel extends BaseModel {
protected $table = 'testdefsite'; protected $table = 'testdefsite';
@ -23,23 +24,18 @@ class TestDefSiteModel extends BaseModel {
'CreateDate', 'CreateDate',
'StartDate', 'StartDate',
'EndDate' 'EndDate'
]; ];
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'CreateDate'; protected $createdField = 'CreateDate';
protected $updatedField = 'StartDate'; protected $updatedField = 'StartDate';
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $deletedField = "EndDate"; protected $deletedField = "EndDate";
/**
* Get all tests with type information
*/
public function getTests($siteId = null, $testType = null, $visibleScr = null, $visibleRpt = null, $keyword = null) { public function getTests($siteId = null, $testType = null, $visibleScr = null, $visibleRpt = null, $keyword = null) {
$builder = $this->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType, $builder = $this->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt, testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate, testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate")
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
->where('testdefsite.EndDate IS NULL'); ->where('testdefsite.EndDate IS NULL');
if ($siteId) { if ($siteId) {
@ -62,27 +58,29 @@ class TestDefSiteModel extends BaseModel {
$builder->like('testdefsite.TestSiteName', $keyword); $builder->like('testdefsite.TestSiteName', $keyword);
} }
return $builder->orderBy('testdefsite.SeqScr', 'ASC')->findAll(); $rows = $builder->orderBy('testdefsite.SeqScr', 'ASC')->findAll();
$rows = ValueSet::transformLabels($rows, [
'TestType' => 'test_type',
]);
return $rows;
} }
/**
* Get single test with all related details based on TestType
*/
public function getTest($TestSiteID) { public function getTest($TestSiteID) {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$row = $this->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName") $row = $this->select("testdefsite.*")
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
->where("testdefsite.TestSiteID", $TestSiteID) ->where("testdefsite.TestSiteID", $TestSiteID)
->find($TestSiteID); ->find($TestSiteID);
if (!$row) return null; if (!$row) return null;
$typeCode = $row['TypeCode'] ?? ''; $row = ValueSet::transformLabels([$row], [
'TestType' => 'test_type',
])[0];
$typeCode = $row['TestType'] ?? '';
// Load related details based on TestType
if ($typeCode === 'CALC') { if ($typeCode === 'CALC') {
// Load calculation details with joined discipline and department
$row['testdefcal'] = $db->table('testdefcal') $row['testdefcal'] = $db->table('testdefcal')
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName') ->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left') ->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
@ -91,32 +89,30 @@ class TestDefSiteModel extends BaseModel {
->where('testdefcal.EndDate IS NULL') ->where('testdefcal.EndDate IS NULL')
->get()->getResultArray(); ->get()->getResultArray();
// Load test mappings
$testMapModel = new \App\Models\Test\TestMapModel(); $testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
} elseif ($typeCode === 'GROUP') { } elseif ($typeCode === 'GROUP') {
// Load group members with test details
$row['testdefgrp'] = $db->table('testdefgrp') $row['testdefgrp'] = $db->table('testdefgrp')
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode') ->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType')
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left') ->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
->join('valueset vs', 'vs.VID=t.TestType', 'left')
->where('testdefgrp.TestSiteID', $TestSiteID) ->where('testdefgrp.TestSiteID', $TestSiteID)
->where('testdefgrp.EndDate IS NULL') ->where('testdefgrp.EndDate IS NULL')
->orderBy('testdefgrp.TestGrpID', 'ASC') ->orderBy('testdefgrp.TestGrpID', 'ASC')
->get()->getResultArray(); ->get()->getResultArray();
// Load test mappings $row['testdefgrp'] = ValueSet::transformLabels($row['testdefgrp'], [
'TestType' => 'test_type',
]);
$testMapModel = new \App\Models\Test\TestMapModel(); $testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
} elseif ($typeCode === 'TITLE') { } elseif ($typeCode === 'TITLE') {
// Load test mappings only for TITLE type
$testMapModel = new \App\Models\Test\TestMapModel(); $testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) { } elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
// TEST or PARAM - load technical details with joined tables
$row['testdeftech'] = $db->table('testdeftech') $row['testdeftech'] = $db->table('testdeftech')
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName') ->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left') ->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
@ -125,7 +121,6 @@ class TestDefSiteModel extends BaseModel {
->where('testdeftech.EndDate IS NULL') ->where('testdeftech.EndDate IS NULL')
->get()->getResultArray(); ->get()->getResultArray();
// Load test mappings
$testMapModel = new \App\Models\Test\TestMapModel(); $testMapModel = new \App\Models\Test\TestMapModel();
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
} }

View File

@ -148,30 +148,30 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label class="label"> <label class="label">
<span class="label-text font-medium">ZIP Code</span> <span class="label-text font-medium">ZIP Code</span>
</label> </label>
<input <input
type="text" type="text"
class="input" class="input"
x-model="form.ZIP" x-model="form.ZIP"
placeholder="12345" placeholder="12345"
/> />
</div>
<div>
<label class="label">
<span class="label-text font-medium">Country</span>
</label>
<select class="select" x-model="form.Country">
<option value="">Select Country</option>
<template x-for="opt in countryOptions" :key="opt.key">
<option :value="opt.key" x-text="opt.value"></option>
</template>
</select>
</div>
</div> </div>
<div>
<label class="label">
<span class="label-text font-medium">Country</span>
</label>
<input
type="text"
class="input"
x-model="form.Country"
placeholder="Indonesia"
/>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -151,6 +151,7 @@ function accounts() {
loading: false, loading: false,
list: [], list: [],
keyword: "", keyword: "",
countryOptions: [],
// Form Modal // Form Modal
showModal: false, showModal: false,
@ -179,6 +180,21 @@ function accounts() {
// Lifecycle // Lifecycle
async init() { async init() {
await this.fetchList(); await this.fetchList();
await this.fetchCountryOptions();
},
// Fetch country options
async fetchCountryOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/account_Country`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.countryOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch country options:', err);
}
}, },
// Fetch account list // Fetch account list

View File

@ -92,6 +92,31 @@
</template> </template>
</select> </select>
</div> </div>
<div>
<label class="label">
<span class="label-text font-medium">Site Type</span>
</label>
<select class="select" x-model="form.SiteTypeID">
<option value="">Select Type</option>
<template x-for="opt in siteTypeOptions" :key="opt.key">
<option :value="opt.key" x-text="opt.value"></option>
</template>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="label">
<span class="label-text font-medium">Site Class</span>
</label>
<select class="select" x-model="form.SiteClassID">
<option value="">Select Class</option>
<template x-for="opt in siteClassOptions" :key="opt.key">
<option :value="opt.key" x-text="opt.value"></option>
</template>
</select>
</div>
<div> <div>
<label class="label"> <label class="label">
<span class="label-text font-medium">ME (Medical Examiner?)</span> <span class="label-text font-medium">ME (Medical Examiner?)</span>

View File

@ -57,7 +57,8 @@
<th>Site Name</th> <th>Site Name</th>
<th>Code</th> <th>Code</th>
<th>Account</th> <th>Account</th>
<th>Parent Site</th> <th>Type</th>
<th>Class</th>
<th class="text-center">Actions</th> <th class="text-center">Actions</th>
</tr> </tr>
</thead> </thead>
@ -88,7 +89,8 @@
</td> </td>
<td x-text="site.SiteCode || '-'"></td> <td x-text="site.SiteCode || '-'"></td>
<td x-text="site.AccountName || '-'"></td> <td x-text="site.AccountName || '-'"></td>
<td x-text="site.ParentName || '-'"></td> <td x-text="site.SiteTypeText || site.SiteTypeID || '-'"></td>
<td x-text="site.SiteClassText || site.SiteClassID || '-'"></td>
<td class="text-center"> <td class="text-center">
<div class="flex items-center justify-center gap-1"> <div class="flex items-center justify-center gap-1">
<button class="btn btn-ghost btn-sm btn-square" @click="editSite(site.SiteID)" title="Edit"> <button class="btn btn-ghost btn-sm btn-square" @click="editSite(site.SiteID)" title="Edit">
@ -172,10 +174,44 @@ function sites() {
deleteTarget: null, deleteTarget: null,
deleting: false, deleting: false,
// Lookup Options
siteTypeOptions: [],
siteClassOptions: [],
// Lifecycle // Lifecycle
async init() { async init() {
await this.fetchList(); await this.fetchList();
await this.fetchAccounts(); await this.fetchAccounts();
await this.fetchSiteTypeOptions();
await this.fetchSiteClassOptions();
},
// Fetch site type options
async fetchSiteTypeOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/site_type`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.siteTypeOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch site type options:', err);
}
},
// Fetch site class options
async fetchSiteClassOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/site_class`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.siteClassOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch site class options:', err);
}
}, },
// Fetch site list // Fetch site list

View File

@ -87,8 +87,9 @@
</label> </label>
<select class="select" x-model="form.Type"> <select class="select" x-model="form.Type">
<option value="">Select Type</option> <option value="">Select Type</option>
<option value="1">Manual</option> <template x-for="opt in typeOptions" :key="opt.key">
<option value="2">Automated</option> <option :value="opt.key" x-text="opt.value"></option>
</template>
</select> </select>
</div> </div>
<div> <div>
@ -96,8 +97,9 @@
<span class="label-text font-medium">Status</span> <span class="label-text font-medium">Status</span>
</label> </label>
<select class="select" x-model="form.Enable"> <select class="select" x-model="form.Enable">
<option value="1">Enabled</option> <template x-for="opt in enableOptions" :key="opt.key">
<option value="0">Disabled</option> <option :value="opt.key" x-text="opt.value"></option>
</template>
</select> </select>
</div> </div>
</div> </div>

View File

@ -89,7 +89,7 @@
<td class="font-mono text-sm" x-text="ws.WorkstationCode || '-'"></td> <td class="font-mono text-sm" x-text="ws.WorkstationCode || '-'"></td>
<td x-text="ws.DepartmentName || '-'"></td> <td x-text="ws.DepartmentName || '-'"></td>
<td> <td>
<span class="badge badge-sm" :class="ws.Enable == 1 ? 'badge-success' : 'badge-ghost'" x-text="ws.Enable == 1 ? 'Active' : 'Disabled'"></span> <span class="badge badge-sm" :class="ws.EnableText === 'Enabled' || ws.Enable === '1' ? 'badge-success' : 'badge-ghost'" x-text="ws.EnableText || (ws.Enable == 1 ? 'Enabled' : 'Disabled')"></span>
</td> </td>
<td class="text-center"> <td class="text-center">
<div class="flex items-center justify-center gap-1"> <div class="flex items-center justify-center gap-1">
@ -161,7 +161,7 @@ function workstations() {
WorkstationName: "", WorkstationName: "",
DepartmentID: "", DepartmentID: "",
Type: "", Type: "",
Enable: 1, Enable: "1",
LinkTo: "" LinkTo: ""
}, },
@ -170,10 +170,48 @@ function workstations() {
deleteTarget: null, deleteTarget: null,
deleting: false, deleting: false,
// Lookup Options
typeOptions: [],
enableOptions: [],
// Lifecycle // Lifecycle
async init() { async init() {
await this.fetchList(); await this.fetchList();
await this.fetchDepartments(); await this.fetchDepartments();
await this.fetchTypeOptions();
await this.fetchEnableOptions();
},
// Fetch workstation type options
async fetchTypeOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/ws_type`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.typeOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch type options:', err);
}
},
// Fetch enable options
async fetchEnableOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/enable_disable`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.enableOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch enable options:', err);
this.enableOptions = [
{ key: '1', value: 'Enabled' },
{ key: '0', value: 'Disabled' }
];
}
}, },
// Fetch workstation list // Fetch workstation list
@ -220,7 +258,7 @@ function workstations() {
WorkstationName: "", WorkstationName: "",
DepartmentID: "", DepartmentID: "",
Type: "", Type: "",
Enable: 1, Enable: "1",
LinkTo: "" LinkTo: ""
}; };
this.errors = {}; this.errors = {};

View File

@ -71,12 +71,12 @@
<label class="label"> <label class="label">
<span class="label-text font-medium">Cap Color</span> <span class="label-text font-medium">Cap Color</span>
</label> </label>
<input <select class="select" x-model="form.Color">
type="text" <option value="">Select Color</option>
class="input" <template x-for="opt in colorOptions" :key="opt.key">
x-model="form.Color" <option :value="opt.key" x-text="opt.value"></option>
placeholder="e.g. Gold, Red, Lavender" </template>
/> </select>
</div> </div>
</div> </div>
@ -96,23 +96,23 @@
<label class="label"> <label class="label">
<span class="label-text font-medium">Additive</span> <span class="label-text font-medium">Additive</span>
</label> </label>
<input <select class="select" x-model="form.Additive">
type="text" <option value="">Select Additive</option>
class="input" <template x-for="opt in additiveOptions" :key="opt.key">
x-model="form.Additive" <option :value="opt.key" x-text="opt.value"></option>
placeholder="SST / EDTA / Heparin" </template>
/> </select>
</div> </div>
<div> <div>
<label class="label"> <label class="label">
<span class="label-text font-medium">Class</span> <span class="label-text font-medium">Class</span>
</label> </label>
<input <select class="select" x-model="form.ConClass">
type="text" <option value="">Select Class</option>
class="input" <template x-for="opt in classOptions" :key="opt.key">
x-model="form.ConClass" <option :value="opt.key" x-text="opt.value"></option>
placeholder="Tube / Swab" </template>
/> </select>
</div> </div>
</div> </div>

View File

@ -90,10 +90,10 @@
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full border border-slate-300" :style="`background-color: ${con.Color || 'transparent'}`"></div> <div class="w-3 h-3 rounded-full border border-slate-300" :style="`background-color: ${con.Color || 'transparent'}`"></div>
<span x-text="con.ColorTxt || con.Color || '-'"></span> <span x-text="con.ColorText || con.Color || '-'"></span>
</div> </div>
</td> </td>
<td x-text="con.AdditiveTxt || con.Additive || '-'"></td> <td x-text="con.AdditiveText || con.Additive || '-'"></td>
<td class="text-center"> <td class="text-center">
<div class="flex items-center justify-center gap-1"> <div class="flex items-center justify-center gap-1">
<button class="btn btn-ghost btn-sm btn-square" @click="editContainer(con.ConDefID)" title="Edit"> <button class="btn btn-ghost btn-sm btn-square" @click="editContainer(con.ConDefID)" title="Edit">
@ -174,10 +174,60 @@ function containers() {
deleteTarget: null, deleteTarget: null,
deleting: false, deleting: false,
// Lookup Options
colorOptions: [],
additiveOptions: [],
classOptions: [],
// Lifecycle // Lifecycle
async init() { async init() {
await this.fetchList(); await this.fetchList();
await this.fetchSites(); await this.fetchSites();
await this.fetchColorOptions();
await this.fetchAdditiveOptions();
await this.fetchClassOptions();
},
// Fetch color options
async fetchColorOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/container_cap_color`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.colorOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch color options:', err);
}
},
// Fetch additive options
async fetchAdditiveOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/additive`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.additiveOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch additive options:', err);
}
},
// Fetch class options
async fetchClassOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/container_class`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.classOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch class options:', err);
}
}, },
// Fetch container list // Fetch container list

View File

@ -27,8 +27,8 @@
@keyup.enter="fetchList()" /> @keyup.enter="fetchList()" />
<select class="select w-40" x-model="filterType" @change="fetchList()"> <select class="select w-40" x-model="filterType" @change="fetchList()">
<option value="">All Types</option> <option value="">All Types</option>
<template x-for="(type, index) in (testTypes || [])" :key="(type?.VID ?? index)"> <template x-for="(type, index) in (testTypes || [])" :key="(type?.key ?? index)">
<option :value="type?.VID" x-text="(type?.VValue || '') + ' - ' + (type?.VDesc || '')"></option> <option :value="type?.key" x-text="(type?.value || '')"></option>
</template> </template>
</select> </select>
<button class="btn btn-primary" @click="fetchList()"> <button class="btn btn-primary" @click="fetchList()">
@ -440,7 +440,7 @@
// Fetch test types from valueset // Fetch test types from valueset
async fetchTestTypes() { async fetchTestTypes() {
try { try {
const res = await fetch(`${BASEURL}api/valuesetdef/27`, { const res = await fetch(`${BASEURL}api/valueset/test_type`, {
credentials: 'include' credentials: 'include'
}); });
if (!res.ok) throw new Error("HTTP error"); if (!res.ok) throw new Error("HTTP error");
@ -448,13 +448,12 @@
this.testTypes = data.data || []; this.testTypes = data.data || [];
} catch (err) { } catch (err) {
console.error('Failed to fetch test types:', err); console.error('Failed to fetch test types:', err);
// Fallback to hardcoded types
this.testTypes = [ this.testTypes = [
{ VID: 1, VValue: 'TEST', VDesc: 'Test' }, { key: 'TEST', value: 'Test' },
{ VID: 2, VValue: 'PARAM', VDesc: 'Parameter' }, { key: 'PARAM', value: 'Parameter' },
{ VID: 3, VValue: 'CALC', VDesc: 'Calculated' }, { key: 'CALC', value: 'Calculated Test' },
{ VID: 4, VValue: 'GROUP', VDesc: 'Group' }, { key: 'GROUP', value: 'Group Test' },
{ VID: 5, VValue: 'TITLE', VDesc: 'Title' } { key: 'TITLE', value: 'Title' }
]; ];
} }
}, },
@ -477,8 +476,7 @@
}, },
// Get type display name // Get type display name
getTypeName(vid) { getTypeName(value) {
const code = this.getTypeCode(vid);
const typeMap = { const typeMap = {
'TEST': 'Test', 'TEST': 'Test',
'PARAM': 'Parameter', 'PARAM': 'Parameter',
@ -486,7 +484,12 @@
'GROUP': 'Group', 'GROUP': 'Group',
'TITLE': 'Title' 'TITLE': 'Title'
}; };
return typeMap[code] || 'Test'; return typeMap[value] || value || 'Test';
},
// Get type code from value
getTypeCode(value) {
return value || '';
}, },
// Fetch test list // Fetch test list
@ -584,23 +587,11 @@
const data = await res.json(); const data = await res.json();
if (data.data) { if (data.data) {
const testData = data.data; const testData = data.data;
// Map TypeCode to TestType (VID) for proper dialog display
const typeCode = testData.TypeCode || '';
let testTypeVid = testData.TestType || null;
// Convert TypeCode string to VID if needed
if (!testTypeVid && typeCode) {
const typeMap = { 'TEST': 1, 'PARAM': 2, 'CALC': 3, 'GROUP': 4, 'TITLE': 5 };
testTypeVid = typeMap[typeCode] || null;
}
this.form = { this.form = {
...this.form, ...this.form,
...testData, ...testData,
TestType: testTypeVid, TestType: testData.TestType || '',
// Store TypeCode directly for dialog display TypeCode: testData.TestType || '',
TypeCode: typeCode,
// Preserve group members if editing group
groupMembers: testData.testdefgrp || [] groupMembers: testData.testdefgrp || []
}; };
this.showModal = true; this.showModal = true;
@ -699,7 +690,6 @@
const method = this.isEditing ? 'PATCH' : 'POST'; const method = this.isEditing ? 'PATCH' : 'POST';
const payload = { ...this.form }; const payload = { ...this.form };
// Handle group members for GROUP type
if (this.getTypeCode(this.form.TestType) === 'GROUP' && this.form.groupMembers?.length > 0) { if (this.getTypeCode(this.form.TestType) === 'GROUP' && this.form.groupMembers?.length > 0) {
payload.groupMembers = this.form.groupMembers; payload.groupMembers = this.form.groupMembers;
} }
@ -762,18 +752,6 @@
} }
}, },
// Get type code from VID (with fallback and null safety)
getTypeCode(vid) {
if (!vid) return '';
// First try to find in loaded testTypes
if (this.testTypes && Array.isArray(this.testTypes) && this.testTypes.length > 0) {
const type = this.testTypes.find(t => t && t.VID === vid);
if (type && type.VValue) return type.VValue;
}
// Fallback to hardcoded mapping
const typeMap = { 1: 'TEST', 2: 'PARAM', 3: 'CALC', 4: 'GROUP', 5: 'TITLE' };
return typeMap[vid] || '';
}
} }
} }
</script> </script>

View File

@ -95,11 +95,12 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="label"> <label class="label">
<span class="label-text font-medium">Gender</span> <span class="label-text font-medium">Sex</span>
</label> </label>
<select class="select" x-model="form.Gender"> <select class="select" x-model="form.Sex">
<option value="1">Male</option> <template x-for="opt in sexOptions" :key="opt.key">
<option value="2">Female</option> <option :value="opt.key" x-text="opt.value"></option>
</template>
</select> </select>
</div> </div>

View File

@ -92,7 +92,7 @@
<tr> <tr>
<th>Patient ID</th> <th>Patient ID</th>
<th>Name</th> <th>Name</th>
<th>Gender</th> <th>Sex</th>
<th>Birth Date</th> <th>Birth Date</th>
<th>Phone</th> <th>Phone</th>
<th class="text-center">Actions</th> <th class="text-center">Actions</th>
@ -134,8 +134,8 @@
<td> <td>
<span <span
class="badge badge-sm" class="badge badge-sm"
:class="patient.Gender == 1 ? 'badge-info' : 'badge-secondary'" :class="patient.Sex === 'M' ? 'badge-info' : patient.Sex === 'F' ? 'badge-secondary' : 'badge-ghost'"
x-text="patient.Gender == 1 ? 'Male' : patient.Gender == 2 ? 'Female' : '-'" x-text="patient.SexText || patient.Sex || '-'"
></span> ></span>
</td> </td>
<td x-text="formatDate(patient.Birthdate)"></td> <td x-text="formatDate(patient.Birthdate)"></td>
@ -227,7 +227,7 @@ function patients() {
NameFirst: "", NameFirst: "",
NameMiddle: "", NameMiddle: "",
NameLast: "", NameLast: "",
Gender: 1, Sex: "M",
Birthdate: "", Birthdate: "",
MobilePhone: "", MobilePhone: "",
EmailAddress1: "", EmailAddress1: "",
@ -242,9 +242,31 @@ function patients() {
deleteTarget: null, deleteTarget: null,
deleting: false, deleting: false,
// Lookup Options
sexOptions: [],
// Lifecycle // Lifecycle
async init() { async init() {
await this.fetchList(); await this.fetchList();
await this.fetchSexOptions();
},
// Fetch sex options from valueset
async fetchSexOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/gender`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.sexOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch sex options:', err);
this.sexOptions = [
{ key: 'M', value: 'Male' },
{ key: 'F', value: 'Female' }
];
}
}, },
// Fetch patient list // Fetch patient list
@ -299,7 +321,7 @@ function patients() {
NameFirst: "", NameFirst: "",
NameMiddle: "", NameMiddle: "",
NameLast: "", NameLast: "",
Gender: 1, Sex: "M",
Birthdate: "", Birthdate: "",
MobilePhone: "", MobilePhone: "",
EmailAddress1: "", EmailAddress1: "",

View File

@ -0,0 +1,645 @@
# Migration Plan: Valueset VID → VValue
## Overview
Transition from using database `valueset` table (VID as primary key) to the new `App\Libraries\ValueSet` library (VValue as key). This eliminates database joins for lookup values and uses JSON-based static lookup files.
## Current State
- Database `valueset` table with columns: `VID` (PK, INT), `VValue` (VARCHAR), `VDesc` (VARCHAR)
- 30+ places using `->join('valueset', 'valueset.VID = ...')`
- Selects use `valueset.VValue` and `valueset.VDesc` for display text
## Target State
- Use `App\Libraries\ValueSet` library with `getLabel(lookupName, key)` method
- Lookup names use table-prefixed PascalCase (e.g., `patient_Sex`, `test_TestType`, `container_ContainerCapColor`)
- All fields store `VValue` codes directly (e.g., '1', '2', 'M', 'F', 'TEST')
- Remove all `valueset` table joins from queries
- Keep raw field values for codes; use `ValueSet::getLabel()` for display text
- JSON files in `app/Libraries/Data/valuesets/` are already populated
---
## Phase 1: JSON Files Rename
Rename all JSON files in `app/Libraries/Data/valuesets/` to use table-prefixed PascalCase format:
| Old Name | New Name | Source Table | Field |
|----------|----------|--------------|-------|
| `gender.json` | `patient_Sex.json` | patient | Gender |
| `country.json` | `patient_Country.json` | patient | Country |
| `race.json` | `patient_Race.json` | patient | Race |
| `religion.json` | `patient_Religion.json` | patient | Religion |
| `ethnic.json` | `patient_Ethnic.json` | patient | Ethnic |
| `marital_status.json` | `patient_MaritalStatus.json` | patient | MaritalStatus |
| `death_indicator.json` | `patient_DeathIndicator.json` | patient | DeathIndicator |
| `test_type.json` | `test_TestType.json` | testdefsite | TestType |
| `container_cap_color.json` | `container_ContainerCapColor.json` | containerdef | Color |
| `container_class.json` | `container_ContainerClass.json` | containerdef | ConClass |
| `additive.json` | `container_Additive.json` | containerdef | Additive |
| `location_type.json` | `location_LocationType.json` | location | LocType |
| `ws_type.json` | `organization_WorkstationType.json` | workstation | Type |
| `enable_disable.json` | `organization_EnableDisable.json` | workstation | Enable |
| `site_type.json` | `organization_SiteType.json` | site | SiteTypeID |
| `site_class.json` | `organization_SiteClass.json` | site | SiteClassID |
| `numeric_ref_type.json` | `ref_NumericRefType.json` | refnum | NumRefType |
| `range_type.json` | `ref_RangeType.json` | refnum | RangeType |
| `text_ref_type.json` | `ref_TextRefType.json` | reftxt | TxtRefType |
| `reference_type.json` | `test_ReferenceType.json` | testdeftech | RefType |
| `math_sign.json` | `ref_MathSign.json` | refnum | LowSign, HighSign |
| `country.json` | `account_Country.json` | account | Country |
| ... | ... | ... | ... |
All lookup names use `{table}_{Field}` format for clarity and namespace isolation.
---
## Phase 2: Database Schema Migration
### Files to DELETE
| File | Action |
|------|--------|
| `app\Database\Seeds\ValueSetSeeder.php` | DELETE |
| `app\Database\Seeds\ValueSetCountrySeeder.php` | DELETE |
| `app\Database\Seeds\MinimalMasterDataSeeder.php` | DELETE |
| `app\Database\Seeds\PatientSeeder.php` | DELETE |
| `app\Database\Migrations\2025-09-15-130122_ValueSet.php` | DELETE |
### Migration: Modify Columns INT → VARCHAR(10)
**File:** `app\Database\Migrations\2026-01-12-000001_ValuesetVidToVvalue.php`
```php
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class ValuesetVidToVvalue extends Migration
{
public function up()
{
// patient table
$this->forge->modifyColumn('patient', [
'Gender' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Race' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Religion' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Ethnic' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'MaritalStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'DeathIndicator' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// testdefsite table
$this->forge->modifyColumn('testdefsite', [
'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
]);
// containerdef table
$this->forge->modifyColumn('containerdef', [
'Additive' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'ConClass' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Color' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// location table
$this->forge->modifyColumn('location', [
'LocType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// workstation table
$this->forge->modifyColumn('workstation', [
'Type' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'Enable' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// site table
$this->forge->modifyColumn('site', [
'SiteTypeID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'SiteClassID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// account table
$this->forge->modifyColumn('account', [
'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// refnum table
$this->forge->modifyColumn('refnum', [
'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'NumRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'RangeType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'LowSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'HighSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// reftxt table
$this->forge->modifyColumn('reftxt', [
'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
'TxtRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
]);
// orderstatus table
$this->forge->modifyColumn('orderstatus', [
'OrderStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
]);
}
public function down()
{
// Revert to INT
$this->forge->modifyColumn('patient', [
'Gender' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Race' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Religion' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Ethnic' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'MaritalStatus' => ['type' => 'TINYINT', 'null' => true],
'DeathIndicator' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('testdefsite', [
'TestType' => ['type' => 'INT', 'null' => false],
]);
$this->forge->modifyColumn('containerdef', [
'Additive' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'ConClass' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'Color' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('location', [
'LocType' => ['type' => 'INT', 'null' => true],
]);
$this->forge->modifyColumn('workstation', [
'Type' => ['type' => 'TINYINT', 'null' => true],
'Enable' => ['type' => 'INT', 'null' => true],
]);
$this->forge->modifyColumn('site', [
'SiteTypeID' => ['type' => 'INT', 'null' => true],
'SiteClassID' => ['type' => 'INT', 'null' => true],
]);
$this->forge->modifyColumn('account', [
'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('refnum', [
'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'NumRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'RangeType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'LowSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'HighSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('reftxt', [
'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'TxtRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
$this->forge->modifyColumn('orderstatus', [
'OrderStatus' => ['type' => 'INT', 'null' => false],
]);
}
}
```
**Note:** No data migration needed - dummy data will be lost. This is acceptable for development/testing environments.
---
## Phase 3: Library & Model Updates
### ValueSet Library - Update to read from JSON files
**File:** `app/Libraries/ValueSet.php`
Ensure the library reads from JSON files in `app/Libraries/Data/valuesets/`:
```php
<?php
namespace App\Libraries;
class ValueSet
{
private static $cache = [];
private static function loadFile(string $name): array
{
if (!isset(self::$cache[$name])) {
$path = APPPATH . 'Libraries/Data/valuesets/' . $name . '.json';
if (file_exists($path)) {
$content = file_get_contents($path);
self::$cache[$name] = json_decode($content, true)['values'] ?? [];
} else {
self::$cache[$name] = [];
}
}
return self::$cache[$name];
}
public static function getLabel(string $lookupName, string $key): ?string
{
$values = self::loadFile($lookupName);
foreach ($values as $item) {
if (($item['key'] ?? $item['value'] ?? null) === $key) {
return $item['value'] ?? $item['label'] ?? null;
}
}
return null;
}
public static function getOptions(string $lookupName): array
{
$values = self::loadFile($lookupName);
return array_map(function ($item) {
return [
'key' => $item['key'] ?? '',
'value' => $item['value'] ?? $item['label'] ?? '',
];
}, $values);
}
public static function transformLabels(array $data, array $fieldMappings): array
{
foreach ($data as &$row) {
foreach ($fieldMappings as $field => $lookupName) {
if (isset($row[$field]) && $row[$field] !== null) {
$row[$field . 'Text'] = self::getLabel($lookupName, $row[$field]) ?? '';
}
}
}
return $data;
}
}
```
### ValueSetModel - Deprecate or Repurpose
**File:** `app/Models/ValueSet/ValueSetModel.php`
Options:
1. **Deprecate entirely** - No longer needed after migration
2. **Repurpose for JSON file management** - Read/write to JSON files
3. **Keep as-is for backward compatibility** - If database valuesets are still needed
Recommended: Deprecate and remove references after migration.
---
## Phase 4: Model Changes
### Pattern for Model Updates
**Before:**
```php
$this->select("..., gender.VValue as Gender, gender.VDesc as GenderText")
->join('valueset gender', 'gender.VID = patient.Gender', 'left')
```
**After:**
```php
use App\Libraries\ValueSet;
$this->select("..., patient.Gender");
// After fetching:
$rows = ValueSet::transformLabels($rows, [
'Gender' => 'patient_Sex',
'Country' => 'patient_Country',
'Race' => 'patient_Race',
'Religion' => 'patient_Religion',
'Ethnic' => 'patient_Ethnic',
'DeathIndicator' => 'patient_DeathIndicator',
'MaritalStatus' => 'patient_MaritalStatus',
]);
```
### Models to Modify (8 files)
#### 1. `app/Models/Patient/PatientModel.php`
**Remove:**
- Line 27: `$this->join('valueset vs', 'vs.vid = Gender', 'left');`
- Lines 52-64: All `*.VID as *VID` aliases
- Lines 75-81: All `valueset.*` joins
**Add transformation in `getPatient()`:**
```php
$patient = ValueSet::transformLabels([$patient], [
'Gender' => 'patient_gender',
'Country' => 'patient_country',
'Race' => 'patient_race',
'Religion' => 'patient_religion',
'Ethnic' => 'patient_ethnic',
'DeathIndicator' => 'patient_death_indicator',
'MaritalStatus' => 'patient_marital_status',
])[0];
```
#### 2. `app/Models/Location/LocationModel.php`
**Remove:**
- Lines 18, 30: `->join("valueset v", "v.VID=location.loctype", ...)`
**Add transformation:**
```php
$rows = ValueSet::transformLabels($rows, [
'LocType' => 'location_LocationType',
]);
```
#### 3. `app/Models/Test/TestDefSiteModel.php`
**Remove:**
- Lines 42, 75, 103: `->join("valueset", "valueset.VID=...")`
**Add transformation:**
```php
$rows = ValueSet::transformLabels($rows, [
'TestType' => 'test_TestType',
]);
```
#### 4. `app/Models/Test/TestDefGrpModel.php`
**Remove:**
- Line 32: `->join('valueset vs', 'vs.VID=t.TestType', 'left')`
#### 5. `app/Models/Specimen/ContainerDefModel.php`
**Remove:**
- Lines 20-22, 37-39: All 6 `valueset.*` joins
**Add transformation:**
```php
$rows = ValueSet::transformLabels($rows, [
'Color' => 'container_ContainerCapColor',
'ConClass' => 'container_ContainerClass',
'Additive' => 'container_Additive',
]);
```
#### 6. `app/Models/Organization/SiteModel.php`
**Remove:**
- Lines 38-39: `->join('valueset sitetype'...)` and `->join('valueset siteclass'...)`
**Add transformation:**
```php
$row = ValueSet::transformLabels([$row], [
'SiteTypeID' => 'organization_SiteType',
'SiteClassID' => 'organization_SiteClass',
])[0];
```
#### 7. `app/Models/Organization/AccountModel.php`
**Remove:**
- Line 41: `->join('valueset country'...)`
**Remove from select:**
- Line 36: `country.VID as country`
**Add transformation in controller if needed:**
```php
$rows = ValueSet::transformLabels($rows, [
'Country' => 'account_Country',
]);
```
#### 8. `app/Models/Organization/WorkstationModel.php`
**Remove:**
- Lines 36-37: `->join('valueset wstype'...)` and `->join('valueset enable'...)`
**Add transformation:**
```php
$row = ValueSet::transformLabels([$row], [
'Type' => 'organization_WorkstationType',
'Enable' => 'organization_EnableDisable',
])[0];
```
---
## Phase 5: Controller Changes
### `app/Controllers/TestsController.php`
**Remove:**
- Line 69: `->join("valueset", "valueset.VID=testdefsite.TestType", "left")`
- Line 111: `->join("valueset", "valueset.VID=testdefsite.TestType", "left")`
- Line 140: `->join('valueset vs', 'vs.VID=t.TestType', 'left')`
**Replace `getVValue()` method:**
```php
private function getVValue($vsetID, $vid) {
// DEPRECATED - Use ValueSet::getLabel() instead
return null;
}
```
**Update references from `getVValue()` to `ValueSet::getLabel()`:**
```php
// Before:
'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']),
// After:
'NumRefTypeVValue' => \App\Libraries\ValueSet::getLabel('ref_NumericRefType', $r['NumRefType']),
```
**VSetID to Lookup Name Mapping:**
| VSetID | Constant | Lookup Name |
|--------|----------|-------------|
| 44 | `VALUESET_REF_TYPE` | `test_ReferenceType` |
| 45 | `VALUESET_RANGE_TYPE` | `ref_RangeType` |
| 46 | `VALUESET_NUM_REF_TYPE` | `ref_NumericRefType` |
| 47 | `VALUESET_TXT_REF_TYPE` | `ref_TextRefType` |
| 3 | `VALUESET_SEX` | `patient_Sex` |
| 41 | `VALUESET_MATH_SIGN` | `ref_MathSign` |
**Update `getValuesetOptions()` to use JSON:**
```php
private function getValuesetOptions($lookupName)
{
return \App\Libraries\ValueSet::getOptions($lookupName);
}
```
---
## Phase 6: API Endpoints - Replace with JSON-based endpoints
### New API Controller: `app/Controllers/ValueSetApiController.php`
```php
<?php
namespace App\Controllers;
use App\Libraries\ValueSet;
class ValueSetApiController extends \CodeIgniter\Controller
{
use \CodeIgniter\API\ResponseTrait;
public function index(string $lookupName)
{
$data = ValueSet::getOptions($lookupName);
return $this->respond([
'status' => 'success',
'data' => $data
], 200);
}
public function all()
{
$dir = APPPATH . 'Libraries/Data/valuesets/';
$files = glob($dir . '*.json');
$result = [];
foreach ($files as $file) {
$name = basename($file, '.json');
$result[] = [
'name' => $name,
'options' => ValueSet::getOptions($name)
];
}
return $this->respond([
'status' => 'success',
'data' => $result
], 200);
}
}
```
### Update Routes: `app/Config/Routes.php`
```php
$routes->group('api', function ($routes) {
$routes->get('valueset/(:segment)', 'ValueSetApiController::index/$1');
$routes->get('valueset', 'ValueSetApiController::all');
});
```
---
## Phase 7: View Updates
### 1. `app/Views/v2/master/valuesets/valuesets_index.php`
Repurpose to manage JSON-based valuesets instead of database table.
| Before | After |
|--------|-------|
| `fetch(...api/valueset...)` | `fetch(...api/valueset/lookupName...)` |
| Database CRUD operations | File-based CRUD operations |
### 2. `app/Views/v2/master/valuesets/valueset_nested_crud.php`
Repurpose for JSON file management.
### 3. `app/Views/v2/master/valuesets/valueset_dialog.php`
Update for JSON file format.
### 4. `app/Views/v2/master/tests/tests_index.php`
| Before | After |
|--------|-------|
| `type?.VID` | `type?.key` |
| `type?.VValue` | `type?.value` |
| `type?.VDesc` | `type?.label` |
| `{ VID: 1, VValue: 'TEST', ... }` | `{ key: 'TEST', value: 'Test', ... }` |
| `getTypeName(vid)` | `getTypeName(value)` |
| `api/valuesetdef/27` | `api/valueset/test_TestType` |
| Hardcoded fallback: `{ VID: 1, VValue: 'TEST', VDesc: 'Test' }` | `{ key: 'TEST', value: 'Test' }` |
### 5. Additional Views to Update
| View File | Fields to Update | Lookup Name |
|-----------|------------------|-------------|
| `app/Views/v2/patients/patients_index.php` | Gender, Country, Race, Religion, Ethnic, MaritalStatus, DeathIndicator | `patient_*` |
| `app/Views/v2/master/specimen/containers_index.php` | Color, ConClass, Additive | `container_*` |
| `app/Views/v2/master/organization/sites_index.php` | SiteTypeID, SiteClassID | `organization_*` |
| `app/Views/v2/master/organization/workstations_index.php` | Type, Enable | `organization_*` |
| `app/Views/v2/master/organization/accounts_index.php` | Country | `account_Country` |
| `app/Views/v2/master/organization/locations_index.php` | LocType | `location_LocationType` |
---
## Phase 8: Test Files Update
### `tests/feature/ValueSet/ValueSetApiControllerTest.php`
Update tests to use new JSON-based API endpoints.
### `tests/_support/v2/MasterTestCase.php`
Update any valueset-related test data setup.
---
## Testing Checklist
1. **Unit Tests**
- Test `ValueSet::getLabel('patient_Sex', '1')` returns 'Female'
- Test `ValueSet::getLabel('test_TestType', 'TEST')` returns 'Test'
- Test `ValueSet::getOptions('container_ContainerCapColor')` returns correct format
- Test `ValueSet::transformLabels()` with table-prefixed field mappings
2. **Integration Tests**
- Patient CRUD (create, read, update, delete)
- Test definition CRUD
- Location CRUD
- Container definition CRUD
- Organization (site, account, workstation) CRUD
3. **Manual Testing**
- Verify all dropdowns display correct labels
- Verify filtering by valueset fields works
- Verify form submissions save correct VValue codes
---
## Rollback Plan
1. Run migration `down()` to revert column types
2. Restore deleted seeders from git if needed
3. Restore deleted migration file from git
---
## Files Summary
| Phase | Action | Files |
|-------|--------|-------|
| 1 | RENAME | ~50 JSON files in `app/Libraries/Data/valuesets/` |
| 2 | DELETE | 5 seeders, 1 migration |
| 2 | CREATE | 1 migration (column changes) |
| 3 | UPDATE | 1 library (ValueSet.php), ValueSetModel.php deprecation |
| 4 | UPDATE | 8 Model files |
| 5 | UPDATE | 1 Controller, Routes |
| 6 | CREATE | 1 Controller (ValueSetApiController.php) |
| 6 | UPDATE | ~6+ View files |
| 8 | UPDATE | 2+ Test files |
---
## Estimated Effort
- Phase 1 (JSON Rename): 15 minutes
- Phase 2 (Migration): 30 minutes
- Phase 3 (Library + Model): 30 minutes
- Phase 4 (Models): 1.5 hours
- Phase 5 (Controller): 30 minutes
- Phase 6 (API + Views): 2 hours
- Phase 8 (Tests): 30 minutes
- Testing: 1 hour
**Total Estimated Time: ~7 hours**

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class MinimalMasterDataSeeder extends Seeder {
public function run() {
$db = \Config\Database::connect();
$orderStatuses = [
['VID' => 1, 'VSetID' => 11, 'VValue' => 'ORD', 'VDesc' => 'Ordered', 'VOrder' => 1],
['VID' => 2, 'VSetID' => 11, 'VValue' => 'SCH', 'VDesc' => 'Scheduled', 'VOrder' => 2],
['VID' => 3, 'VSetID' => 11, 'VValue' => 'ANA', 'VDesc' => 'Analysis', 'VOrder' => 3],
['VID' => 4, 'VSetID' => 11, 'VValue' => 'VER', 'VDesc' => 'Verified', 'VOrder' => 4],
['VID' => 5, 'VSetID' => 11, 'VValue' => 'REV', 'VDesc' => 'Reviewed', 'VOrder' => 5],
['VID' => 6, 'VSetID' => 11, 'VValue' => 'REP', 'VDesc' => 'Reported', 'VOrder' => 6],
];
$priorities = [
['VID' => 1, 'VSetID' => 10, 'VValue' => 'S', 'VDesc' => 'Stat', 'VOrder' => 1],
['VID' => 2, 'VSetID' => 10, 'VValue' => 'A', 'VDesc' => 'ASAP', 'VOrder' => 2],
['VID' => 3, 'VSetID' => 10, 'VValue' => 'R', 'VDesc' => 'Routine', 'VOrder' => 3],
['VID' => 4, 'VSetID' => 10, 'VValue' => 'P', 'VDesc' => 'Preop', 'VOrder' => 4],
];
$specimenTypes = [
['VID' => 1, 'VSetID' => 29, 'VValue' => 'BLD', 'VDesc' => 'Blood', 'VOrder' => 1],
['VID' => 2, 'VSetID' => 29, 'VValue' => 'SER', 'VDesc' => 'Serum', 'VOrder' => 2],
['VID' => 3, 'VSetID' => 29, 'VValue' => 'PLAS', 'VDesc' => 'Plasma', 'VOrder' => 3],
['VID' => 4, 'VSetID' => 29, 'VValue' => 'UR', 'VDesc' => 'Urine', 'VOrder' => 4],
['VID' => 5, 'VSetID' => 29, 'VValue' => 'CSF', 'VDesc' => 'Cerebrospinal Fluid', 'VOrder' => 5],
];
$testTypes = [
['VID' => 1, 'VSetID' => 27, 'VValue' => 'TEST', 'VDesc' => 'Test', 'VOrder' => 1],
['VID' => 2, 'VSetID' => 27, 'VValue' => 'PARAM', 'VDesc' => 'Parameter', 'VOrder' => 2],
['VID' => 3, 'VSetID' => 27, 'VValue' => 'CALC', 'VDesc' => 'Calculated Test', 'VOrder' => 3],
['VID' => 4, 'VSetID' => 27, 'VValue' => 'GROUP', 'VDesc' => 'Group Test', 'VOrder' => 4],
['VID' => 5, 'VSetID' => 27, 'VValue' => 'TITLE', 'VDesc' => 'Title', 'VOrder' => 5],
];
$genders = [
['VID' => 1, 'VSetID' => 3, 'VValue' => '1', 'VDesc' => 'Female', 'VOrder' => 1],
['VID' => 2, 'VSetID' => 3, 'VValue' => '2', 'VDesc' => 'Male', 'VOrder' => 2],
['VID' => 3, 'VSetID' => 3, 'VValue' => '3', 'VDesc' => 'Unknown', 'VOrder' => 3],
];
foreach ($orderStatuses as $row) {
$exists = $db->table('valueset')->where('VSetID', $row['VSetID'])->where('VValue', $row['VValue'])->get()->getRow();
if (!$exists) {
$db->table('valueset')->insert($row);
}
}
foreach ($priorities as $row) {
$exists = $db->table('valueset')->where('VSetID', $row['VSetID'])->where('VValue', $row['VValue'])->get()->getRow();
if (!$exists) {
$db->table('valueset')->insert($row);
}
}
foreach ($specimenTypes as $row) {
$exists = $db->table('valueset')->where('VSetID', $row['VSetID'])->where('VValue', $row['VValue'])->get()->getRow();
if (!$exists) {
$db->table('valueset')->insert($row);
}
}
foreach ($testTypes as $row) {
$exists = $db->table('valueset')->where('VSetID', $row['VSetID'])->where('VValue', $row['VValue'])->get()->getRow();
if (!$exists) {
$db->table('valueset')->insert($row);
}
}
foreach ($genders as $row) {
$exists = $db->table('valueset')->where('VSetID', $row['VSetID'])->where('VValue', $row['VValue'])->get()->getRow();
if (!$exists) {
$db->table('valueset')->insert($row);
}
}
$counterExists = $db->table('counter')->where('CounterName', 'ORDER')->get()->getRow();
if (!$counterExists) {
$db->table('counter')->insert(['CounterName' => 'ORDER', 'CounterValue' => 1]);
}
$siteExists = $db->table('site')->where('SiteCode', '00')->get()->getRow();
if (!$siteExists) {
$db->table('site')->insert([
'SiteCode' => '00',
'SiteName' => 'Main Laboratory',
'SiteType' => 'PHL',
'CreateDate' => date('Y-m-d H:i:s')
]);
}
echo "Minimal master data seeded successfully.\n";
}
}

View File

@ -33,7 +33,7 @@ class PatientCreateTest extends CIUnitTestCase
"NameLast"=> "Wijaya", "NameLast"=> "Wijaya",
"Suffix"=> "S.kom", "Suffix"=> "S.kom",
"NameAlias"=> "Bud", "NameAlias"=> "Bud",
"Gender"=> "1", "Sex"=> "1",
]; ];
$result = $this->withBodyFormat('json')->call('post', 'api/patient', $payload); $result = $this->withBodyFormat('json')->call('post', 'api/patient', $payload);
$result->assertStatus(400); $result->assertStatus(400);
@ -57,7 +57,7 @@ class PatientCreateTest extends CIUnitTestCase
"NameLast" => $faker->lastName, "NameLast" => $faker->lastName,
"Suffix" => "S.Kom", "Suffix" => "S.Kom",
"NameAlias" => $faker->userName, "NameAlias" => $faker->userName,
"Gender" => $faker->numberBetween(5, 6), "Sex" => $faker->numberBetween(5, 6),
"PlaceOfBirth" => $faker->city, "PlaceOfBirth" => $faker->city,
"Birthdate" => $faker->date('Y-m-d'), "Birthdate" => $faker->date('Y-m-d'),
"ZIP" => $faker->postcode, "ZIP" => $faker->postcode,
@ -118,7 +118,7 @@ class PatientCreateTest extends CIUnitTestCase
"NameLast" => $faker->lastName, "NameLast" => $faker->lastName,
"Suffix" => "S.Kom", "Suffix" => "S.Kom",
"NameAlias" => $faker->userName, "NameAlias" => $faker->userName,
"Gender" => $faker->numberBetween(5, 6), "Sex" => $faker->numberBetween(5, 6),
"PlaceOfBirth" => $faker->city, "PlaceOfBirth" => $faker->city,
"Birthdate" => $faker->date('Y-m-d'), "Birthdate" => $faker->date('Y-m-d'),
"ZIP" => $faker->postcode, "ZIP" => $faker->postcode,
@ -177,7 +177,7 @@ class PatientCreateTest extends CIUnitTestCase
"NameLast" => $faker->lastName, "NameLast" => $faker->lastName,
"Suffix" => "S.Kom", "Suffix" => "S.Kom",
"NameAlias" => $faker->userName, "NameAlias" => $faker->userName,
"Gender" => $faker->numberBetween(5, 6), "Sex" => $faker->numberBetween(5, 6),
"PlaceOfBirth" => $faker->city, "PlaceOfBirth" => $faker->city,
"Birthdate" => $faker->date('Y-m-d'), "Birthdate" => $faker->date('Y-m-d'),
"ZIP" => $faker->postcode, "ZIP" => $faker->postcode,
@ -233,7 +233,7 @@ class PatientCreateTest extends CIUnitTestCase
"NameLast" => $faker->lastName, "NameLast" => $faker->lastName,
"Suffix" => "S.Kom", "Suffix" => "S.Kom",
"NameAlias" => $faker->userName, "NameAlias" => $faker->userName,
"Gender" => $faker->numberBetween(5, 6), "Sex" => $faker->numberBetween(5, 6),
"PlaceOfBirth" => $faker->city, "PlaceOfBirth" => $faker->city,
"Birthdate" => $faker->date('Y-m-d'), "Birthdate" => $faker->date('Y-m-d'),
"ZIP" => $faker->postcode, "ZIP" => $faker->postcode,
@ -293,7 +293,7 @@ class PatientCreateTest extends CIUnitTestCase
"NameLast" => $faker->lastName, "NameLast" => $faker->lastName,
"Suffix" => "S.Kom", "Suffix" => "S.Kom",
"NameAlias" => $faker->userName, "NameAlias" => $faker->userName,
"Gender" => $faker->numberBetween(5, 6), "Sex" => $faker->numberBetween(5, 6),
"PlaceOfBirth" => $faker->city, "PlaceOfBirth" => $faker->city,
"Birthdate" => $faker->date('Y-m-d'), "Birthdate" => $faker->date('Y-m-d'),
"ZIP" => $faker->postcode, "ZIP" => $faker->postcode,

View File

@ -223,7 +223,7 @@ class PatientDeleteTest extends CIUnitTestCase
// "Prefix" => "Mr.", // "Prefix" => "Mr.",
// "NameFirst" => "ToBeDeleted", // "NameFirst" => "ToBeDeleted",
// "NameLast" => "Patient", // "NameLast" => "Patient",
// "Gender" => "5", // "Sex" => "5",
// "Birthdate" => "1990-01-01", // "Birthdate" => "1990-01-01",
// ]; // ];
// //

View File

@ -41,7 +41,7 @@ class PatientUpdateTest extends CIUnitTestCase
"MobilePhone" => $faker->numerify('08##########'), "MobilePhone" => $faker->numerify('08##########'),
'NameFirst' => $faker->firstName, 'NameFirst' => $faker->firstName,
'NameLast' => $faker->lastName, 'NameLast' => $faker->lastName,
'Gender' => '1', 'Sex' => '1',
'Birthdate' => $faker->date('Y-m-d'), 'Birthdate' => $faker->date('Y-m-d'),
'PatIdt' => [ 'IdentifierType' => 'KTP', 'Identifier' => $faker->nik() ], 'PatIdt' => [ 'IdentifierType' => 'KTP', 'Identifier' => $faker->nik() ],
'PatCom' => $faker->sentence, 'PatCom' => $faker->sentence,
@ -71,7 +71,7 @@ class PatientUpdateTest extends CIUnitTestCase
'NameFirst' => $faker->firstName, 'NameFirst' => $faker->firstName,
'NameMiddle' => $faker->firstName, 'NameMiddle' => $faker->firstName,
'NameLast' => $faker->lastName, 'NameLast' => $faker->lastName,
'Gender' => '1', 'Sex' => '1',
'Birthdate' => $faker->date('Y-m-d'), 'Birthdate' => $faker->date('Y-m-d'),
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
"Phone" => $faker->numerify('08##########'), "Phone" => $faker->numerify('08##########'),
@ -115,7 +115,7 @@ class PatientUpdateTest extends CIUnitTestCase
'NameFirst' => $faker->firstName, 'NameFirst' => $faker->firstName,
'NameMiddle' => $faker->firstName, 'NameMiddle' => $faker->firstName,
'NameLast' => $faker->lastName, 'NameLast' => $faker->lastName,
'Gender' => '1', 'Sex' => '1',
'Birthdate' => $faker->date('Y-m-d'), 'Birthdate' => $faker->date('Y-m-d'),
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
"Phone" => $faker->numerify('08##########'), "Phone" => $faker->numerify('08##########'),
@ -155,7 +155,7 @@ class PatientUpdateTest extends CIUnitTestCase
'NameFirst' => $faker->firstName, 'NameFirst' => $faker->firstName,
'NameMiddle' => $faker->firstName, 'NameMiddle' => $faker->firstName,
'NameLast' => $faker->lastName, 'NameLast' => $faker->lastName,
'Gender' => '1', 'Sex' => '1',
'Birthdate' => $faker->date('Y-m-d'), 'Birthdate' => $faker->date('Y-m-d'),
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
"Phone" => $faker->numerify('08##########'), "Phone" => $faker->numerify('08##########'),
@ -193,7 +193,7 @@ class PatientUpdateTest extends CIUnitTestCase
'NameFirst' => $faker->firstName, 'NameFirst' => $faker->firstName,
'NameMiddle' => $faker->firstName, 'NameMiddle' => $faker->firstName,
'NameLast' => $faker->lastName, 'NameLast' => $faker->lastName,
'Gender' => '1', 'Sex' => '1',
'Birthdate' => $faker->date('Y-m-d'), 'Birthdate' => $faker->date('Y-m-d'),
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
"Phone" => $faker->numerify('08##########'), "Phone" => $faker->numerify('08##########'),
@ -233,7 +233,7 @@ class PatientUpdateTest extends CIUnitTestCase
'NameFirst' => $faker->firstName, 'NameFirst' => $faker->firstName,
'NameMiddle' => $faker->firstName, 'NameMiddle' => $faker->firstName,
'NameLast' => $faker->lastName, 'NameLast' => $faker->lastName,
'Gender' => '1', 'Sex' => '1',
'Birthdate' => $faker->date('Y-m-d'), 'Birthdate' => $faker->date('Y-m-d'),
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com', 'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
"Phone" => $faker->numerify('08##########'), "Phone" => $faker->numerify('08##########'),

View File

@ -0,0 +1,179 @@
<?php
namespace Tests\Feature\ValueSet;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
use App\Libraries\ValueSet;
class ValueSetApiControllerTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected function setUp(): void
{
parent::setUp();
ValueSet::clearCache();
}
public function testIndexReturnsAllLookups()
{
$result = $this->call('get', 'api/valueset');
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertArrayHasKey('gender', $data['data']);
$this->assertArrayHasKey('specimen_type', $data['data']);
$this->assertArrayHasKey('order_priority', $data['data']);
$this->assertArrayHasKey('specimen_status', $data['data']);
}
public function testShowByNameReturnsSingleLookup()
{
$result = $this->call('get', 'api/valueset/gender');
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertIsArray($data['data']);
$this->assertNotEmpty($data['data']);
$this->assertArrayHasKey('value', $data['data'][0]);
$this->assertArrayHasKey('label', $data['data'][0]);
}
public function testShowByNameGenderReturnsCorrectValues()
{
$result = $this->call('get', 'api/valueset/gender');
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$values = array_column($data['data'], 'value');
$labels = array_column($data['data'], 'label');
$this->assertContains('1', $values);
$this->assertContains('2', $values);
$this->assertContains('Female', $labels);
$this->assertContains('Male', $labels);
}
public function testShowByNameInvalidLookupReturns404()
{
$result = $this->call('get', 'api/valueset/nonexistent_lookup');
$result->assertStatus(404);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('error', $data['status']);
}
public function testShowByNameOrderPriority()
{
$result = $this->call('get', 'api/valueset/order_priority');
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertIsArray($data['data']);
$labels = array_column($data['data'], 'label');
$this->assertContains('Stat', $labels);
$this->assertContains('ASAP', $labels);
$this->assertContains('Routine', $labels);
}
public function testCreateReturns403()
{
$result = $this->call('post', 'api/valueset', [
'name' => 'test',
'values' => []
]);
$result->assertStatus(403);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('error', $data['status']);
$this->assertStringContainsString('disabled', $data['message']);
}
public function testUpdateReturns403()
{
$result = $this->call('patch', 'api/valueset', [
'name' => 'gender',
'values' => []
]);
$result->assertStatus(403);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('error', $data['status']);
$this->assertStringContainsString('disabled', $data['message']);
}
public function testDeleteReturns403()
{
$result = $this->call('delete', 'api/valueset', [
'name' => 'gender'
]);
$result->assertStatus(403);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('error', $data['status']);
$this->assertStringContainsString('disabled', $data['message']);
}
public function testIndexWithParamFiltersResults()
{
$result = $this->call('get', 'api/valueset', ['param' => 'gender']);
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertIsArray($data['data']);
}
public function testShowByNameSpecimenStatus()
{
$result = $this->call('get', 'api/valueset/specimen_status');
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertIsArray($data['data']);
$values = array_column($data['data'], 'value');
$labels = array_column($data['data'], 'label');
$this->assertContains('STC', $values);
$this->assertContains('SCtd', $values);
$this->assertContains('To be collected', $labels);
$this->assertContains('Collected', $labels);
}
}

View File

@ -114,7 +114,7 @@ class PatientModelTest extends CIUnitTestCase
$this->assertIsArray($allowedFields); $this->assertIsArray($allowedFields);
$this->assertContains('PatientID', $allowedFields); $this->assertContains('PatientID', $allowedFields);
$this->assertContains('NameFirst', $allowedFields); $this->assertContains('NameFirst', $allowedFields);
$this->assertContains('Gender', $allowedFields); $this->assertContains('Sex', $allowedFields);
$this->assertContains('Birthdate', $allowedFields); $this->assertContains('Birthdate', $allowedFields);
$this->assertContains('EmailAddress1', $allowedFields); $this->assertContains('EmailAddress1', $allowedFields);
$this->assertContains('Phone', $allowedFields); $this->assertContains('Phone', $allowedFields);

View File

@ -0,0 +1,110 @@
<?php
namespace Tests\Unit\Lookups;
use CodeIgniter\Test\CIUnitTestCase;
use App\Libraries\Lookups;
class LookupsBackwardCompatibilityTest extends CIUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
\App\Libraries\ValueSet::clearCache();
}
public function testLookupsGetMethodWorks()
{
$result = Lookups::get('gender');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertArrayHasKey('value', $result[0]);
$this->assertArrayHasKey('label', $result[0]);
}
public function testLookupsGetRawMethodWorks()
{
$result = Lookups::getRaw('gender');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertArrayHasKey('key', $result[0]);
$this->assertArrayHasKey('value', $result[0]);
}
public function testLookupsGetRawGenderContainsExpectedData()
{
$result = Lookups::getRaw('gender');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$genderMap = array_combine($keys, $values);
$this->assertEquals('Female', $genderMap['1']);
$this->assertEquals('Male', $genderMap['2']);
}
public function testLookupsGetLabelMethodWorks()
{
$this->assertEquals('Female', Lookups::getLabel('gender', '1'));
$this->assertEquals('Male', Lookups::getLabel('gender', '2'));
$this->assertEquals('Stat', Lookups::getLabel('order_priority', 'S'));
}
public function testLookupsGetAllMethodWorks()
{
$result = Lookups::getAll();
$this->assertIsArray($result);
$this->assertArrayHasKey('gender', $result);
$this->assertArrayHasKey('specimen_type', $result);
$this->assertArrayHasKey('order_priority', $result);
}
public function testLookupsClearCacheMethodWorks()
{
Lookups::getAll();
$result = Lookups::clearCache();
$this->assertTrue($result);
}
public function testLookupsSameAsValueSet()
{
$lookupResult = Lookups::get('gender');
$valueSetResult = \App\Libraries\ValueSet::get('gender');
$this->assertEquals($lookupResult, $valueSetResult);
}
public function testLookupsBackwardCompatWithSpecimenStatus()
{
$result = Lookups::getRaw('specimen_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$statusMap = array_combine($keys, $values);
$this->assertEquals('To be collected', $statusMap['STC']);
$this->assertEquals('Collected', $statusMap['SCtd']);
$this->assertEquals('In-transport', $statusMap['STran']);
}
public function testLookupsBackwardCompatWithMaritalStatus()
{
$result = Lookups::getRaw('marital_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$statusMap = array_combine($keys, $values);
$this->assertEquals('Married', $statusMap['M']);
$this->assertEquals('Single', $statusMap['S']);
$this->assertEquals('Divorced', $statusMap['D']);
}
public function testLookupsBackwardCompatWithReligion()
{
$result = Lookups::getRaw('religion');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$religionMap = array_combine($keys, $values);
$this->assertEquals('Islam', $religionMap['ISLAM']);
$this->assertEquals('Kristen', $religionMap['KRSTN']);
$this->assertEquals('Katolik', $religionMap['KTLIK']);
}
}

View File

@ -0,0 +1,380 @@
<?php
namespace Tests\Unit\ValueSet;
use CodeIgniter\Test\CIUnitTestCase;
use App\Libraries\ValueSet;
class ValueSetTest extends CIUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
ValueSet::clearCache();
}
public function testGetPatientSexReturnsFormattedArray()
{
$result = ValueSet::get('gender');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertArrayHasKey('value', $result[0]);
$this->assertArrayHasKey('label', $result[0]);
}
public function testGetPatientSexContainsExpectedValues()
{
$result = ValueSet::get('gender');
$values = array_column($result, 'value');
$labels = array_column($result, 'label');
$this->assertContains('1', $values);
$this->assertContains('2', $values);
$this->assertContains('3', $values);
$this->assertContains('Female', $labels);
$this->assertContains('Male', $labels);
$this->assertContains('Unknown', $labels);
}
public function testGetRawReturnsArrayOfKeyValuePairs()
{
$result = ValueSet::getRaw('gender');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertArrayHasKey('key', $result[0]);
$this->assertArrayHasKey('value', $result[0]);
}
public function testGetRawPatientSexContainsExpectedData()
{
$result = ValueSet::getRaw('gender');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$this->assertContains('1', $keys);
$this->assertContains('2', $keys);
$this->assertContains('Female', $values);
$this->assertContains('Male', $values);
}
public function testGetLabelConvertsCodeToLabel()
{
$this->assertEquals('Female', ValueSet::getLabel('gender', '1'));
$this->assertEquals('Male', ValueSet::getLabel('gender', '2'));
$this->assertEquals('Unknown', ValueSet::getLabel('gender', '3'));
}
public function testGetLabelForOrderPriority()
{
$this->assertEquals('Stat', ValueSet::getLabel('order_priority', 'S'));
$this->assertEquals('ASAP', ValueSet::getLabel('order_priority', 'A'));
$this->assertEquals('Routine', ValueSet::getLabel('order_priority', 'R'));
$this->assertEquals('Preop', ValueSet::getLabel('order_priority', 'P'));
}
public function testGetLabelReturnsNullForInvalidKey()
{
$this->assertNull(ValueSet::getLabel('gender', '99'));
$this->assertNull(ValueSet::getLabel('gender', 'invalid'));
}
public function testGetLabelReturnsNullForInvalidLookup()
{
$this->assertNull(ValueSet::getLabel('nonexistent_lookup', '1'));
}
public function testGetReturnsNullForInvalidLookup()
{
$result = ValueSet::get('nonexistent_lookup');
$this->assertNull($result);
}
public function testGetAllReturnsMultipleLookups()
{
$result = ValueSet::getAll();
$this->assertIsArray($result);
$this->assertArrayHasKey('gender', $result);
$this->assertArrayHasKey('specimen_type', $result);
$this->assertArrayHasKey('order_priority', $result);
$this->assertArrayHasKey('specimen_status', $result);
}
public function testGetAllContainsValuesKey()
{
$result = ValueSet::getAll();
$this->assertIsArray($result['gender']['values']);
$this->assertNotEmpty($result['gender']['values']);
}
public function testGetAllContainsMetadata()
{
$result = ValueSet::getAll();
$this->assertArrayHasKey('VSetID', $result['gender']);
$this->assertArrayHasKey('VSName', $result['gender']);
$this->assertArrayHasKey('VCategory', $result['gender']);
$this->assertArrayHasKey('values', $result['gender']);
}
public function testGetPatientSex()
{
$result = ValueSet::get('gender');
$this->assertEquals('1', $result[0]['value']);
$this->assertEquals('Female', $result[0]['label']);
$this->assertEquals('2', $result[1]['value']);
$this->assertEquals('Male', $result[1]['label']);
}
public function testGetSpecimenStatus()
{
$result = ValueSet::getRaw('specimen_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$this->assertContains('STC', $keys);
$this->assertContains('SCtd', $keys);
$this->assertContains('STran', $keys);
$this->assertContains('SArrv', $keys);
$this->assertContains('SRejc', $keys);
$this->assertContains('SRcvd', $keys);
$statusMap = array_combine($keys, $values);
$this->assertEquals('To be collected', $statusMap['STC']);
$this->assertEquals('Collected', $statusMap['SCtd']);
$this->assertEquals('In-transport', $statusMap['STran']);
}
public function testGetOrderPriority()
{
$result = ValueSet::getRaw('order_priority');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$this->assertContains('S', $keys);
$this->assertContains('A', $keys);
$this->assertContains('R', $keys);
$priorityMap = array_combine($keys, $values);
$this->assertEquals('Stat', $priorityMap['S']);
$this->assertEquals('ASAP', $priorityMap['A']);
$this->assertEquals('Routine', $priorityMap['R']);
$this->assertEquals('Preop', $priorityMap['P']);
}
public function testGetSpecimenType()
{
$result = ValueSet::getRaw('specimen_type');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$typeMap = array_combine($keys, $values);
$this->assertEquals('Whole blood', $typeMap['BLD']);
$this->assertEquals('Serum', $typeMap['SER']);
$this->assertEquals('Plasma', $typeMap['PLAS']);
$this->assertEquals('Urine', $typeMap['UR']);
}
public function testGetMaritalStatus()
{
$result = ValueSet::getRaw('marital_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$statusMap = array_combine($keys, $values);
$this->assertEquals('Separated', $statusMap['A']);
$this->assertEquals('Divorced', $statusMap['D']);
$this->assertEquals('Married', $statusMap['M']);
$this->assertEquals('Single', $statusMap['S']);
$this->assertEquals('Widowed', $statusMap['W']);
}
public function testGetPatientReligion()
{
$result = ValueSet::getRaw('religion');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$religionMap = array_combine($keys, $values);
$this->assertEquals('Islam', $religionMap['ISLAM']);
$this->assertEquals('Kristen', $religionMap['KRSTN']);
$this->assertEquals('Katolik', $religionMap['KTLIK']);
$this->assertEquals('Hindu', $religionMap['HINDU']);
$this->assertEquals('Budha', $religionMap['BUDHA']);
}
public function testGetResultType()
{
$result = ValueSet::getRaw('result_type');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$typeMap = array_combine($keys, $values);
$this->assertEquals('Numeric', $typeMap['NMRIC']);
$this->assertEquals('Range', $typeMap['RANGE']);
$this->assertEquals('Text', $typeMap['TEXT']);
$this->assertEquals('Value set', $typeMap['VSET']);
}
public function testClearCacheDoesNotThrowError()
{
ValueSet::getAll();
$result = ValueSet::clearCache();
$this->assertTrue($result);
}
public function testGetOrderPriorityFromAlias()
{
$result = ValueSet::getRaw('priority');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$priorityMap = array_combine($keys, $values);
$this->assertEquals('Stat', $priorityMap['S']);
$this->assertEquals('Routine', $priorityMap['R']);
}
public function testGetTestStatus()
{
$result = ValueSet::getRaw('test_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$statusMap = array_combine($keys, $values);
$this->assertEquals('Waiting for Results', $statusMap['PENDING']);
$this->assertEquals('Analyzing', $statusMap['IN_PROCESS']);
$this->assertEquals('Verified & Signed', $statusMap['VERIFIED']);
}
public function testGetOrderRequestStatus()
{
$result = ValueSet::getRaw('request_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$statusMap = array_combine($keys, $values);
$this->assertEquals('To be collected', $statusMap['STC']);
$this->assertEquals('Collected', $statusMap['SCtd']);
}
public function testGetResultResultStatus()
{
$result = ValueSet::getRaw('result_status');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$statusMap = array_combine($keys, $values);
$this->assertEquals('Preliminary', $statusMap['PRELIMINARY']);
$this->assertEquals('Final', $statusMap['FINAL']);
$this->assertEquals('Corrected', $statusMap['CORRECTED']);
}
public function testGetReturnsFormattedValues()
{
$result = ValueSet::get('gender');
$this->assertEquals('1', $result[0]['value']);
$this->assertEquals('Female', $result[0]['label']);
$this->assertEquals('2', $result[1]['value']);
$this->assertEquals('Male', $result[1]['label']);
}
public function testGetWithSpecialCharactersInKey()
{
$result = ValueSet::get('result_unit');
$values = array_column($result, 'value');
$this->assertContains('g/dL', $values);
$this->assertContains('mg/dL', $values);
$this->assertContains('x106/mL', $values);
}
public function testGetMathSigns()
{
$result = ValueSet::getRaw('math_sign');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$signMap = array_combine($keys, $values);
$this->assertEquals('Equal', $signMap['=']);
$this->assertEquals('Less than', $signMap['<']);
$this->assertEquals('Greater than', $signMap['>']);
$this->assertEquals('Less than or equal to', $signMap['<=']);
$this->assertEquals('Greater than or equal to', $signMap['>=']);
}
public function testGetContainerCapColor()
{
$result = ValueSet::getRaw('container_cap_color');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$colorMap = array_combine($keys, $values);
$this->assertEquals('Purple', $colorMap['PRPL']);
$this->assertEquals('Red', $colorMap['RED']);
$this->assertEquals('Yellow', $colorMap['YLLW']);
$this->assertEquals('Green', $colorMap['GRN']);
}
public function testGetOrganizationSiteType()
{
$result = ValueSet::getRaw('site_type');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$siteMap = array_combine($keys, $values);
$this->assertEquals('Government Hospital', $siteMap['GH']);
$this->assertEquals('Private Hospital', $siteMap['PH']);
$this->assertEquals('Government Lab', $siteMap['GL']);
$this->assertEquals('Private Lab', $siteMap['PL']);
}
public function testGetPatEntityType()
{
$result = ValueSet::getRaw('entity_type');
$keys = array_column($result, 'key');
$values = array_column($result, 'value');
$entityMap = array_combine($keys, $values);
$this->assertEquals('HIS', $entityMap['HIS']);
$this->assertEquals('Site', $entityMap['SITE']);
$this->assertEquals('Workstation', $entityMap['WST']);
$this->assertEquals('Equipment/Instrument', $entityMap['INST']);
}
public function testGetTestType()
{
$result = ValueSet::get('test_type');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$values = array_column($result, 'value');
$this->assertContains('TEST', $values);
$this->assertContains('PARAM', $values);
$this->assertContains('CALC', $values);
$this->assertContains('GROUP', $values);
$this->assertContains('TITLE', $values);
}
public function testTransformLabels()
{
$data = [
['Gender' => '1', 'Country' => 'ID'],
['Gender' => '2', 'Country' => 'US']
];
$result = ValueSet::transformLabels($data, [
'Gender' => 'gender',
'Country' => 'country'
]);
$this->assertEquals('Female', $result[0]['GenderText']);
$this->assertEquals('Male', $result[1]['GenderText']);
}
public function testGetOptions()
{
$result = ValueSet::getOptions('gender');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertArrayHasKey('key', $result[0]);
$this->assertArrayHasKey('value', $result[0]);
}
}