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:
parent
f11bde4d30
commit
bb7df6b70c
@ -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
221
AGENTS.md
Normal 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
174
CLAUDE.md
@ -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
284
MVP_TODO.md
Normal 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
|
||||
@ -149,11 +149,11 @@ $routes->group('api', function ($routes) {
|
||||
// ValueSet
|
||||
$routes->group('valueset', function ($routes) {
|
||||
$routes->get('/', 'ValueSet\ValueSetController::index');
|
||||
$routes->get('(:num)', 'ValueSet\ValueSetController::show/$1');
|
||||
$routes->get('valuesetdef/(:num)', 'ValueSet\ValueSetController::showByValueSetDef/$1');
|
||||
$routes->get('(:any)', 'ValueSet\ValueSetController::showByName/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSetController::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSetController::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSetController::delete');
|
||||
$routes->post('refresh', 'ValueSet\ValueSetController::refresh');
|
||||
});
|
||||
|
||||
$routes->group('valuesetdef', function ($routes) {
|
||||
@ -164,6 +164,17 @@ $routes->group('api', function ($routes) {
|
||||
$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
|
||||
$routes->group('counter', function ($routes) {
|
||||
$routes->get('/', 'CounterController::index');
|
||||
@ -279,7 +290,23 @@ $routes->group('api', function ($routes) {
|
||||
$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->post('results', 'EdgeController::results');
|
||||
$routes->get('orders', 'EdgeController::orders');
|
||||
|
||||
@ -3,215 +3,190 @@ namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
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 {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $model;
|
||||
protected $patientModel;
|
||||
protected $visitModel;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->rulesOrderTest = [
|
||||
'NameFirst' => 'required'
|
||||
$this->model = new OrderTestModel();
|
||||
$this->patientModel = new PatientModel();
|
||||
$this->visitModel = new PatVisitModel();
|
||||
$this->rules = [
|
||||
'InternalPID' => 'required|is_natural'
|
||||
];
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$rows = $this->db->table('ordertest')->select("*")->get()->getResultArray();
|
||||
$internalPID = $this->request->getVar('InternalPID');
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "no Data.",
|
||||
'data' => [],
|
||||
], 200);
|
||||
try {
|
||||
if ($internalPID) {
|
||||
$rows = $this->model->getOrdersByPatient($internalPID);
|
||||
} else {
|
||||
$rows = $this->db->table('ordertest')
|
||||
->where('DelDate', null)
|
||||
->orderBy('OrderDateTime', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message'=> "fetch success",
|
||||
'data' => $rows,
|
||||
'message' => 'Data fetched successfully',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function show($OrderID = null) {
|
||||
$row=$this->db->table('ordertest')->select("*")->where('OrderID', $OrderID)->get()->getRowArray();
|
||||
|
||||
public function show($orderID = null) {
|
||||
try {
|
||||
$row = $this->model->getOrder($orderID);
|
||||
if (empty($row)) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Data not found.",
|
||||
'data' => null,
|
||||
'message' => 'Data not found.',
|
||||
'data' => null
|
||||
], 200);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message'=> "Data fetched successfully",
|
||||
'data' => $row,
|
||||
'message' => 'Data fetched successfully',
|
||||
'data' => $row
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function create() {
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Prepare data
|
||||
$dataOrderTest = $this->prepareOrderTestData($input);
|
||||
$dataOrderCom = $this->prepareOrderComData($input);
|
||||
$dataOrderAtt = $this->prepareOrderAttData($input);
|
||||
|
||||
if (!$this->validateData($dataLocation, $this->rules)) {
|
||||
if (!$this->validateData($input, $this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
$this->db->transStart();
|
||||
|
||||
// Insert location
|
||||
$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);
|
||||
try {
|
||||
if (!$this->patientModel->find($input['InternalPID'])) {
|
||||
return $this->failValidationErrors(['InternalPID' => 'Patient not found']);
|
||||
}
|
||||
|
||||
// Complete transaction
|
||||
$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')
|
||||
);
|
||||
if (!empty($input['PatVisitID'])) {
|
||||
$visit = $this->visitModel->find($input['PatVisitID']);
|
||||
if (!$visit) {
|
||||
return $this->failValidationErrors(['PatVisitID' => 'Visit not found']);
|
||||
}
|
||||
}
|
||||
|
||||
$orderID = $this->model->createOrder($input);
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Location created successfully',
|
||||
'data' => $dataLocation,
|
||||
'message' => 'Order created successfully',
|
||||
'data' => ['OrderID' => $orderID]
|
||||
], 201);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Ensure rollback if something goes wrong
|
||||
if ($this->db->transStatus() !== false) {
|
||||
$this->db->transRollback();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function update() {
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Prepare data
|
||||
$dataLocation = $this->prepareLocationData($input);
|
||||
$dataLocationAddress = $this->prepareLocationAddressData($input);
|
||||
|
||||
if (!$this->validateData($dataLocation, $this->rules)) {
|
||||
return $this->failValidationErrors( $this->validator->getErrors());
|
||||
if (empty($input['OrderID'])) {
|
||||
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
$this->db->transStart();
|
||||
|
||||
// Insert location
|
||||
$this->db->table('location')->where('LocationID', $dataLocation["LocationID"])->update($dataLocation);
|
||||
|
||||
// Insert address if available
|
||||
if (!empty($dataLocationAddress)) {
|
||||
$dataLocationAddress['LocationID'] = $input["LocationID"];
|
||||
$this->db->table('locationaddress')->upsert($dataLocationAddress);
|
||||
try {
|
||||
$order = $this->model->getOrder($input['OrderID']);
|
||||
if (!$order) {
|
||||
return $this->failNotFound('Order not found');
|
||||
}
|
||||
|
||||
// Complete transaction
|
||||
$this->db->transComplete();
|
||||
$updateData = [];
|
||||
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'];
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
$dbError = $this->db->error();
|
||||
return $this->failServerError(
|
||||
'Failed to update location data (transaction rolled back): ' . ($dbError['message'] ?? 'Unknown database error')
|
||||
);
|
||||
if (!empty($updateData)) {
|
||||
$this->model->update($input['OrderID'], $updateData);
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
return $this->respond([
|
||||
'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();
|
||||
}
|
||||
'message' => 'Order updated successfully',
|
||||
'data' => $this->model->getOrder($input['OrderID'])
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
$LocationID = $input["LocationID"];
|
||||
if (!$LocationID) {
|
||||
return $this->failValidationError('LocationID is required.');
|
||||
$orderID = $input['OrderID'] ?? null;
|
||||
|
||||
if (empty($orderID)) {
|
||||
return $this->failValidationErrors(['OrderID' => 'OrderID is required']);
|
||||
}
|
||||
|
||||
|
||||
$location = $this->db->table('location')->where('LocationID', $LocationID)->get()->getRow();
|
||||
if (!$location) {
|
||||
return $this->failNotFound("LocationID with {$LocationID} not found.");
|
||||
try {
|
||||
$order = $this->model->getOrder($orderID);
|
||||
if (!$order) {
|
||||
return $this->failNotFound('Order not found');
|
||||
}
|
||||
|
||||
$this->db->table('location')->where('LocationID', $LocationID)->update(['DelDate' => NOW()]);
|
||||
$this->model->softDelete($orderID);
|
||||
|
||||
return $this->respondDeleted([
|
||||
'status' => 'success',
|
||||
'message' => "Location with {$LocationID} deleted successfully."
|
||||
'message' => 'Order deleted successfully'
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Ensure rollback if something goes wrong
|
||||
if ($this->db->transStatus() !== false) {
|
||||
$this->db->transRollback();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareLocationData(array $input): array {
|
||||
$LinkTo = null;
|
||||
if (!empty($input['LinkTo'])) {
|
||||
$ids = array_column($input['LinkTo'], 'InternalPID');
|
||||
$LinkTo = implode(',', $ids);
|
||||
public function updateStatus() {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (empty($input['OrderID']) || empty($input['OrderStatus'])) {
|
||||
return $this->failValidationErrors(['error' => 'OrderID and OrderStatus are required']);
|
||||
}
|
||||
|
||||
$data = [
|
||||
"LocCode" => $input['LocCode'] ?? null,
|
||||
"Parent" => $input['Parent'] ?? null,
|
||||
"LocFull" => $input['LocFull'] ?? null,
|
||||
"Description" => $input['Description'] ?? null,
|
||||
];
|
||||
|
||||
if(!empty($input["LocationID"])) { $data["LocationID"] = $input["LocationID"]; }
|
||||
|
||||
return $data;
|
||||
$validStatuses = ['ORD', 'SCH', 'ANA', 'VER', 'REV', 'REP'];
|
||||
if (!in_array($input['OrderStatus'], $validStatuses)) {
|
||||
return $this->failValidationErrors(['OrderStatus' => 'Invalid status. Valid: ' . implode(', ', $validStatuses)]);
|
||||
}
|
||||
|
||||
private function prepareLocationAddressData(array $input): array {
|
||||
$data = [
|
||||
"LocationID" => $input['LocationID'] ?? null,
|
||||
"Street1" => $input['Street1'] ?? null,
|
||||
"Street2" => $input['Street2'] ?? null,
|
||||
"City" => $input['City'] ?? null,
|
||||
"Province" => $input['Province'] ?? null,
|
||||
"PostCode" => $input['PostCode'] ?? null,
|
||||
"GeoLocationSystem" => $input['GeoLocationSystem'] ?? null,
|
||||
"GeoLocationData" => $input['GeoLocationData'] ?? null,
|
||||
];
|
||||
try {
|
||||
$order = $this->model->getOrder($input['OrderID']);
|
||||
if (!$order) {
|
||||
return $this->failNotFound('Order not found');
|
||||
}
|
||||
|
||||
return $data;
|
||||
$this->model->updateStatus($input['OrderID'], $input['OrderStatus']);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Order status updated successfully',
|
||||
'data' => $this->model->getOrder($input['OrderID'])
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ class PatientController extends Controller {
|
||||
'PatientID' => 'required|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]',
|
||||
'Gender' => 'required',
|
||||
'Sex' => 'required',
|
||||
|
||||
'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]',
|
||||
|
||||
78
app/Controllers/Test/DemoOrderController.php
Normal file
78
app/Controllers/Test/DemoOrderController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class TestsController extends BaseController
|
||||
{
|
||||
@ -15,18 +16,9 @@ class TestsController extends BaseController
|
||||
protected $modelTech;
|
||||
protected $modelGrp;
|
||||
protected $modelMap;
|
||||
protected $modelValueSet;
|
||||
protected $modelRefNum;
|
||||
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()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
@ -35,11 +27,9 @@ class TestsController extends BaseController
|
||||
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
||||
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
||||
$this->modelMap = new \App\Models\Test\TestMapModel;
|
||||
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
||||
$this->modelRefNum = new \App\Models\RefRange\RefNumModel;
|
||||
$this->modelRefTxt = new \App\Models\RefRange\RefTxtModel;
|
||||
|
||||
// Validation rules for main test definition
|
||||
$this->rules = [
|
||||
'TestSiteCode' => 'required|min_length[3]|max_length[6]',
|
||||
'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()
|
||||
{
|
||||
$siteId = $this->request->getGet('SiteID');
|
||||
@ -64,9 +49,7 @@ class TestsController extends BaseController
|
||||
$builder = $this->db->table('testdefsite')
|
||||
->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate")
|
||||
->where('testdefsite.EndDate IS NULL');
|
||||
|
||||
if ($siteId) {
|
||||
@ -94,21 +77,20 @@ class TestsController extends BaseController
|
||||
if (empty($rows)) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests/{id}
|
||||
* GET /v1/tests/site/{id}
|
||||
* Get single test by ID with all related details
|
||||
*/
|
||||
public function show($id = null)
|
||||
{
|
||||
if (!$id)
|
||||
return $this->failValidationErrors('TestSiteID is required');
|
||||
|
||||
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
$row = $this->model->select("testdefsite.*")
|
||||
->where("testdefsite.TestSiteID", $id)
|
||||
->find($id);
|
||||
|
||||
@ -116,11 +98,13 @@ class TestsController extends BaseController
|
||||
return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => null], 200);
|
||||
}
|
||||
|
||||
// Load related details based on TestType
|
||||
$typeCode = $row['TypeCode'] ?? '';
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'TestType' => 'test_type',
|
||||
])[0];
|
||||
|
||||
$typeCode = $row['TestType'] ?? '';
|
||||
|
||||
if ($typeCode === 'CALC') {
|
||||
// Load calculation details
|
||||
$row['testdefcal'] = $this->db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
@ -129,29 +113,27 @@ class TestsController extends BaseController
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
// Load group members with test details
|
||||
$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('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $id)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->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();
|
||||
|
||||
} elseif ($typeCode === 'TITLE') {
|
||||
// Load test mappings only for TITLE type
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} else {
|
||||
// TEST or PARAM - load technical details
|
||||
$row['testdeftech'] = $this->db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
@ -160,50 +142,38 @@ class TestsController extends BaseController
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
// Load refnum/reftxt based on RefType
|
||||
if (!empty($row['testdeftech'])) {
|
||||
$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
|
||||
->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('Display', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// Add VValue for display
|
||||
$row['refnum'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefNumID' => $r['RefNumID'],
|
||||
'NumRefType' => (int) $r['NumRefType'],
|
||||
'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']),
|
||||
'RangeType' => (int) $r['RangeType'],
|
||||
'RangeTypeVValue' => $this->getVValue(45, $r['RangeType']),
|
||||
'Sex' => (int) $r['Sex'],
|
||||
'SexVValue' => $this->getVValue(3, $r['Sex']),
|
||||
'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']),
|
||||
'NumRefType' => $r['NumRefType'],
|
||||
'NumRefTypeVValue' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
|
||||
'RangeTypeVValue' => ValueSet::getLabel('range_type', $r['RangeType']),
|
||||
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
|
||||
'LowSignVValue' => ValueSet::getLabel('math_sign', $r['LowSign']),
|
||||
'HighSignVValue' => ValueSet::getLabel('math_sign', $r['HighSign']),
|
||||
'High' => $r['High'] !== null ? (int) $r['High'] : null,
|
||||
'Flag' => $r['Flag']
|
||||
];
|
||||
}, $refnumData ?? []);
|
||||
|
||||
$row['numRefTypeOptions'] = $this->getValuesetOptions(46);
|
||||
$row['rangeTypeOptions'] = $this->getValuesetOptions(45);
|
||||
$row['numRefTypeOptions'] = ValueSet::getOptions('numeric_ref_type');
|
||||
$row['rangeTypeOptions'] = ValueSet::getOptions('range_type');
|
||||
}
|
||||
|
||||
// Load reftxt for TEXT type (RefType = 2)
|
||||
if ($refType === 2) {
|
||||
if ($refType === '2') {
|
||||
$reftxtData = $this->modelRefTxt
|
||||
->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
@ -213,10 +183,10 @@ class TestsController extends BaseController
|
||||
$row['reftxt'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefTxtID' => $r['RefTxtID'],
|
||||
'TxtRefType' => (int) $r['TxtRefType'],
|
||||
'TxtRefTypeVValue' => $this->getVValue(47, $r['TxtRefType']),
|
||||
'Sex' => (int) $r['Sex'],
|
||||
'SexVValue' => $this->getVValue(3, $r['Sex']),
|
||||
'TxtRefType' => $r['TxtRefType'],
|
||||
'TxtRefTypeVValue' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
|
||||
'Sex' => $r['Sex'],
|
||||
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
|
||||
'AgeStart' => (int) $r['AgeStart'],
|
||||
'AgeEnd' => (int) $r['AgeEnd'],
|
||||
'RefTxt' => $r['RefTxt'],
|
||||
@ -224,24 +194,18 @@ class TestsController extends BaseController
|
||||
];
|
||||
}, $reftxtData ?? []);
|
||||
|
||||
$row['txtRefTypeOptions'] = $this->getValuesetOptions(47);
|
||||
$row['txtRefTypeOptions'] = ValueSet::getOptions('text_ref_type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include valueset options for dropdowns
|
||||
$row['refTypeOptions'] = $this->getValuesetOptions(self::VALUESET_REF_TYPE);
|
||||
$row['sexOptions'] = $this->getValuesetOptions(self::VALUESET_SEX);
|
||||
$row['mathSignOptions'] = $this->getValuesetOptions(self::VALUESET_MATH_SIGN);
|
||||
$row['refTypeOptions'] = ValueSet::getOptions('reference_type');
|
||||
$row['sexOptions'] = ValueSet::getOptions('gender');
|
||||
$row['mathSignOptions'] = ValueSet::getOptions('math_sign');
|
||||
|
||||
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()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
@ -253,7 +217,6 @@ class TestsController extends BaseController
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Insert into Main Table (testdefsite)
|
||||
$testSiteData = [
|
||||
'SiteID' => $input['SiteID'],
|
||||
'TestSiteCode' => $input['TestSiteCode'],
|
||||
@ -275,7 +238,6 @@ class TestsController extends BaseController
|
||||
throw new \Exception("Failed to insert main test definition");
|
||||
}
|
||||
|
||||
// 2. Handle Details based on TestType
|
||||
$this->handleDetails($id, $input, 'insert');
|
||||
|
||||
$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)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) {
|
||||
$id = $input["TestSiteID"];
|
||||
}
|
||||
@ -312,7 +268,6 @@ class TestsController extends BaseController
|
||||
return $this->failValidationErrors('TestSiteID is required.');
|
||||
}
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
@ -321,7 +276,6 @@ class TestsController extends BaseController
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Update Main Table
|
||||
$testSiteData = [];
|
||||
$allowedUpdateFields = [
|
||||
'TestSiteCode',
|
||||
@ -348,7 +302,6 @@ class TestsController extends BaseController
|
||||
$this->model->update($id, $testSiteData);
|
||||
}
|
||||
|
||||
// 2. Handle Details
|
||||
$this->handleDetails($id, $input, 'update');
|
||||
|
||||
$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)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) {
|
||||
$id = $input["TestSiteID"];
|
||||
}
|
||||
@ -385,13 +332,11 @@ class TestsController extends BaseController
|
||||
return $this->failValidationErrors('TestSiteID is required.');
|
||||
}
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
// Check if already disabled
|
||||
if (!empty($existing['EndDate'])) {
|
||||
return $this->failValidationErrors('Test is already disabled');
|
||||
}
|
||||
@ -401,15 +346,11 @@ class TestsController extends BaseController
|
||||
try {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// 1. Soft delete main record
|
||||
$this->model->update($id, ['EndDate' => $now]);
|
||||
|
||||
// 2. Get TestType to handle related records
|
||||
$testType = $existing['TestType'];
|
||||
$vs = $this->modelValueSet->find($testType);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
$typeCode = $testType;
|
||||
|
||||
// 3. Soft delete related records based on TestType
|
||||
if ($typeCode === 'CALC') {
|
||||
$this->db->table('testdefcal')
|
||||
->where('TestSiteID', $id)
|
||||
@ -423,12 +364,10 @@ class TestsController extends BaseController
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
|
||||
// Soft delete refnum and reftxt records
|
||||
$this->modelRefNum->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')
|
||||
->where('TestSiteID', $id)
|
||||
->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)
|
||||
{
|
||||
$testTypeID = $input['TestType'] ?? null;
|
||||
|
||||
// If update and TestType not in payload, fetch from DB
|
||||
if (!$testTypeID && $action === 'update') {
|
||||
$existing = $this->model->find($testSiteID);
|
||||
$testTypeID = $existing['TestType'] ?? null;
|
||||
@ -493,11 +401,8 @@ class TestsController extends BaseController
|
||||
if (!$testTypeID)
|
||||
return;
|
||||
|
||||
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
||||
$vs = $this->modelValueSet->find($testTypeID);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
$typeCode = $testTypeID;
|
||||
|
||||
// Get details data from input
|
||||
$details = $input['details'] ?? $input;
|
||||
$details['TestSiteID'] = $testSiteID;
|
||||
$details['SiteID'] = $input['SiteID'] ?? 1;
|
||||
@ -512,8 +417,6 @@ class TestsController extends BaseController
|
||||
break;
|
||||
|
||||
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'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
@ -524,32 +427,25 @@ class TestsController extends BaseController
|
||||
default:
|
||||
$this->saveTechDetails($testSiteID, $details, $action, $typeCode);
|
||||
|
||||
// Save refnum/reftxt for TEST/PARAM types
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Save test mappings for TEST and CALC types as well
|
||||
if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save technical details for TEST and PARAM types
|
||||
*/
|
||||
private function saveTechDetails($testSiteID, $data, $action, $typeCode)
|
||||
{
|
||||
$techData = [
|
||||
@ -586,9 +482,6 @@ class TestsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save refnum ranges for NMRC type
|
||||
*/
|
||||
private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
@ -601,14 +494,14 @@ class TestsController extends BaseController
|
||||
$this->modelRefNum->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'NumRefType' => (int) $range['NumRefType'],
|
||||
'RangeType' => (int) $range['RangeType'],
|
||||
'Sex' => (int) $range['Sex'],
|
||||
'NumRefType' => $range['NumRefType'],
|
||||
'RangeType' => $range['RangeType'],
|
||||
'Sex' => $range['Sex'],
|
||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||
'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,
|
||||
'HighSign' => !empty($range['HighSign']) ? (int) $range['HighSign'] : null,
|
||||
'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null,
|
||||
'High' => !empty($range['High']) ? (int) $range['High'] : null,
|
||||
'Flag' => $range['Flag'] ?? null,
|
||||
'Display' => $index,
|
||||
@ -617,9 +510,6 @@ class TestsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save reftxt ranges for TEXT type
|
||||
*/
|
||||
private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
@ -632,8 +522,8 @@ class TestsController extends BaseController
|
||||
$this->modelRefTxt->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'TxtRefType' => (int) $range['TxtRefType'],
|
||||
'Sex' => (int) $range['Sex'],
|
||||
'TxtRefType' => $range['TxtRefType'],
|
||||
'Sex' => $range['Sex'],
|
||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
||||
'RefTxt' => $range['RefTxt'] ?? '',
|
||||
@ -643,9 +533,6 @@ class TestsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save calculation details for CALC type
|
||||
*/
|
||||
private function saveCalcDetails($testSiteID, $data, $action)
|
||||
{
|
||||
$calcData = [
|
||||
@ -678,19 +565,14 @@ class TestsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save group details for GROUP type
|
||||
*/
|
||||
private function saveGroupDetails($testSiteID, $data, $input, $action)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing members
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
// Get members from details or input
|
||||
$members = $data['members'] ?? ($input['Members'] ?? []);
|
||||
|
||||
if (is_array($members)) {
|
||||
@ -706,13 +588,9 @@ class TestsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save test mappings
|
||||
*/
|
||||
private function saveTestMap($testSiteID, $mappings, $action)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
|
||||
@ -3,87 +3,80 @@ namespace App\Controllers\ValueSet;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ValueSet\ValueSetModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class ValueSetController extends BaseController {
|
||||
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() {
|
||||
$param = $this->request->getVar('param');
|
||||
$VSetID = $this->request->getVar('VSetID');
|
||||
|
||||
$data = $this->model->getValueSets($param, $VSetID);
|
||||
if ($param) {
|
||||
$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([
|
||||
'status' => 'success',
|
||||
'message'=> "Data fetched successfully",
|
||||
'data' => $data
|
||||
], 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() {
|
||||
$input = $this->request->getJSON(true);
|
||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
||||
try {
|
||||
$VID = $this->model->insert($input);
|
||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data $VID created successfully" ]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'message' => 'CRUD operations on value sets are disabled. Edit JSON files directly.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
public function update() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$VID = $input["VID"];
|
||||
if (!$VID) { return $this->failValidationErrors('VID is required.'); }
|
||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors( $this->validator->getErrors() ); }
|
||||
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());
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'message' => 'CRUD operations on value sets are disabled. Edit JSON files directly.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$input = $this->request->getJSON(true);
|
||||
$VID = $input["VID"];
|
||||
if (!$VID) { return $this->failValidationErrors('VID is required.'); }
|
||||
try {
|
||||
$this->model->delete($VID);
|
||||
return $this->respondDeleted(['status' => 'success', 'message' => "Data $VID deleted successfully."]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'message' => 'CRUD operations on value sets are disabled. Edit JSON files directly.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
public function refresh() {
|
||||
ValueSet::clearCache();
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Cache cleared'
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
||||
38
app/Controllers/ValueSetApiController.php
Normal file
38
app/Controllers/ValueSetApiController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ class PatientSeeder extends Seeder
|
||||
'NameLast' => 'Patient',
|
||||
'Suffix' => 'S.Kom',
|
||||
'NameAlias' => 'DummyTest',
|
||||
'Gender' => 5,
|
||||
'Sex' => 5,
|
||||
'PlaceOfBirth' => 'Jakarta',
|
||||
'Birthdate' => '1990-05-15',
|
||||
'Street_1' => 'Jl. Sudirman No. 123',
|
||||
@ -126,7 +126,7 @@ class PatientSeeder extends Seeder
|
||||
'NameLast' => 'Doe',
|
||||
'Suffix' => null,
|
||||
'NameAlias' => 'JaneDoe',
|
||||
'Gender' => 6, // Female
|
||||
'Sex' => 6, // Female
|
||||
'PlaceOfBirth' => 'Bandung',
|
||||
'Birthdate' => '1985-10-20',
|
||||
'Street_1' => 'Jl. Asia Afrika No. 456',
|
||||
@ -164,7 +164,7 @@ class PatientSeeder extends Seeder
|
||||
'NameLast' => 'Wijaya',
|
||||
'Suffix' => null,
|
||||
'NameAlias' => 'BudiW',
|
||||
'Gender' => 5,
|
||||
'Sex' => 5,
|
||||
'PlaceOfBirth' => 'Surabaya',
|
||||
'Birthdate' => '2000-01-01',
|
||||
'Street_1' => 'Jl. Pahlawan No. 789',
|
||||
|
||||
54
app/Libraries/Data/valuesets/_meta.json
Normal file
54
app/Libraries/Data/valuesets/_meta.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/activity_result.json
Normal file
11
app/Libraries/Data/valuesets/activity_result.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
27
app/Libraries/Data/valuesets/additive.json
Normal file
27
app/Libraries/Data/valuesets/additive.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
21
app/Libraries/Data/valuesets/adt_event.json
Normal file
21
app/Libraries/Data/valuesets/adt_event.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/area_class.json
Normal file
11
app/Libraries/Data/valuesets/area_class.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/body_site.json
Normal file
12
app/Libraries/Data/valuesets/body_site.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
18
app/Libraries/Data/valuesets/collection_method.json
Normal file
18
app/Libraries/Data/valuesets/collection_method.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
16
app/Libraries/Data/valuesets/container_cap_color.json
Normal file
16
app/Libraries/Data/valuesets/container_cap_color.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/container_class.json
Normal file
11
app/Libraries/Data/valuesets/container_class.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/container_size.json
Normal file
12
app/Libraries/Data/valuesets/container_size.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
26
app/Libraries/Data/valuesets/country.json
Normal file
26
app/Libraries/Data/valuesets/country.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/death_indicator.json
Normal file
10
app/Libraries/Data/valuesets/death_indicator.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"VSetID": 5,
|
||||
"name": "death_indicator",
|
||||
"VSName": "Death Indicator",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "Y", "value": "Death"},
|
||||
{"key": "N", "value": "Life"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/did_type.json
Normal file
11
app/Libraries/Data/valuesets/did_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/enable_disable.json
Normal file
10
app/Libraries/Data/valuesets/enable_disable.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"VSetID": 2,
|
||||
"name": "enable_disable",
|
||||
"VSName": "Enable/Disable",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "0", "value": "Disabled"},
|
||||
{"key": "1", "value": "Enabled"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/entity_type.json
Normal file
12
app/Libraries/Data/valuesets/entity_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
16
app/Libraries/Data/valuesets/ethnic.json
Normal file
16
app/Libraries/Data/valuesets/ethnic.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/fasting_status.json
Normal file
11
app/Libraries/Data/valuesets/fasting_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/formula_language.json
Normal file
12
app/Libraries/Data/valuesets/formula_language.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/gender.json
Normal file
11
app/Libraries/Data/valuesets/gender.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/generate_by.json
Normal file
10
app/Libraries/Data/valuesets/generate_by.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
13
app/Libraries/Data/valuesets/identifier_type.json
Normal file
13
app/Libraries/Data/valuesets/identifier_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
16
app/Libraries/Data/valuesets/location_type.json
Normal file
16
app/Libraries/Data/valuesets/location_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
16
app/Libraries/Data/valuesets/marital_status.json
Normal file
16
app/Libraries/Data/valuesets/marital_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
13
app/Libraries/Data/valuesets/math_sign.json
Normal file
13
app/Libraries/Data/valuesets/math_sign.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/numeric_ref_type.json
Normal file
10
app/Libraries/Data/valuesets/numeric_ref_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/operation.json
Normal file
12
app/Libraries/Data/valuesets/operation.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
15
app/Libraries/Data/valuesets/order_priority.json
Normal file
15
app/Libraries/Data/valuesets/order_priority.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
20
app/Libraries/Data/valuesets/order_status.json
Normal file
20
app/Libraries/Data/valuesets/order_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
15
app/Libraries/Data/valuesets/priority.json
Normal file
15
app/Libraries/Data/valuesets/priority.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
39
app/Libraries/Data/valuesets/race.json
Normal file
39
app/Libraries/Data/valuesets/race.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/range_type.json
Normal file
12
app/Libraries/Data/valuesets/range_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/reference_type.json
Normal file
10
app/Libraries/Data/valuesets/reference_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
15
app/Libraries/Data/valuesets/religion.json
Normal file
15
app/Libraries/Data/valuesets/religion.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
28
app/Libraries/Data/valuesets/request_status.json
Normal file
28
app/Libraries/Data/valuesets/request_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/requested_entity.json
Normal file
12
app/Libraries/Data/valuesets/requested_entity.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/result_status.json
Normal file
12
app/Libraries/Data/valuesets/result_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/result_type.json
Normal file
12
app/Libraries/Data/valuesets/result_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
18
app/Libraries/Data/valuesets/result_unit.json
Normal file
18
app/Libraries/Data/valuesets/result_unit.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
14
app/Libraries/Data/valuesets/site_class.json
Normal file
14
app/Libraries/Data/valuesets/site_class.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
14
app/Libraries/Data/valuesets/site_type.json
Normal file
14
app/Libraries/Data/valuesets/site_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
15
app/Libraries/Data/valuesets/specimen_activity.json
Normal file
15
app/Libraries/Data/valuesets/specimen_activity.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
19
app/Libraries/Data/valuesets/specimen_condition.json
Normal file
19
app/Libraries/Data/valuesets/specimen_condition.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
17
app/Libraries/Data/valuesets/specimen_role.json
Normal file
17
app/Libraries/Data/valuesets/specimen_role.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
28
app/Libraries/Data/valuesets/specimen_status.json
Normal file
28
app/Libraries/Data/valuesets/specimen_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
23
app/Libraries/Data/valuesets/specimen_type.json
Normal file
23
app/Libraries/Data/valuesets/specimen_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
13
app/Libraries/Data/valuesets/test_activity.json
Normal file
13
app/Libraries/Data/valuesets/test_activity.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
12
app/Libraries/Data/valuesets/test_status.json
Normal file
12
app/Libraries/Data/valuesets/test_status.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
13
app/Libraries/Data/valuesets/test_type.json
Normal file
13
app/Libraries/Data/valuesets/test_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/text_ref_type.json
Normal file
10
app/Libraries/Data/valuesets/text_ref_type.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
11
app/Libraries/Data/valuesets/unit.json
Normal file
11
app/Libraries/Data/valuesets/unit.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/v_category.json
Normal file
10
app/Libraries/Data/valuesets/v_category.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"VSetID": 42,
|
||||
"name": "v_category",
|
||||
"VSName": "VCategory",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "0", "value": "System"},
|
||||
{"key": "1", "value": "User-defined"}
|
||||
]
|
||||
}
|
||||
10
app/Libraries/Data/valuesets/ws_type.json
Normal file
10
app/Libraries/Data/valuesets/ws_type.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"VSetID": 1,
|
||||
"name": "ws_type",
|
||||
"VSName": "Workstation Type",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "0", "value": "Primary"},
|
||||
{"key": "1", "value": "Secondary"}
|
||||
]
|
||||
}
|
||||
@ -1,649 +1,5 @@
|
||||
<?php
|
||||
namespace App\Libraries;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
class Lookups extends ValueSet {
|
||||
}
|
||||
|
||||
108
app/Libraries/ValueSet.php
Normal file
108
app/Libraries/ValueSet.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace App\Models\Location;
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class LocationModel extends BaseModel {
|
||||
protected $table = 'location';
|
||||
@ -14,24 +15,31 @@ class LocationModel extends BaseModel {
|
||||
protected $deletedField = 'EndDate';
|
||||
|
||||
public function getLocations($LocCode, $LocName) {
|
||||
$sql = $this->select("LocationID, LocCode, Parent, LocFull, LocType, v.VDesc as LocTypeText")
|
||||
->join("valueset v", "v.VID=location.loctype", 'left');
|
||||
$sql = $this->select("LocationID, LocCode, Parent, LocFull, LocType");
|
||||
if($LocName != '') { $sql->like('LocFull', $LocName, 'both'); }
|
||||
if($LocCode != '') { $sql->like('LocCode', $LocCode, 'both'); }
|
||||
$rows = $sql->findAll();
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'LocType' => 'location_type',
|
||||
]);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
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,
|
||||
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("valueset v", "v.VID=location.loctype", "left")
|
||||
->join("areageo prop", "la.Province=prop.AreaGeoID", "left")
|
||||
->join("areageo city", "la.City=city.AreaGeoID", "left")
|
||||
->join("site", "site.SiteID=location.SiteID", "left")
|
||||
->where('location.LocationID', (int) $LocationID)->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'LocType' => 'location_type',
|
||||
])[0];
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
@ -83,4 +91,3 @@ class LocationModel extends BaseModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
app/Models/OrderTest/OrderTestModel.php
Normal file
102
app/Models/OrderTest/OrderTestModel.php
Normal 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')]);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace App\Models\Organization;
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class AccountModel extends BaseModel {
|
||||
protected $table = 'account';
|
||||
@ -32,15 +33,20 @@ class AccountModel extends BaseModel {
|
||||
|
||||
public function getAccount($AccountID) {
|
||||
$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,
|
||||
country.VValue as CountryName, country.VID as country')
|
||||
city.AreaName as CityName, city.AreaGeoID as City, prov.AreaName as ProvName, prov.AreaGeoID as Prov')
|
||||
->join('account pa', 'pa.AccountID=account.Parent', 'left')
|
||||
->join('areageo', 'areageo.AreaCode=account.AreaCode', 'left')
|
||||
->join('areageo city', 'city.AreaGeoID=account.City', 'left')
|
||||
->join('areageo prov', 'prov.AreaGeoID=account.Province', 'left')
|
||||
->join('valueset country', 'country.VID=account.Country', 'left')
|
||||
->where('account.AccountID', $AccountID)
|
||||
->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'Country' => 'account_Country',
|
||||
])[0];
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace App\Models\Organization;
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class SiteModel extends BaseModel {
|
||||
protected $table = 'site';
|
||||
@ -32,13 +33,19 @@ class SiteModel extends BaseModel {
|
||||
}
|
||||
|
||||
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('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)
|
||||
->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'SiteTypeID' => 'site_type',
|
||||
'SiteClassID' => 'site_class',
|
||||
])[0];
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace App\Models\Organization;
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class WorkstationModel extends BaseModel {
|
||||
protected $table = 'workstation';
|
||||
@ -30,13 +31,19 @@ class WorkstationModel extends BaseModel {
|
||||
}
|
||||
|
||||
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('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)
|
||||
->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'Type' => 'ws_type',
|
||||
'Enable' => 'enable_disable',
|
||||
])[0];
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
namespace App\Models\Patient;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
use App\Models\Patient\PatAttModel;
|
||||
use App\Models\Patient\PatComModel;
|
||||
@ -10,7 +11,7 @@ use App\Models\Patient\PatIdtModel;
|
||||
class PatientModel extends BaseModel {
|
||||
protected $table = 'patient';
|
||||
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',
|
||||
'DeathIndicator', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ];
|
||||
|
||||
@ -23,8 +24,7 @@ class PatientModel extends BaseModel {
|
||||
public function getPatients($filters = []) {
|
||||
$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->join('valueset vs', 'vs.vid = Gender', 'left');
|
||||
$this->select("InternalPID, PatientID, $qname as FullName, Sex, Birthdate, EmailAddress1 as Email, MobilePhone");
|
||||
|
||||
if (!empty($filters['Name'])) {
|
||||
$this->like($qname, $filters['Name'], 'both');
|
||||
@ -42,26 +42,16 @@ class PatientModel extends BaseModel {
|
||||
$this->where('Birthdate', $filters['Birthdate']);
|
||||
}
|
||||
|
||||
return $this->findAll();
|
||||
$rows = $this->findAll();
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'Sex' => 'gender',
|
||||
]);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function getPatient($InternalPID) {
|
||||
$rows = $this->select("
|
||||
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,
|
||||
patidt.IdentifierType,
|
||||
patidt.Identifier,
|
||||
@ -72,13 +62,6 @@ class PatientModel extends BaseModel {
|
||||
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('patidt', 'patidt.InternalPID = patient.InternalPID', '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['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['PatAtt'] = [];
|
||||
|
||||
@ -133,24 +125,20 @@ class PatientModel extends BaseModel {
|
||||
|
||||
try {
|
||||
|
||||
// Insert Data ke Tabel Patient, get ID dan cek apa ada error
|
||||
$this->insert($input);
|
||||
$newInternalPID = $this->getInsertID();
|
||||
$this->checkDbError($db, 'Insert patient');
|
||||
|
||||
// Insert Data ke Tabel PatIdt
|
||||
if (!empty($input['PatIdt'])) {
|
||||
$modelPatIdt->createPatIdt($input['PatIdt'], $newInternalPID);
|
||||
$this->checkDbError($db, 'Insert PatIdt');
|
||||
}
|
||||
|
||||
// Insert Data ke Tabel PatCom
|
||||
if (!empty($input['PatCom'])) {
|
||||
$modelPatCom->createPatCom($input['PatCom'], $newInternalPID);
|
||||
$this->checkDbError($db, 'Insert PatCom');
|
||||
}
|
||||
|
||||
// Insert Data ke Tabel PatAtt
|
||||
if (!empty($input['PatAtt'])) {
|
||||
$modelPatAtt->createPatAtt($input['PatAtt'], $newInternalPID);
|
||||
$this->checkDbError($db, 'Insert PatAtt');
|
||||
@ -178,12 +166,10 @@ class PatientModel extends BaseModel {
|
||||
|
||||
try {
|
||||
|
||||
// Update Patient
|
||||
$InternalPID = $input['InternalPID'];
|
||||
$this->where('InternalPID',$InternalPID)->set($input)->update();
|
||||
$this->checkDbError($db, 'Update patient');
|
||||
|
||||
// Update Patidt
|
||||
if (!empty($input['PatIdt'])) {
|
||||
$modelPatIdt->updatePatIdt($input['PatIdt'], $InternalPID);
|
||||
$this->checkDbError($db, 'Update patIdt');
|
||||
@ -192,7 +178,6 @@ class PatientModel extends BaseModel {
|
||||
$this->checkDbError($db, 'Update patidt');
|
||||
}
|
||||
|
||||
// Update Patcom
|
||||
if (!empty($input['PatCom'])) {
|
||||
$modelPatCom->updatePatCom($input['PatCom'], $InternalPID);
|
||||
$this->checkDbError($db, 'Update PatCom');
|
||||
@ -201,7 +186,6 @@ class PatientModel extends BaseModel {
|
||||
$this->checkDbError($db, 'Update patcom');
|
||||
}
|
||||
|
||||
// Update Patatt
|
||||
if (!empty($input['PatAtt'])) {
|
||||
$modelPatAtt->updatePatAtt($input['PatAtt'], $InternalPID);
|
||||
$this->checkDbError($db, 'Update PatAtt');
|
||||
@ -254,11 +238,9 @@ class PatientModel extends BaseModel {
|
||||
->first() ?: null;
|
||||
}
|
||||
|
||||
// Conversion to (Years Months Days) - For Age
|
||||
private function calculateAgeFromBirthdate($birthdate, $deathdate) {
|
||||
$dob = new \DateTime($birthdate);
|
||||
|
||||
// Cek DeathTime
|
||||
if ($deathdate == null) {
|
||||
$today = new \DateTime();
|
||||
} else {
|
||||
@ -281,19 +263,17 @@ class PatientModel extends BaseModel {
|
||||
return $formattedAge;
|
||||
}
|
||||
|
||||
// Conversion Time to Format Y-m-d\TH:i:s\Z
|
||||
private function formattedDate(?string $dateString): ?string {
|
||||
try {
|
||||
if (empty($dateString)) {return null;}
|
||||
|
||||
$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) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion Time to Format j M Y - For BirthdateConversion
|
||||
private function formatedDateForDisplay($dateString) {
|
||||
$date = \DateTime::createFromFormat('Y-m-d H:i', $dateString);
|
||||
|
||||
@ -308,7 +288,6 @@ class PatientModel extends BaseModel {
|
||||
return $date->format('j M Y');
|
||||
}
|
||||
|
||||
// Check Error and Send Spesific Messages
|
||||
private function checkDbError($db, string $context) {
|
||||
$error = $db->error();
|
||||
if (!empty($error['code'])) {
|
||||
@ -318,17 +297,14 @@ class PatientModel extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Preventif 0000-00-00
|
||||
private function isValidDateTime($datetime) {
|
||||
if (empty($datetime) || $datetime=="") {return null; }
|
||||
try {
|
||||
// Kalau input hanya Y-m-d (tanpa jam)
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $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);
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
namespace App\Models\Specimen;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class ContainerDefModel extends BaseModel {
|
||||
protected $table = 'containerdef';
|
||||
@ -16,10 +17,7 @@ class ContainerDefModel extends BaseModel {
|
||||
|
||||
|
||||
public function getContainers($filter = []) {
|
||||
$builder = $this->select('containerdef.*, vscol.VValue as ColorTxt, vscla.VValue as ConClassTxt, vsadd.VValue as AdditiveTxt')
|
||||
->join('valueset vscol', 'vscol.VID=containerdef.Color', 'left')
|
||||
->join('valueset vscla', 'vscla.VID=containerdef.ConClass', 'left')
|
||||
->join('valueset vsadd', 'vsadd.VID=containerdef.Additive', 'left');
|
||||
$builder = $this->select('containerdef.*');
|
||||
|
||||
if (!empty($filter['ConCode'])) {
|
||||
$builder->like('containerdef.ConCode', $filter['ConCode'], 'both');
|
||||
@ -29,15 +27,26 @@ class ContainerDefModel extends BaseModel {
|
||||
}
|
||||
|
||||
$rows = $builder->findAll();
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'Color' => 'container_cap_color',
|
||||
'ConClass' => 'container_class',
|
||||
'Additive' => 'additive',
|
||||
]);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function getContainer($ConDefID) {
|
||||
$row = $this->select('containerdef.*, vscol.VValue as ColorTxt, vscla.VValue as ConClassTxt, vsadd.VValue as AdditiveTxt')
|
||||
->join('valueset vscol', 'vscol.VID=containerdef.Color', 'left')
|
||||
->join('valueset vscla', 'vscla.VID=containerdef.ConClass', 'left')
|
||||
->join('valueset vsadd', 'vsadd.VID=containerdef.Additive', 'left')
|
||||
$row = $this->select('containerdef.*')
|
||||
->where('ConDefID', $ConDefID)->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'Color' => 'container_cap_color',
|
||||
'ConClass' => 'container_class',
|
||||
'Additive' => 'additive',
|
||||
])[0];
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models\Test;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class TestDefGrpModel extends BaseModel {
|
||||
protected $table = 'testdefgrp';
|
||||
@ -20,25 +21,24 @@ class TestDefGrpModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
/**
|
||||
* Get group members for a test group
|
||||
*/
|
||||
public function getGroupMembers($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
$rows = $db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $testSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'TestType' => 'test_type',
|
||||
]);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups that contain a specific test
|
||||
*/
|
||||
public function getGroupsContainingTest($memberTestSiteID) {
|
||||
return $this->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.TestSiteID', 'left')
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models\Test;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class TestDefSiteModel extends BaseModel {
|
||||
protected $table = 'testdefsite';
|
||||
@ -31,15 +32,10 @@ class TestDefSiteModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
/**
|
||||
* Get all tests with type information
|
||||
*/
|
||||
public function getTests($siteId = null, $testType = null, $visibleScr = null, $visibleRpt = null, $keyword = null) {
|
||||
$builder = $this->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate")
|
||||
->where('testdefsite.EndDate IS NULL');
|
||||
|
||||
if ($siteId) {
|
||||
@ -62,27 +58,29 @@ class TestDefSiteModel extends BaseModel {
|
||||
$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) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$row = $this->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
$row = $this->select("testdefsite.*")
|
||||
->where("testdefsite.TestSiteID", $TestSiteID)
|
||||
->find($TestSiteID);
|
||||
|
||||
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') {
|
||||
// Load calculation details with joined discipline and department
|
||||
$row['testdefcal'] = $db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
@ -91,32 +89,30 @@ class TestDefSiteModel extends BaseModel {
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
// Load group members with test details
|
||||
$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('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $TestSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testdefgrp'] = ValueSet::transformLabels($row['testdefgrp'], [
|
||||
'TestType' => 'test_type',
|
||||
]);
|
||||
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'TITLE') {
|
||||
// Load test mappings only for TITLE type
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
// TEST or PARAM - load technical details with joined tables
|
||||
$row['testdeftech'] = $db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
@ -125,7 +121,6 @@ class TestDefSiteModel extends BaseModel {
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||
}
|
||||
|
||||
@ -164,12 +164,12 @@
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Country</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Country"
|
||||
placeholder="Indonesia"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@ -151,6 +151,7 @@ function accounts() {
|
||||
loading: false,
|
||||
list: [],
|
||||
keyword: "",
|
||||
countryOptions: [],
|
||||
|
||||
// Form Modal
|
||||
showModal: false,
|
||||
@ -179,6 +180,21 @@ function accounts() {
|
||||
// Lifecycle
|
||||
async init() {
|
||||
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
|
||||
|
||||
@ -92,6 +92,31 @@
|
||||
</template>
|
||||
</select>
|
||||
</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>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ME (Medical Examiner?)</span>
|
||||
|
||||
@ -57,7 +57,8 @@
|
||||
<th>Site Name</th>
|
||||
<th>Code</th>
|
||||
<th>Account</th>
|
||||
<th>Parent Site</th>
|
||||
<th>Type</th>
|
||||
<th>Class</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -88,7 +89,8 @@
|
||||
</td>
|
||||
<td x-text="site.SiteCode || '-'"></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">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editSite(site.SiteID)" title="Edit">
|
||||
@ -172,10 +174,44 @@ function sites() {
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lookup Options
|
||||
siteTypeOptions: [],
|
||||
siteClassOptions: [],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
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
|
||||
|
||||
@ -87,8 +87,9 @@
|
||||
</label>
|
||||
<select class="select" x-model="form.Type">
|
||||
<option value="">Select Type</option>
|
||||
<option value="1">Manual</option>
|
||||
<option value="2">Automated</option>
|
||||
<template x-for="opt in typeOptions" :key="opt.key">
|
||||
<option :value="opt.key" x-text="opt.value"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@ -96,8 +97,9 @@
|
||||
<span class="label-text font-medium">Status</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Enable">
|
||||
<option value="1">Enabled</option>
|
||||
<option value="0">Disabled</option>
|
||||
<template x-for="opt in enableOptions" :key="opt.key">
|
||||
<option :value="opt.key" x-text="opt.value"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
<td class="font-mono text-sm" x-text="ws.WorkstationCode || '-'"></td>
|
||||
<td x-text="ws.DepartmentName || '-'"></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 class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
@ -161,7 +161,7 @@ function workstations() {
|
||||
WorkstationName: "",
|
||||
DepartmentID: "",
|
||||
Type: "",
|
||||
Enable: 1,
|
||||
Enable: "1",
|
||||
LinkTo: ""
|
||||
},
|
||||
|
||||
@ -170,10 +170,48 @@ function workstations() {
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lookup Options
|
||||
typeOptions: [],
|
||||
enableOptions: [],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
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
|
||||
@ -220,7 +258,7 @@ function workstations() {
|
||||
WorkstationName: "",
|
||||
DepartmentID: "",
|
||||
Type: "",
|
||||
Enable: 1,
|
||||
Enable: "1",
|
||||
LinkTo: ""
|
||||
};
|
||||
this.errors = {};
|
||||
|
||||
@ -71,12 +71,12 @@
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Cap Color</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Color"
|
||||
placeholder="e.g. Gold, Red, Lavender"
|
||||
/>
|
||||
<select class="select" x-model="form.Color">
|
||||
<option value="">Select Color</option>
|
||||
<template x-for="opt in colorOptions" :key="opt.key">
|
||||
<option :value="opt.key" x-text="opt.value"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -96,23 +96,23 @@
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Additive</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Additive"
|
||||
placeholder="SST / EDTA / Heparin"
|
||||
/>
|
||||
<select class="select" x-model="form.Additive">
|
||||
<option value="">Select Additive</option>
|
||||
<template x-for="opt in additiveOptions" :key="opt.key">
|
||||
<option :value="opt.key" x-text="opt.value"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Class</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ConClass"
|
||||
placeholder="Tube / Swab"
|
||||
/>
|
||||
<select class="select" x-model="form.ConClass">
|
||||
<option value="">Select Class</option>
|
||||
<template x-for="opt in classOptions" :key="opt.key">
|
||||
<option :value="opt.key" x-text="opt.value"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -90,10 +90,10 @@
|
||||
<td>
|
||||
<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>
|
||||
<span x-text="con.ColorTxt || con.Color || '-'"></span>
|
||||
<span x-text="con.ColorText || con.Color || '-'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td x-text="con.AdditiveTxt || con.Additive || '-'"></td>
|
||||
<td x-text="con.AdditiveText || con.Additive || '-'"></td>
|
||||
<td class="text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="editContainer(con.ConDefID)" title="Edit">
|
||||
@ -174,10 +174,60 @@ function containers() {
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lookup Options
|
||||
colorOptions: [],
|
||||
additiveOptions: [],
|
||||
classOptions: [],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
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
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
@keyup.enter="fetchList()" />
|
||||
<select class="select w-40" x-model="filterType" @change="fetchList()">
|
||||
<option value="">All Types</option>
|
||||
<template x-for="(type, index) in (testTypes || [])" :key="(type?.VID ?? index)">
|
||||
<option :value="type?.VID" x-text="(type?.VValue || '') + ' - ' + (type?.VDesc || '')"></option>
|
||||
<template x-for="(type, index) in (testTypes || [])" :key="(type?.key ?? index)">
|
||||
<option :value="type?.key" x-text="(type?.value || '')"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button class="btn btn-primary" @click="fetchList()">
|
||||
@ -440,7 +440,7 @@
|
||||
// Fetch test types from valueset
|
||||
async fetchTestTypes() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valuesetdef/27`, {
|
||||
const res = await fetch(`${BASEURL}api/valueset/test_type`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
@ -448,13 +448,12 @@
|
||||
this.testTypes = data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch test types:', err);
|
||||
// Fallback to hardcoded types
|
||||
this.testTypes = [
|
||||
{ VID: 1, VValue: 'TEST', VDesc: 'Test' },
|
||||
{ VID: 2, VValue: 'PARAM', VDesc: 'Parameter' },
|
||||
{ VID: 3, VValue: 'CALC', VDesc: 'Calculated' },
|
||||
{ VID: 4, VValue: 'GROUP', VDesc: 'Group' },
|
||||
{ VID: 5, VValue: 'TITLE', VDesc: 'Title' }
|
||||
{ key: 'TEST', value: 'Test' },
|
||||
{ key: 'PARAM', value: 'Parameter' },
|
||||
{ key: 'CALC', value: 'Calculated Test' },
|
||||
{ key: 'GROUP', value: 'Group Test' },
|
||||
{ key: 'TITLE', value: 'Title' }
|
||||
];
|
||||
}
|
||||
},
|
||||
@ -477,8 +476,7 @@
|
||||
},
|
||||
|
||||
// Get type display name
|
||||
getTypeName(vid) {
|
||||
const code = this.getTypeCode(vid);
|
||||
getTypeName(value) {
|
||||
const typeMap = {
|
||||
'TEST': 'Test',
|
||||
'PARAM': 'Parameter',
|
||||
@ -486,7 +484,12 @@
|
||||
'GROUP': 'Group',
|
||||
'TITLE': 'Title'
|
||||
};
|
||||
return typeMap[code] || 'Test';
|
||||
return typeMap[value] || value || 'Test';
|
||||
},
|
||||
|
||||
// Get type code from value
|
||||
getTypeCode(value) {
|
||||
return value || '';
|
||||
},
|
||||
|
||||
// Fetch test list
|
||||
@ -584,23 +587,11 @@
|
||||
const data = await res.json();
|
||||
if (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,
|
||||
...testData,
|
||||
TestType: testTypeVid,
|
||||
// Store TypeCode directly for dialog display
|
||||
TypeCode: typeCode,
|
||||
// Preserve group members if editing group
|
||||
TestType: testData.TestType || '',
|
||||
TypeCode: testData.TestType || '',
|
||||
groupMembers: testData.testdefgrp || []
|
||||
};
|
||||
this.showModal = true;
|
||||
@ -699,7 +690,6 @@
|
||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
||||
const payload = { ...this.form };
|
||||
|
||||
// Handle group members for GROUP type
|
||||
if (this.getTypeCode(this.form.TestType) === 'GROUP' && this.form.groupMembers?.length > 0) {
|
||||
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>
|
||||
|
||||
@ -95,11 +95,12 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Gender</span>
|
||||
<span class="label-text font-medium">Sex</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Gender">
|
||||
<option value="1">Male</option>
|
||||
<option value="2">Female</option>
|
||||
<select class="select" x-model="form.Sex">
|
||||
<template x-for="opt in sexOptions" :key="opt.key">
|
||||
<option :value="opt.key" x-text="opt.value"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@
|
||||
<tr>
|
||||
<th>Patient ID</th>
|
||||
<th>Name</th>
|
||||
<th>Gender</th>
|
||||
<th>Sex</th>
|
||||
<th>Birth Date</th>
|
||||
<th>Phone</th>
|
||||
<th class="text-center">Actions</th>
|
||||
@ -134,8 +134,8 @@
|
||||
<td>
|
||||
<span
|
||||
class="badge badge-sm"
|
||||
:class="patient.Gender == 1 ? 'badge-info' : 'badge-secondary'"
|
||||
x-text="patient.Gender == 1 ? 'Male' : patient.Gender == 2 ? 'Female' : '-'"
|
||||
:class="patient.Sex === 'M' ? 'badge-info' : patient.Sex === 'F' ? 'badge-secondary' : 'badge-ghost'"
|
||||
x-text="patient.SexText || patient.Sex || '-'"
|
||||
></span>
|
||||
</td>
|
||||
<td x-text="formatDate(patient.Birthdate)"></td>
|
||||
@ -227,7 +227,7 @@ function patients() {
|
||||
NameFirst: "",
|
||||
NameMiddle: "",
|
||||
NameLast: "",
|
||||
Gender: 1,
|
||||
Sex: "M",
|
||||
Birthdate: "",
|
||||
MobilePhone: "",
|
||||
EmailAddress1: "",
|
||||
@ -242,9 +242,31 @@ function patients() {
|
||||
deleteTarget: null,
|
||||
deleting: false,
|
||||
|
||||
// Lookup Options
|
||||
sexOptions: [],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
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
|
||||
@ -299,7 +321,7 @@ function patients() {
|
||||
NameFirst: "",
|
||||
NameMiddle: "",
|
||||
NameLast: "",
|
||||
Gender: 1,
|
||||
Sex: "M",
|
||||
Birthdate: "",
|
||||
MobilePhone: "",
|
||||
EmailAddress1: "",
|
||||
|
||||
645
docs/MIGRATION_VALUESET_VID_TO_VVALUE.md
Normal file
645
docs/MIGRATION_VALUESET_VID_TO_VVALUE.md
Normal 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**
|
||||
23078
docs/prj_clinical laboratory quality management system_3a.md
Normal file
23078
docs/prj_clinical laboratory quality management system_3a.md
Normal file
File diff suppressed because it is too large
Load Diff
100
seeds/MinimalMasterDataSeeder.php
Normal file
100
seeds/MinimalMasterDataSeeder.php
Normal 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";
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ class PatientCreateTest extends CIUnitTestCase
|
||||
"NameLast"=> "Wijaya",
|
||||
"Suffix"=> "S.kom",
|
||||
"NameAlias"=> "Bud",
|
||||
"Gender"=> "1",
|
||||
"Sex"=> "1",
|
||||
];
|
||||
$result = $this->withBodyFormat('json')->call('post', 'api/patient', $payload);
|
||||
$result->assertStatus(400);
|
||||
@ -57,7 +57,7 @@ class PatientCreateTest extends CIUnitTestCase
|
||||
"NameLast" => $faker->lastName,
|
||||
"Suffix" => "S.Kom",
|
||||
"NameAlias" => $faker->userName,
|
||||
"Gender" => $faker->numberBetween(5, 6),
|
||||
"Sex" => $faker->numberBetween(5, 6),
|
||||
"PlaceOfBirth" => $faker->city,
|
||||
"Birthdate" => $faker->date('Y-m-d'),
|
||||
"ZIP" => $faker->postcode,
|
||||
@ -118,7 +118,7 @@ class PatientCreateTest extends CIUnitTestCase
|
||||
"NameLast" => $faker->lastName,
|
||||
"Suffix" => "S.Kom",
|
||||
"NameAlias" => $faker->userName,
|
||||
"Gender" => $faker->numberBetween(5, 6),
|
||||
"Sex" => $faker->numberBetween(5, 6),
|
||||
"PlaceOfBirth" => $faker->city,
|
||||
"Birthdate" => $faker->date('Y-m-d'),
|
||||
"ZIP" => $faker->postcode,
|
||||
@ -177,7 +177,7 @@ class PatientCreateTest extends CIUnitTestCase
|
||||
"NameLast" => $faker->lastName,
|
||||
"Suffix" => "S.Kom",
|
||||
"NameAlias" => $faker->userName,
|
||||
"Gender" => $faker->numberBetween(5, 6),
|
||||
"Sex" => $faker->numberBetween(5, 6),
|
||||
"PlaceOfBirth" => $faker->city,
|
||||
"Birthdate" => $faker->date('Y-m-d'),
|
||||
"ZIP" => $faker->postcode,
|
||||
@ -233,7 +233,7 @@ class PatientCreateTest extends CIUnitTestCase
|
||||
"NameLast" => $faker->lastName,
|
||||
"Suffix" => "S.Kom",
|
||||
"NameAlias" => $faker->userName,
|
||||
"Gender" => $faker->numberBetween(5, 6),
|
||||
"Sex" => $faker->numberBetween(5, 6),
|
||||
"PlaceOfBirth" => $faker->city,
|
||||
"Birthdate" => $faker->date('Y-m-d'),
|
||||
"ZIP" => $faker->postcode,
|
||||
@ -293,7 +293,7 @@ class PatientCreateTest extends CIUnitTestCase
|
||||
"NameLast" => $faker->lastName,
|
||||
"Suffix" => "S.Kom",
|
||||
"NameAlias" => $faker->userName,
|
||||
"Gender" => $faker->numberBetween(5, 6),
|
||||
"Sex" => $faker->numberBetween(5, 6),
|
||||
"PlaceOfBirth" => $faker->city,
|
||||
"Birthdate" => $faker->date('Y-m-d'),
|
||||
"ZIP" => $faker->postcode,
|
||||
|
||||
@ -223,7 +223,7 @@ class PatientDeleteTest extends CIUnitTestCase
|
||||
// "Prefix" => "Mr.",
|
||||
// "NameFirst" => "ToBeDeleted",
|
||||
// "NameLast" => "Patient",
|
||||
// "Gender" => "5",
|
||||
// "Sex" => "5",
|
||||
// "Birthdate" => "1990-01-01",
|
||||
// ];
|
||||
//
|
||||
|
||||
@ -41,7 +41,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
||||
"MobilePhone" => $faker->numerify('08##########'),
|
||||
'NameFirst' => $faker->firstName,
|
||||
'NameLast' => $faker->lastName,
|
||||
'Gender' => '1',
|
||||
'Sex' => '1',
|
||||
'Birthdate' => $faker->date('Y-m-d'),
|
||||
'PatIdt' => [ 'IdentifierType' => 'KTP', 'Identifier' => $faker->nik() ],
|
||||
'PatCom' => $faker->sentence,
|
||||
@ -71,7 +71,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
||||
'NameFirst' => $faker->firstName,
|
||||
'NameMiddle' => $faker->firstName,
|
||||
'NameLast' => $faker->lastName,
|
||||
'Gender' => '1',
|
||||
'Sex' => '1',
|
||||
'Birthdate' => $faker->date('Y-m-d'),
|
||||
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
|
||||
"Phone" => $faker->numerify('08##########'),
|
||||
@ -115,7 +115,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
||||
'NameFirst' => $faker->firstName,
|
||||
'NameMiddle' => $faker->firstName,
|
||||
'NameLast' => $faker->lastName,
|
||||
'Gender' => '1',
|
||||
'Sex' => '1',
|
||||
'Birthdate' => $faker->date('Y-m-d'),
|
||||
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
|
||||
"Phone" => $faker->numerify('08##########'),
|
||||
@ -155,7 +155,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
||||
'NameFirst' => $faker->firstName,
|
||||
'NameMiddle' => $faker->firstName,
|
||||
'NameLast' => $faker->lastName,
|
||||
'Gender' => '1',
|
||||
'Sex' => '1',
|
||||
'Birthdate' => $faker->date('Y-m-d'),
|
||||
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
|
||||
"Phone" => $faker->numerify('08##########'),
|
||||
@ -193,7 +193,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
||||
'NameFirst' => $faker->firstName,
|
||||
'NameMiddle' => $faker->firstName,
|
||||
'NameLast' => $faker->lastName,
|
||||
'Gender' => '1',
|
||||
'Sex' => '1',
|
||||
'Birthdate' => $faker->date('Y-m-d'),
|
||||
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
|
||||
"Phone" => $faker->numerify('08##########'),
|
||||
@ -233,7 +233,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
||||
'NameFirst' => $faker->firstName,
|
||||
'NameMiddle' => $faker->firstName,
|
||||
'NameLast' => $faker->lastName,
|
||||
'Gender' => '1',
|
||||
'Sex' => '1',
|
||||
'Birthdate' => $faker->date('Y-m-d'),
|
||||
'EmailAddress1' => 'update_' . $faker->numberBetween(1,999) . '@gmail.com',
|
||||
"Phone" => $faker->numerify('08##########'),
|
||||
|
||||
179
tests/feature/ValueSet/ValueSetApiControllerTest.php
Normal file
179
tests/feature/ValueSet/ValueSetApiControllerTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -114,7 +114,7 @@ class PatientModelTest extends CIUnitTestCase
|
||||
$this->assertIsArray($allowedFields);
|
||||
$this->assertContains('PatientID', $allowedFields);
|
||||
$this->assertContains('NameFirst', $allowedFields);
|
||||
$this->assertContains('Gender', $allowedFields);
|
||||
$this->assertContains('Sex', $allowedFields);
|
||||
$this->assertContains('Birthdate', $allowedFields);
|
||||
$this->assertContains('EmailAddress1', $allowedFields);
|
||||
$this->assertContains('Phone', $allowedFields);
|
||||
|
||||
110
tests/unit/ValueSet/LookupsBackwardCompatibilityTest.php
Normal file
110
tests/unit/ValueSet/LookupsBackwardCompatibilityTest.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
380
tests/unit/ValueSet/ValueSetTest.php
Normal file
380
tests/unit/ValueSet/ValueSetTest.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user