diff --git a/.factory/config.json b/.factory/config.json deleted file mode 100644 index 8a75ea0..0000000 --- a/.factory/config.json +++ /dev/null @@ -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 - } - ] -} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5a98dc9 --- /dev/null +++ b/AGENTS.md @@ -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 +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 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 0c9e95e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -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 diff --git a/MVP_TODO.md b/MVP_TODO.md new file mode 100644 index 0000000..344a285 --- /dev/null +++ b/MVP_TODO.md @@ -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 " \ + -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 diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d933534..3475865 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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'); diff --git a/app/Controllers/OrderTestController.php b/app/Controllers/OrderTestController.php index a7ed0da..2565720 100644 --- a/app/Controllers/OrderTestController.php +++ b/app/Controllers/OrderTestController.php @@ -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'); + + try { + if ($internalPID) { + $rows = $this->model->getOrdersByPatient($internalPID); + } else { + $rows = $this->db->table('ordertest') + ->where('DelDate', null) + ->orderBy('OrderDateTime', 'DESC') + ->get() + ->getResultArray(); + } - if (empty($rows)) { return $this->respond([ - 'status' => 'success', - 'message' => "no Data.", - 'data' => [], + 'status' => 'success', + 'message' => 'Data fetched successfully', + 'data' => $rows ], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); } - - return $this->respond([ - 'status' => 'success', - 'message'=> "fetch success", - 'data' => $rows, - ], 200); } - public function show($OrderID = null) { - $row=$this->db->table('ordertest')->select("*")->where('OrderID', $OrderID)->get()->getRowArray(); - - if (empty($row)) { + 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 + ], 200); + } return $this->respond([ - 'status' => 'success', - 'message' => "Data not found.", - 'data' => null, + 'status' => 'success', + 'message' => 'Data fetched successfully', + 'data' => $row ], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); } - - return $this->respond([ - 'status' => 'success', - 'message'=> "Data fetched successfully", - 'data' => $row, - ], 200); } public function create() { + $input = $this->request->getJSON(true); + + if (!$this->validateData($input, $this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + 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)) { - return $this->failValidationErrors($this->validator->getErrors()); + if (!$this->patientModel->find($input['InternalPID'])) { + return $this->failValidationErrors(['InternalPID' => 'Patient not found']); } - // 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); + if (!empty($input['PatVisitID'])) { + $visit = $this->visitModel->find($input['PatVisitID']); + if (!$visit) { + return $this->failValidationErrors(['PatVisitID' => 'Visit 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') - ); - } + $orderID = $this->model->createOrder($input); return $this->respondCreated([ - 'status' => 'success', - 'message' => 'Location created successfully', - 'data' => $dataLocation, + 'status' => 'success', + '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() { + $input = $this->request->getJSON(true); + + if (empty($input['OrderID'])) { + return $this->failValidationErrors(['OrderID' => 'OrderID is required']); + } + 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()); + $order = $this->model->getOrder($input['OrderID']); + if (!$order) { + return $this->failNotFound('Order not found'); } - // Start transaction - $this->db->transStart(); + $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']; - // 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); + if (!empty($updateData)) { + $this->model->update($input['OrderID'], $updateData); } - // Complete transaction - $this->db->transComplete(); - - 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') - ); - } - - return $this->respondCreated([ - 'status' => 'success', - 'message' => 'Location updated successfully', - 'data' => $dataLocation, - ], 201); - - } catch (\Throwable $e) { - // Ensure rollback if something goes wrong - if ($this->db->transStatus() !== false) { - $this->db->transRollback(); - } + return $this->respond([ + 'status' => 'success', + '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() { + $input = $this->request->getJSON(true); + $orderID = $input['OrderID'] ?? null; + + if (empty($orderID)) { + return $this->failValidationErrors(['OrderID' => 'OrderID is required']); + } + try { - $input = $this->request->getJSON(true); - $LocationID = $input["LocationID"]; - if (!$LocationID) { - return $this->failValidationError('LocationID is required.'); + $order = $this->model->getOrder($orderID); + if (!$order) { + return $this->failNotFound('Order not found'); } - - $location = $this->db->table('location')->where('LocationID', $LocationID)->get()->getRow(); - if (!$location) { - return $this->failNotFound("LocationID with {$LocationID} not found."); - } - - $this->db->table('location')->where('LocationID', $LocationID)->update(['DelDate' => NOW()]); + $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, - ]; + $validStatuses = ['ORD', 'SCH', 'ANA', 'VER', 'REV', 'REP']; + if (!in_array($input['OrderStatus'], $validStatuses)) { + return $this->failValidationErrors(['OrderStatus' => 'Invalid status. Valid: ' . implode(', ', $validStatuses)]); + } - if(!empty($input["LocationID"])) { $data["LocationID"] = $input["LocationID"]; } + try { + $order = $this->model->getOrder($input['OrderID']); + if (!$order) { + return $this->failNotFound('Order not found'); + } - return $data; - } + $this->model->updateStatus($input['OrderID'], $input['OrderStatus']); - private function prepareLocationAddressData(array $input): array { - $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, - ]; - - return $data; + 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()); + } } } diff --git a/app/Controllers/Patient/PatientController.php b/app/Controllers/Patient/PatientController.php index ae63990..666775d 100644 --- a/app/Controllers/Patient/PatientController.php +++ b/app/Controllers/Patient/PatientController.php @@ -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]', diff --git a/app/Controllers/Test/DemoOrderController.php b/app/Controllers/Test/DemoOrderController.php new file mode 100644 index 0000000..5d2cb6f --- /dev/null +++ b/app/Controllers/Test/DemoOrderController.php @@ -0,0 +1,78 @@ +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); + } +} diff --git a/app/Controllers/TestsController.php b/app/Controllers/TestsController.php index b43ba6c..34bc231 100644 --- a/app/Controllers/TestsController.php +++ b/app/Controllers/TestsController.php @@ -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')]); diff --git a/app/Controllers/ValueSet/ValueSetController.php b/app/Controllers/ValueSet/ValueSetController.php index eaa2257..7480d13 100644 --- a/app/Controllers/ValueSet/ValueSetController.php +++ b/app/Controllers/ValueSet/ValueSetController.php @@ -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); } - } diff --git a/app/Controllers/ValueSetApiController.php b/app/Controllers/ValueSetApiController.php new file mode 100644 index 0000000..d537e5b --- /dev/null +++ b/app/Controllers/ValueSetApiController.php @@ -0,0 +1,38 @@ +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); + } +} diff --git a/app/Database/Migrations/2026-01-12-000001_ValuesetVidToVvalue.php b/app/Database/Migrations/2026-01-12-000001_ValuesetVidToVvalue.php new file mode 100644 index 0000000..b9dae01 --- /dev/null +++ b/app/Database/Migrations/2026-01-12-000001_ValuesetVidToVvalue.php @@ -0,0 +1,124 @@ +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], + ]); + } +} diff --git a/app/Database/Migrations/2026-01-12-000002_RenamePatientGenderToSex.php b/app/Database/Migrations/2026-01-12-000002_RenamePatientGenderToSex.php new file mode 100644 index 0000000..ee32856 --- /dev/null +++ b/app/Database/Migrations/2026-01-12-000002_RenamePatientGenderToSex.php @@ -0,0 +1,19 @@ +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], + ]); + } +} diff --git a/app/Database/Seeds/PatientSeeder.php b/app/Database/Seeds/PatientSeeder.php index 0a97504..a18561e 100644 --- a/app/Database/Seeds/PatientSeeder.php +++ b/app/Database/Seeds/PatientSeeder.php @@ -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', diff --git a/app/Libraries/Data/valuesets/_meta.json b/app/Libraries/Data/valuesets/_meta.json new file mode 100644 index 0000000..c87e8cd --- /dev/null +++ b/app/Libraries/Data/valuesets/_meta.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/activity_result.json b/app/Libraries/Data/valuesets/activity_result.json new file mode 100644 index 0000000..8a9fcdb --- /dev/null +++ b/app/Libraries/Data/valuesets/activity_result.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/additive.json b/app/Libraries/Data/valuesets/additive.json new file mode 100644 index 0000000..1364ece --- /dev/null +++ b/app/Libraries/Data/valuesets/additive.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/adt_event.json b/app/Libraries/Data/valuesets/adt_event.json new file mode 100644 index 0000000..5174faf --- /dev/null +++ b/app/Libraries/Data/valuesets/adt_event.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/area_class.json b/app/Libraries/Data/valuesets/area_class.json new file mode 100644 index 0000000..47ba62f --- /dev/null +++ b/app/Libraries/Data/valuesets/area_class.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/body_site.json b/app/Libraries/Data/valuesets/body_site.json new file mode 100644 index 0000000..991d550 --- /dev/null +++ b/app/Libraries/Data/valuesets/body_site.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/collection_method.json b/app/Libraries/Data/valuesets/collection_method.json new file mode 100644 index 0000000..05f21a1 --- /dev/null +++ b/app/Libraries/Data/valuesets/collection_method.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/container_cap_color.json b/app/Libraries/Data/valuesets/container_cap_color.json new file mode 100644 index 0000000..a030e81 --- /dev/null +++ b/app/Libraries/Data/valuesets/container_cap_color.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/container_class.json b/app/Libraries/Data/valuesets/container_class.json new file mode 100644 index 0000000..c91ce3d --- /dev/null +++ b/app/Libraries/Data/valuesets/container_class.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/container_size.json b/app/Libraries/Data/valuesets/container_size.json new file mode 100644 index 0000000..368bcaa --- /dev/null +++ b/app/Libraries/Data/valuesets/container_size.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/country.json b/app/Libraries/Data/valuesets/country.json new file mode 100644 index 0000000..f1b301e --- /dev/null +++ b/app/Libraries/Data/valuesets/country.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/death_indicator.json b/app/Libraries/Data/valuesets/death_indicator.json new file mode 100644 index 0000000..a49f808 --- /dev/null +++ b/app/Libraries/Data/valuesets/death_indicator.json @@ -0,0 +1,10 @@ +{ + "VSetID": 5, + "name": "death_indicator", + "VSName": "Death Indicator", + "VCategory": "System", + "values": [ + {"key": "Y", "value": "Death"}, + {"key": "N", "value": "Life"} + ] +} diff --git a/app/Libraries/Data/valuesets/did_type.json b/app/Libraries/Data/valuesets/did_type.json new file mode 100644 index 0000000..65e17f7 --- /dev/null +++ b/app/Libraries/Data/valuesets/did_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/enable_disable.json b/app/Libraries/Data/valuesets/enable_disable.json new file mode 100644 index 0000000..b612359 --- /dev/null +++ b/app/Libraries/Data/valuesets/enable_disable.json @@ -0,0 +1,10 @@ +{ + "VSetID": 2, + "name": "enable_disable", + "VSName": "Enable/Disable", + "VCategory": "System", + "values": [ + {"key": "0", "value": "Disabled"}, + {"key": "1", "value": "Enabled"} + ] +} diff --git a/app/Libraries/Data/valuesets/entity_type.json b/app/Libraries/Data/valuesets/entity_type.json new file mode 100644 index 0000000..081ff58 --- /dev/null +++ b/app/Libraries/Data/valuesets/entity_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/ethnic.json b/app/Libraries/Data/valuesets/ethnic.json new file mode 100644 index 0000000..40cfdcf --- /dev/null +++ b/app/Libraries/Data/valuesets/ethnic.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/fasting_status.json b/app/Libraries/Data/valuesets/fasting_status.json new file mode 100644 index 0000000..f2f0681 --- /dev/null +++ b/app/Libraries/Data/valuesets/fasting_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/formula_language.json b/app/Libraries/Data/valuesets/formula_language.json new file mode 100644 index 0000000..544d857 --- /dev/null +++ b/app/Libraries/Data/valuesets/formula_language.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/gender.json b/app/Libraries/Data/valuesets/gender.json new file mode 100644 index 0000000..bc3754b --- /dev/null +++ b/app/Libraries/Data/valuesets/gender.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/generate_by.json b/app/Libraries/Data/valuesets/generate_by.json new file mode 100644 index 0000000..77ffca7 --- /dev/null +++ b/app/Libraries/Data/valuesets/generate_by.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/identifier_type.json b/app/Libraries/Data/valuesets/identifier_type.json new file mode 100644 index 0000000..b479d30 --- /dev/null +++ b/app/Libraries/Data/valuesets/identifier_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/location_type.json b/app/Libraries/Data/valuesets/location_type.json new file mode 100644 index 0000000..8a179ce --- /dev/null +++ b/app/Libraries/Data/valuesets/location_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/marital_status.json b/app/Libraries/Data/valuesets/marital_status.json new file mode 100644 index 0000000..0de77e2 --- /dev/null +++ b/app/Libraries/Data/valuesets/marital_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/math_sign.json b/app/Libraries/Data/valuesets/math_sign.json new file mode 100644 index 0000000..8e1af8b --- /dev/null +++ b/app/Libraries/Data/valuesets/math_sign.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/numeric_ref_type.json b/app/Libraries/Data/valuesets/numeric_ref_type.json new file mode 100644 index 0000000..3c583ce --- /dev/null +++ b/app/Libraries/Data/valuesets/numeric_ref_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/operation.json b/app/Libraries/Data/valuesets/operation.json new file mode 100644 index 0000000..2b738fe --- /dev/null +++ b/app/Libraries/Data/valuesets/operation.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/order_priority.json b/app/Libraries/Data/valuesets/order_priority.json new file mode 100644 index 0000000..a3202f5 --- /dev/null +++ b/app/Libraries/Data/valuesets/order_priority.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/order_status.json b/app/Libraries/Data/valuesets/order_status.json new file mode 100644 index 0000000..cc0e1a7 --- /dev/null +++ b/app/Libraries/Data/valuesets/order_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/priority.json b/app/Libraries/Data/valuesets/priority.json new file mode 100644 index 0000000..026d0b3 --- /dev/null +++ b/app/Libraries/Data/valuesets/priority.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/race.json b/app/Libraries/Data/valuesets/race.json new file mode 100644 index 0000000..8eab480 --- /dev/null +++ b/app/Libraries/Data/valuesets/race.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/range_type.json b/app/Libraries/Data/valuesets/range_type.json new file mode 100644 index 0000000..efd3b9a --- /dev/null +++ b/app/Libraries/Data/valuesets/range_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/reference_type.json b/app/Libraries/Data/valuesets/reference_type.json new file mode 100644 index 0000000..898cc74 --- /dev/null +++ b/app/Libraries/Data/valuesets/reference_type.json @@ -0,0 +1,10 @@ +{ + "VSetID": 44, + "name": "reference_type", + "VSName": "Reference Type", + "VCategory": "User-defined", + "values": [ + {"key": "NMRC", "value": "Numeric"}, + {"key": "TEXT", "value": "Text"} + ] +} diff --git a/app/Libraries/Data/valuesets/religion.json b/app/Libraries/Data/valuesets/religion.json new file mode 100644 index 0000000..b373db3 --- /dev/null +++ b/app/Libraries/Data/valuesets/religion.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/request_status.json b/app/Libraries/Data/valuesets/request_status.json new file mode 100644 index 0000000..ab48ed9 --- /dev/null +++ b/app/Libraries/Data/valuesets/request_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/requested_entity.json b/app/Libraries/Data/valuesets/requested_entity.json new file mode 100644 index 0000000..ba4d192 --- /dev/null +++ b/app/Libraries/Data/valuesets/requested_entity.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/result_status.json b/app/Libraries/Data/valuesets/result_status.json new file mode 100644 index 0000000..e84fc1e --- /dev/null +++ b/app/Libraries/Data/valuesets/result_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/result_type.json b/app/Libraries/Data/valuesets/result_type.json new file mode 100644 index 0000000..e59f160 --- /dev/null +++ b/app/Libraries/Data/valuesets/result_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/result_unit.json b/app/Libraries/Data/valuesets/result_unit.json new file mode 100644 index 0000000..9710db7 --- /dev/null +++ b/app/Libraries/Data/valuesets/result_unit.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/site_class.json b/app/Libraries/Data/valuesets/site_class.json new file mode 100644 index 0000000..949ba7f --- /dev/null +++ b/app/Libraries/Data/valuesets/site_class.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/site_type.json b/app/Libraries/Data/valuesets/site_type.json new file mode 100644 index 0000000..019d8dc --- /dev/null +++ b/app/Libraries/Data/valuesets/site_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/specimen_activity.json b/app/Libraries/Data/valuesets/specimen_activity.json new file mode 100644 index 0000000..6fc98be --- /dev/null +++ b/app/Libraries/Data/valuesets/specimen_activity.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/specimen_condition.json b/app/Libraries/Data/valuesets/specimen_condition.json new file mode 100644 index 0000000..6358077 --- /dev/null +++ b/app/Libraries/Data/valuesets/specimen_condition.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/specimen_role.json b/app/Libraries/Data/valuesets/specimen_role.json new file mode 100644 index 0000000..f3b0c0b --- /dev/null +++ b/app/Libraries/Data/valuesets/specimen_role.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/specimen_status.json b/app/Libraries/Data/valuesets/specimen_status.json new file mode 100644 index 0000000..e3647fc --- /dev/null +++ b/app/Libraries/Data/valuesets/specimen_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/specimen_type.json b/app/Libraries/Data/valuesets/specimen_type.json new file mode 100644 index 0000000..a3a97b6 --- /dev/null +++ b/app/Libraries/Data/valuesets/specimen_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/test_activity.json b/app/Libraries/Data/valuesets/test_activity.json new file mode 100644 index 0000000..64e1ab7 --- /dev/null +++ b/app/Libraries/Data/valuesets/test_activity.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/test_status.json b/app/Libraries/Data/valuesets/test_status.json new file mode 100644 index 0000000..19f7af0 --- /dev/null +++ b/app/Libraries/Data/valuesets/test_status.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/test_type.json b/app/Libraries/Data/valuesets/test_type.json new file mode 100644 index 0000000..6dfc543 --- /dev/null +++ b/app/Libraries/Data/valuesets/test_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/text_ref_type.json b/app/Libraries/Data/valuesets/text_ref_type.json new file mode 100644 index 0000000..a50d3e6 --- /dev/null +++ b/app/Libraries/Data/valuesets/text_ref_type.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/unit.json b/app/Libraries/Data/valuesets/unit.json new file mode 100644 index 0000000..555497e --- /dev/null +++ b/app/Libraries/Data/valuesets/unit.json @@ -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"} + ] +} diff --git a/app/Libraries/Data/valuesets/v_category.json b/app/Libraries/Data/valuesets/v_category.json new file mode 100644 index 0000000..6d9f496 --- /dev/null +++ b/app/Libraries/Data/valuesets/v_category.json @@ -0,0 +1,10 @@ +{ + "VSetID": 42, + "name": "v_category", + "VSName": "VCategory", + "VCategory": "System", + "values": [ + {"key": "0", "value": "System"}, + {"key": "1", "value": "User-defined"} + ] +} diff --git a/app/Libraries/Data/valuesets/ws_type.json b/app/Libraries/Data/valuesets/ws_type.json new file mode 100644 index 0000000..64bd148 --- /dev/null +++ b/app/Libraries/Data/valuesets/ws_type.json @@ -0,0 +1,10 @@ +{ + "VSetID": 1, + "name": "ws_type", + "VSName": "Workstation Type", + "VCategory": "System", + "values": [ + {"key": "0", "value": "Primary"}, + {"key": "1", "value": "Secondary"} + ] +} diff --git a/app/Libraries/Lookups.php b/app/Libraries/Lookups.php index 903cd08..ec0b009 100644 --- a/app/Libraries/Lookups.php +++ b/app/Libraries/Lookups.php @@ -1,649 +1,5 @@ '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 { } diff --git a/app/Libraries/ValueSet.php b/app/Libraries/ValueSet.php new file mode 100644 index 0000000..0d86fe3 --- /dev/null +++ b/app/Libraries/ValueSet.php @@ -0,0 +1,108 @@ +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); + } +} diff --git a/app/Models/Location/LocationModel.php b/app/Models/Location/LocationModel.php index 569a2fe..e2dbadb 100644 --- a/app/Models/Location/LocationModel.php +++ b/app/Models/Location/LocationModel.php @@ -1,6 +1,7 @@ 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 { } } } - \ No newline at end of file diff --git a/app/Models/OrderTest/OrderTestModel.php b/app/Models/OrderTest/OrderTestModel.php new file mode 100644 index 0000000..b610ff7 --- /dev/null +++ b/app/Models/OrderTest/OrderTestModel.php @@ -0,0 +1,102 @@ +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')]); + } +} diff --git a/app/Models/Organization/AccountModel.php b/app/Models/Organization/AccountModel.php index 6d8023f..24d1018 100644 --- a/app/Models/Organization/AccountModel.php +++ b/app/Models/Organization/AccountModel.php @@ -1,6 +1,7 @@ select('account.AccountID, account.AccountName, account.Parent, pa.AccountName as ParentName, account.Initial') ->join('account pa', 'pa.AccountID=account.Parent', 'left'); @@ -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; } } diff --git a/app/Models/Organization/SiteModel.php b/app/Models/Organization/SiteModel.php index 7f9ef3a..35a764f 100644 --- a/app/Models/Organization/SiteModel.php +++ b/app/Models/Organization/SiteModel.php @@ -1,6 +1,7 @@ select('site.SiteID, site.SiteCode, site.SiteName, s1.SiteName as ParentName, account.AccountName') ->join('account', 'account.AccountID=site.AccountID', 'left') @@ -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; } } diff --git a/app/Models/Organization/WorkstationModel.php b/app/Models/Organization/WorkstationModel.php index 626e088..a0737e3 100644 --- a/app/Models/Organization/WorkstationModel.php +++ b/app/Models/Organization/WorkstationModel.php @@ -1,6 +1,7 @@ select('workstation.*, department.DepartmentName, wst1.WorkstationName as LinkToName') ->join('workstation wst1', 'workstation.LinkTo=wst1.WorkstationID', 'left') ->join('department', 'department.DepartmentID=workstation.DepartmentID', 'left'); - + if (!empty($filter['WorkstationCode'])) { $this->like('workstation.WorkstationCode', $filter['WorkstationCode'], 'both'); } @@ -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; } } diff --git a/app/Models/Patient/PatientModel.php b/app/Models/Patient/PatientModel.php index 705c740..822df55 100644 --- a/app/Models/Patient/PatientModel.php +++ b/app/Models/Patient/PatientModel.php @@ -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,9 +11,9 @@ 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' ]; + 'DeathIndicator', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ]; protected $useTimestamps = true; protected $createdField = 'CreateDate'; @@ -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'); diff --git a/app/Models/Specimen/ContainerDefModel.php b/app/Models/Specimen/ContainerDefModel.php index 524ee79..0fa8627 100644 --- a/app/Models/Specimen/ContainerDefModel.php +++ b/app/Models/Specimen/ContainerDefModel.php @@ -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; } -} \ No newline at end of file +} diff --git a/app/Models/Test/TestDefGrpModel.php b/app/Models/Test/TestDefGrpModel.php index dcd6d0a..2a4f951 100644 --- a/app/Models/Test/TestDefGrpModel.php +++ b/app/Models/Test/TestDefGrpModel.php @@ -3,6 +3,7 @@ namespace App\Models\Test; use App\Models\BaseModel; +use App\Libraries\ValueSet; class TestDefGrpModel extends BaseModel { protected $table = 'testdefgrp'; @@ -13,32 +14,31 @@ class TestDefGrpModel extends BaseModel { 'CreateDate', 'EndDate' ]; - + protected $useTimestamps = true; protected $createdField = 'CreateDate'; protected $updatedField = ''; 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') diff --git a/app/Models/Test/TestDefSiteModel.php b/app/Models/Test/TestDefSiteModel.php index 52986d8..e375147 100644 --- a/app/Models/Test/TestDefSiteModel.php +++ b/app/Models/Test/TestDefSiteModel.php @@ -3,6 +3,7 @@ namespace App\Models\Test; use App\Models\BaseModel; +use App\Libraries\ValueSet; class TestDefSiteModel extends BaseModel { protected $table = 'testdefsite'; @@ -23,23 +24,18 @@ class TestDefSiteModel extends BaseModel { 'CreateDate', 'StartDate', 'EndDate' - ]; - + ]; + protected $useTimestamps = true; protected $createdField = 'CreateDate'; protected $updatedField = 'StartDate'; 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(); } diff --git a/app/Views/v2/master/organization/account_dialog.php b/app/Views/v2/master/organization/account_dialog.php index 51011a2..2aa3b4b 100644 --- a/app/Views/v2/master/organization/account_dialog.php +++ b/app/Views/v2/master/organization/account_dialog.php @@ -148,30 +148,30 @@ -
-
- - +
+
+ + +
+
+ + +
-
- - -
-
diff --git a/app/Views/v2/master/organization/accounts_index.php b/app/Views/v2/master/organization/accounts_index.php index 2adc0fc..1db0a44 100644 --- a/app/Views/v2/master/organization/accounts_index.php +++ b/app/Views/v2/master/organization/accounts_index.php @@ -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 diff --git a/app/Views/v2/master/organization/site_dialog.php b/app/Views/v2/master/organization/site_dialog.php index 16d8fdc..335c951 100644 --- a/app/Views/v2/master/organization/site_dialog.php +++ b/app/Views/v2/master/organization/site_dialog.php @@ -92,6 +92,31 @@ +
+ + +
+ + +
+
+ + +
diff --git a/app/Views/v2/master/organization/workstations_index.php b/app/Views/v2/master/organization/workstations_index.php index d2208c9..1fc6a52 100644 --- a/app/Views/v2/master/organization/workstations_index.php +++ b/app/Views/v2/master/organization/workstations_index.php @@ -89,7 +89,7 @@ - +
@@ -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 = {}; diff --git a/app/Views/v2/master/specimen/container_dialog.php b/app/Views/v2/master/specimen/container_dialog.php index 5fdc612..886bd2f 100644 --- a/app/Views/v2/master/specimen/container_dialog.php +++ b/app/Views/v2/master/specimen/container_dialog.php @@ -71,12 +71,12 @@ - +
@@ -96,23 +96,23 @@ - +
- +
diff --git a/app/Views/v2/master/specimen/containers_index.php b/app/Views/v2/master/specimen/containers_index.php index 9f22c1a..a27f00b 100644 --- a/app/Views/v2/master/specimen/containers_index.php +++ b/app/Views/v2/master/specimen/containers_index.php @@ -90,10 +90,10 @@
- +
- +