● refactor: update API responses to use {field}Label format
- Transform coded fields to lowercase with Label suffix for display text - Controllers: OrderTestController, DemoOrderController, SpecimenController,
SpecimenStatusController, SpecimenCollectionController, ContainerDefController,
ContactController, TestMapController
- Example: Priority: "R" → priority: "R", priorityLabel: "Routine"
- Update api-docs.yaml with new OpenAPI schema definitions
- Add API docs reminder to CLAUDE.md
This commit is contained in:
parent
e36e390f71
commit
e5ac1957fe
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/cache
|
||||
89
.serena/project.yml
Normal file
89
.serena/project.yml
Normal file
@ -0,0 +1,89 @@
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# powershell python python_jedi r rego
|
||||
# ruby ruby_solargraph rust scala swift
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- php
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in all projects
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "clqms01"
|
||||
included_optional_tools: []
|
||||
235
AGENTS.md
235
AGENTS.md
@ -1,235 +0,0 @@
|
||||
# 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/
|
||||
│ └── ValueSet.php # Base lookup class (loads from JSON)
|
||||
│ └── Lookups.php # Extends ValueSet - use this for lookups
|
||||
└── Views/v2/
|
||||
```
|
||||
|
||||
### Imports and Namespaces
|
||||
```php
|
||||
<?php
|
||||
namespace App\Controllers\Patient;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Models\Patient\PatientModel;
|
||||
```
|
||||
|
||||
- Use fully qualified class names or `use` statements
|
||||
- Group imports logically
|
||||
- Avoid unnecessary aliases
|
||||
|
||||
### Code Formatting
|
||||
- **Indentation:** 4 spaces (not tabs)
|
||||
- **Braces:** Allman style for classes/functions, K&R for control structures
|
||||
- **Line length:** Soft limit 120 characters
|
||||
- **Empty lines:** Single blank line between method definitions and logical groups
|
||||
|
||||
### Type Hints and Return Types
|
||||
```php
|
||||
// Required for new code
|
||||
public function getPatient(int $internalPID): ?array
|
||||
protected function createPatient(array $input): int
|
||||
private function checkDbError(object $db, string $context): void
|
||||
|
||||
// Use nullable types for optional returns
|
||||
public function findById(?int $id): ?array
|
||||
```
|
||||
|
||||
### Controller Patterns
|
||||
```php
|
||||
class PatientController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->model = new PatientModel();
|
||||
$this->rules = [...]; // Validation rules
|
||||
}
|
||||
|
||||
public function index() {
|
||||
try {
|
||||
$data = $this->model->findAll();
|
||||
return $this->respond([...], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Patterns
|
||||
```php
|
||||
class PatientModel extends BaseModel {
|
||||
protected $table = 'patient';
|
||||
protected $primaryKey = 'InternalPID';
|
||||
protected $allowedFields = [...];
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = 'DelDate';
|
||||
|
||||
public function getPatients(array $filters = []): array {
|
||||
// Query builder chain
|
||||
$this->select('...');
|
||||
$this->join(...);
|
||||
if (!empty($filters['key'])) {
|
||||
$this->where('key', $filters['key']);
|
||||
}
|
||||
return $this->findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
- Controllers: Use try-catch with `failServerError()`, `failValidationErrors()`, `failNotFound()`
|
||||
- Models: Throw `\Exception` with descriptive messages
|
||||
- Database errors: Check `$db->error()` after operations
|
||||
- Always validate input before DB operations
|
||||
|
||||
### Validation Rules
|
||||
```php
|
||||
protected $rules = [
|
||||
'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
|
||||
'EmailAddress' => 'permit_empty|valid_email|max_length[100]',
|
||||
'Phone' => 'permit_empty|regex_match[/^\+?[0-9]{8,15}$/]',
|
||||
];
|
||||
```
|
||||
|
||||
### Date Handling
|
||||
- All dates stored/retrieved in UTC via `BaseModel` callbacks
|
||||
- Use `utc` helper functions: `convert_array_to_utc()`, `convert_array_to_utc_iso()`
|
||||
- Format: ISO 8601 (`Y-m-d\TH:i:s\Z`) for API responses
|
||||
|
||||
### API Response Format
|
||||
```php
|
||||
// Success
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Data fetched successfully',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
|
||||
// Created
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Record created'
|
||||
]);
|
||||
|
||||
// Error
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
```
|
||||
|
||||
### Database Transactions
|
||||
```php
|
||||
$db->transBegin();
|
||||
try {
|
||||
$this->insert($data);
|
||||
$this->checkDbError($db, 'Insert operation');
|
||||
$db->transCommit();
|
||||
return $insertId;
|
||||
} catch (\Exception $e) {
|
||||
$db->transRollback();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Integration (Alpine.js)
|
||||
- API calls use `BASEURL` global variable
|
||||
- Include `credentials: 'include'` for authenticated requests
|
||||
- Modals use `x-show` with `@click.self` backdrop close
|
||||
|
||||
### Lookups Library
|
||||
Use `App\Libraries\Lookups` (extends `ValueSet`) for all static lookup values. Data is loaded from JSON files in `app/Libraries/Data/valuesets/`:
|
||||
```php
|
||||
use App\Libraries\Lookups;
|
||||
|
||||
// Get formatted for frontend dropdowns [{value: 'X', label: 'Y'}, ...]
|
||||
$gender = Lookups::get('gender');
|
||||
$priorities = Lookups::get('order_priority');
|
||||
|
||||
// Get raw data [{key: 'X', value: 'Y'}, ...]
|
||||
$raw = Lookups::getRaw('gender');
|
||||
|
||||
// Get single label by key
|
||||
$label = Lookups::getLabel('gender', '1'); // Returns 'Female'
|
||||
|
||||
// Get options with key/value pairs
|
||||
$options = Lookups::getOptions('gender');
|
||||
// Returns: [['key' => '1', 'value' => 'Female'], ...]
|
||||
|
||||
// Transform data with lookup labels
|
||||
$patients = Lookups::transformLabels($patients, [
|
||||
'Sex' => 'gender',
|
||||
'Priority' => 'order_priority'
|
||||
]);
|
||||
|
||||
// Clear cache after data changes
|
||||
Lookups::clearCache();
|
||||
```
|
||||
|
||||
### 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
|
||||
200
CLAUDE.md
Normal file
200
CLAUDE.md
Normal file
@ -0,0 +1,200 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
---
|
||||
|
||||
## Tool Preference: Use Serena MCP
|
||||
|
||||
**Always prioritize Serena MCP tools for code operations to minimize tool calls:**
|
||||
|
||||
- **Use `find_symbol` / `get_symbols_overview`** instead of `Read` for exploring code structure
|
||||
- **Use `replace_symbol_body`** instead of `Edit` for modifying functions, methods, classes
|
||||
- **Use `insert_before_symbol` / `insert_after_symbol`** for adding new code symbols
|
||||
- **Use `search_for_pattern`** instead of `Grep` for searching code patterns
|
||||
- **Use `list_dir` / `find_file`** instead of `Glob` for file discovery
|
||||
|
||||
This leverages semantic code understanding for this PHP codebase.
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
vendor/bin/phpunit
|
||||
|
||||
# Run specific test file
|
||||
vendor/bin/phpunit tests/feature/UniformShowTest.php
|
||||
|
||||
# Run tests in a directory
|
||||
vendor/bin/phpunit app/Models
|
||||
|
||||
# Generate code coverage report
|
||||
vendor/bin/phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
|
||||
```
|
||||
|
||||
### Database
|
||||
```bash
|
||||
# Run migrations
|
||||
spark migrate
|
||||
|
||||
# Rollback migrations
|
||||
spark migrate:rollback
|
||||
|
||||
# Seed database
|
||||
spark db:seed DBSeeder
|
||||
|
||||
# Refresh all migrations and seed
|
||||
spark migrate:refresh --seed
|
||||
```
|
||||
|
||||
### CodeIgniter CLI
|
||||
```bash
|
||||
# General help
|
||||
spark help
|
||||
|
||||
# Generate code (scaffolding)
|
||||
spark make:model ModelName
|
||||
spark make:controller ControllerName
|
||||
spark make:migration MigrationName
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
```bash
|
||||
# Keep api-docs.yaml updated whenever controllers change
|
||||
# File: public/api-docs.yaml
|
||||
# After modifying controllers, update the OpenAPI schema definitions
|
||||
# to reflect new field names, types, and response formats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Pattern: MVC + Libraries
|
||||
|
||||
CLQMS follows CodeIgniter 4 conventions with these key architectural patterns:
|
||||
|
||||
**Controllers** (`app/Controllers/`)
|
||||
- Organized by domain: `Patient/`, `Organization/`, `Specimen/`, `Test/`, `Contact/`
|
||||
- Root-level controllers for cross-domain concerns: `AuthController`, `EdgeController`, `ValueSetController`
|
||||
- All controllers extend `BaseController` which provides request/response helpers
|
||||
|
||||
**Models** (`app/Models/`)
|
||||
- Organized by domain matching controllers: `Patient/`, `Organization/`, `Specimen/`, `Test/`, etc.
|
||||
- All models extend `BaseModel` which provides automatic UTC date normalization
|
||||
- Model callbacks: `beforeInsert/Update` normalize dates to UTC, `afterFind/Insert/Update` convert to UTC ISO format
|
||||
|
||||
**Libraries** (`app/Libraries/`)
|
||||
- `ValueSet` - Static lookup system using JSON files (see "Lookup System" below)
|
||||
|
||||
### Lookup System: ValueSet Library
|
||||
|
||||
The system uses a **JSON file-based lookup system** instead of database queries for static values.
|
||||
|
||||
**Location:** `app/Libraries/Data/*.json` (44+ JSON files)
|
||||
|
||||
**Key Files:**
|
||||
- `_meta.json` - Metadata about lookup definitions
|
||||
- `sex.json`, `order_status.json`, `specimen_type.json`, `test_type.json`, etc.
|
||||
|
||||
**Usage:**
|
||||
```php
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
// Get dropdown-formatted options
|
||||
$options = ValueSet::get('sex'); // [["value"=>"1","label"=>"Female"],...]
|
||||
|
||||
// Get raw data
|
||||
$raw = ValueSet::getRaw('sex'); // [["key"=>"1","value"=>"Female"],...]
|
||||
|
||||
// Get label for a specific key
|
||||
$label = ValueSet::getLabel('sex', '1'); // "Female"
|
||||
|
||||
// Transform database records with lookup text labels
|
||||
$data = ValueSet::transformLabels($patients, ['Sex' => 'sex']);
|
||||
|
||||
// Clear cache after modifying JSON files
|
||||
ValueSet::clearCache();
|
||||
```
|
||||
|
||||
**When to use ValueSet vs API:**
|
||||
- **ValueSet library** - Static values that rarely change (fast, cached)
|
||||
- **API `/api/valueset*`** - Dynamic values managed by admins at runtime
|
||||
|
||||
### Database Migrations Structure
|
||||
|
||||
Migrations are numbered sequentially starting with `2026-01-01-`:
|
||||
|
||||
| Migration | Tables Created |
|
||||
|-----------|----------------|
|
||||
| 000001 | valueset, counter, containerdef, occupation, specialty |
|
||||
| 000002 | account, site, location, discipline, department |
|
||||
| 000003 | patient, patidentifier, pataddress, patcontact |
|
||||
| 000004 | contact, contactdetail, userdevices, loginattempts |
|
||||
| 000005 | patvisit, patinsurance |
|
||||
| 000006 | porder, orderitem |
|
||||
| 000007 | specimen, specmenactivity, containerdef |
|
||||
| 000008 | testdefinition, testactivity, refnum, reftxt |
|
||||
| 000009 | patresult, patresultdetail, patresultcomment |
|
||||
| 000010 | edgeres, edgestatus, edgeack, workstation |
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- **JWT-based authentication** using `firebase/php-jwt`
|
||||
- **AuthFilter** - Route filter protecting API endpoints
|
||||
- **AuthController** - Login/logout endpoints (`POST /api/login`, `POST /api/logout`)
|
||||
- **AuthV2Controller** - V2 authentication endpoints
|
||||
|
||||
### Edge API (Instrument Integration)
|
||||
|
||||
The `EdgeController` provides endpoints for laboratory instrument integration via `tiny-edge` middleware:
|
||||
|
||||
- `POST /api/edge/results` - Receive instrument results (stored in `edgeres` table)
|
||||
- `GET /api/edge/orders` - Fetch pending orders for instruments
|
||||
- `POST /api/edge/orders/:id/ack` - Acknowledge order delivery
|
||||
- `POST /api/edge/status` - Log instrument status updates
|
||||
|
||||
**Workflow:** `Instrument → tiny-edge → edgeres table → [Processing] → patresult table`
|
||||
|
||||
### Test Types System
|
||||
|
||||
Tests in `testdefinition` table support multiple types via `TestType` field:
|
||||
|
||||
| Code | Type | Description |
|
||||
|------|------|-------------|
|
||||
| TEST | Technical | Individual lab test with specs |
|
||||
| PARAM | Parameter | Non-lab measurement |
|
||||
| CALC | Calculated | Test with formula |
|
||||
| GROUP | Panel/Profile | Contains multiple tests |
|
||||
| TITLE | Section | Report organization header |
|
||||
|
||||
### Reference Range Architecture
|
||||
|
||||
Reference ranges support multiple types for result validation:
|
||||
|
||||
| Type | Table | Purpose |
|
||||
|------|-------|---------|
|
||||
| Numeric | `refnum` | Ranges with age/sex criteria |
|
||||
| Threshold | `refthold` | Critical values |
|
||||
| Text | `reftxt` | Text-based references |
|
||||
| Value Set | `refvset` | Coded references |
|
||||
|
||||
### Routes Organization
|
||||
|
||||
Routes are defined in `app/Config/Routes.php`:
|
||||
- API routes: `/api/{resource}`
|
||||
- Auth-protected routes use `AuthFilter`
|
||||
|
||||
---
|
||||
|
||||
## Project Structure Notes
|
||||
|
||||
- **Language:** PHP 8.1+ (PSR-compliant)
|
||||
- **Framework:** CodeIgniter 4
|
||||
- **Database:** MySQL with migration-based schema management
|
||||
- **Testing:** PHPUnit 10.5+ (tests in `tests/` directory)
|
||||
- **Entry point:** `public/index.php` (web), `spark` (CLI)
|
||||
- **Environment config:** `.env` file (copy from `env` template)
|
||||
747
PRD.md
Normal file
747
PRD.md
Normal file
@ -0,0 +1,747 @@
|
||||
# CLQMS Product Requirements Document
|
||||
|
||||
> Clinical Laboratory Quality Management System - Minimum Viable Product
|
||||
>
|
||||
> Version: 1.0
|
||||
> Last Updated: 2026-01-28
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
CLQMS is a **REST API backend** for a Clinical Laboratory Quality Management System designed to manage the complete laboratory workflow from order creation to result reporting. The system integrates with laboratory instruments via the Edge API and provides comprehensive specimen tracking, result management, and quality control capabilities.
|
||||
|
||||
### Product Vision
|
||||
To provide a **headless, API-first** laboratory information system that serves as the backend for any frontend client (web, mobile, desktop) while streamlining laboratory operations, ensuring regulatory compliance, and integrating seamlessly with laboratory instruments.
|
||||
|
||||
### Core Value Propositions
|
||||
- **API-First Design**: Pure REST API with comprehensive endpoints for all laboratory operations
|
||||
- **Complete Workflow Coverage**: Order → Collection → Reception → Preparation → Analysis → Verification → Review → Reporting
|
||||
- **Instrument Integration**: Standard Edge API for bidirectional communication with laboratory analyzers
|
||||
- **Quality Management**: Built-in QC, calibration tracking, and audit trails
|
||||
- **Modern Architecture**: RESTful API, JWT authentication, UTC timestamp normalization
|
||||
- **Frontend Agnostic**: No view layer - clients consume JSON API responses
|
||||
|
||||
---
|
||||
|
||||
## 2. Product Goals
|
||||
|
||||
### MVP Success Criteria
|
||||
| Goal | Description | Success Metric |
|
||||
|------|-------------|----------------|
|
||||
| Patient Registration | Register patients with demographics | Patient can be created and retrieved |
|
||||
| Test Ordering | Generate valid order IDs and test mappings | Order ID format: LLYYMMDDXXXXX |
|
||||
| Specimen Tracking | Track specimens through entire lifecycle | Status updates: Collection→Transport→Reception→Prep→Analysis |
|
||||
| Result Entry | Enter results with reference range validation | Numeric, text, valueset, and range results supported |
|
||||
| Result Verification | Multi-level verification workflow | VER → REV → REP status tracking |
|
||||
| Instrument Integration | Receive results via Edge API | Results stored and processed to patient results |
|
||||
| Master Data Management | Complete lookup and reference data | All value sets and test definitions manageable |
|
||||
|
||||
---
|
||||
|
||||
## 3. Target Users
|
||||
|
||||
### API Consumers
|
||||
|
||||
| Consumer Type | Description | Key API Needs |
|
||||
|---------------|-------------|---------------|
|
||||
| Frontend Applications | Web apps, mobile apps, desktop clients consuming the API | Full CRUD operations, authentication, real-time updates |
|
||||
| Laboratory Instruments | Analyzers and middleware systems | Edge API for orders/results, status monitoring |
|
||||
| Third-Party Systems | HIS, EMR, billing systems | Patient data sync, order/results integration |
|
||||
| Integration Partners | External lab networks, reference labs | Order referral, result reporting APIs |
|
||||
|
||||
### API Client Types
|
||||
- **Web Clients**: Single-page applications (React, Vue, Angular) consuming REST endpoints
|
||||
- **Mobile Clients**: iOS/Android apps for mobile laboratory workflows
|
||||
- **Desktop Clients**: Native applications for result entry and verification
|
||||
- **Middleware Systems**: Instrument integration layer (tiny-edge)
|
||||
- **Batch Processors**: Scheduled jobs, data import/export utilities
|
||||
|
||||
---
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### 4.1 Priority Framework
|
||||
|
||||
| Priority | Definition | Timeline |
|
||||
|----------|------------|----------|
|
||||
| **P0** | Must-have for MVP | Phase 1 |
|
||||
| **P1** | Should-have for MVP | Phase 2 |
|
||||
| **P2** | Nice-to-have for production | Phase 3 |
|
||||
| **P3** | Future enhancement | Phase 4+ |
|
||||
|
||||
### 4.2 Master Data Requirements (P0 - Foundation)
|
||||
|
||||
> **Rationale:** All transactional API workflows depend on complete master data. Master data must be completed before order/test/result API operations can function properly.
|
||||
|
||||
#### 4.2.1 Value Sets & Lookups ✅
|
||||
| Requirement | Description | Status |
|
||||
|-------------|-------------|--------|
|
||||
| VS-001 | Value set definitions management | Complete |
|
||||
| VS-002 | Value set values management | Complete |
|
||||
| VS-003 | JSON-based static lookup system | Complete |
|
||||
| VS-004 | Lookup caching for performance | Complete |
|
||||
|
||||
**Key Value Sets:**
|
||||
- Demographics: sex, marital_status, race, ethnic
|
||||
- Orders: order_status, order_priority, priority
|
||||
- Specimens: specimen_type, specimen_status, specimen_condition
|
||||
- Results: result_type, result_status, result_unit
|
||||
- Organizations: site_type, location_type
|
||||
- Technical: test_type, unit, formula_language
|
||||
|
||||
#### 4.2.2 Test Definitions ✅
|
||||
| Requirement | Description | Status |
|
||||
|-------------|-------------|--------|
|
||||
| TD-001 | Test site definitions (testdefsite) | Complete |
|
||||
| TD-002 | Technical specifications (testdeftech) | Complete |
|
||||
| TD-003 | Calculated test formulas (testdefcal) | Complete |
|
||||
| TD-004 | Test groups/panels (testdefgrp) | Complete |
|
||||
| TD-005 | Test parameters | Complete |
|
||||
| TD-006 | Instrument test mapping (testmap) | Complete |
|
||||
|
||||
**Test Types Supported:**
|
||||
| Code | Type | Description |
|
||||
|------|------|-------------|
|
||||
| TEST | Technical | Individual lab test with specs |
|
||||
| PARAM | Parameter | Non-lab measurement |
|
||||
| CALC | Calculated | Test with formula |
|
||||
| GROUP | Panel/Profile | Contains multiple tests |
|
||||
| TITLE | Section | Report organization header |
|
||||
|
||||
#### 4.2.3 Reference Ranges ⚠️
|
||||
| Requirement | Description | Status |
|
||||
|-------------|-------------|--------|
|
||||
| RR-001 | Numeric reference ranges (refnum) | Complete |
|
||||
| RR-002 | Threshold ranges (refthold) | Missing migration |
|
||||
| RR-003 | Text reference ranges (reftxt) | Complete |
|
||||
| RR-004 | Value set reference ranges (refvset) | Missing migration |
|
||||
| RR-005 | Age/sex-based criteria | Complete (refnum) |
|
||||
| RR-006 | Critical value flagging | Needs implementation |
|
||||
|
||||
**Gap Analysis:** Missing database migrations for `refthold` and `refvset` tables.
|
||||
|
||||
#### 4.2.4 Organization Structure ✅
|
||||
| Requirement | Description | Status |
|
||||
|-------------|-------------|--------|
|
||||
| ORG-001 | Account management | Complete |
|
||||
| ORG-002 | Site management | Complete |
|
||||
| ORG-003 | Location management | Complete |
|
||||
| ORG-004 | Discipline management | Complete |
|
||||
| ORG-005 | Department management | Complete |
|
||||
| ORG-006 | Workstation management | Complete |
|
||||
|
||||
#### 4.2.5 Specimen Types ✅
|
||||
| Requirement | Description | Status |
|
||||
|-------------|-------------|--------|
|
||||
| SPEC-001 | Container definitions (containerdef) | Complete |
|
||||
| SPEC-002 | Specimen type catalog | Complete |
|
||||
| SPEC-003 | Collection methods | Complete |
|
||||
| SPEC-004 | Container classes and sizes | Complete |
|
||||
|
||||
### 4.3 Order Management (P0)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| ORD-001 | Create order with patient linkage | OrderID generated in LLYYMMDDXXXXX format |
|
||||
| ORD-002 | Update order details | All fields editable before verification |
|
||||
| ORD-003 | Delete order (soft delete) | DelDate set, order not displayed |
|
||||
| ORD-004 | Order status tracking | ORD → SCH → ANA → VER → REV → REP |
|
||||
| ORD-005 | Priority assignment | Stat/Routine/ASAP |
|
||||
| ORD-006 | Order attachment handling | Support for ordercom, orderatt tables |
|
||||
| ORD-007 | Test-to-order mapping | Multiple tests per order |
|
||||
| ORD-008 | Calculated test auto-selection | Parameters auto-added based on formulas |
|
||||
| ORD-009 | Provider identification | Ordering provider captured |
|
||||
| ORD-010 | Order date/time tracking | UTC timestamps normalized |
|
||||
|
||||
**Minimum Data Required for Order Creation:**
|
||||
```sql
|
||||
-- Patient (at least 1 required)
|
||||
-- Order Status values (VSetID=11): ORD, SCH, ANA, VER, REV, REP
|
||||
-- Priority values (VSetID=10): S, R, A
|
||||
-- Counter for Order ID generation
|
||||
```
|
||||
|
||||
### 4.4 Specimen Management (P0)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| SPM-001 | Create specimen from order | SpecimenID = OrderID + SSS + C |
|
||||
| SPM-002 | Collection status | Specimen marked as collected |
|
||||
| SPM-003 | In-transit status | Track specimen transport |
|
||||
| SPM-004 | Reception | Received or Rejected status |
|
||||
| SPM-005 | Preparation | Centrifuge, Aliquot, Pre-treatment |
|
||||
| SPM-006 | Storage | Stored status with location |
|
||||
| SPM-007 | Dispatch | Dispatch status for referral |
|
||||
| SPM-008 | Condition tracking | HEM, ITC, LIP flag support |
|
||||
| SPM-009 | Activity logging | Full audit trail via specmenactivity |
|
||||
| SPM-010 | Specimen-test linking | Tests mapped to specimens |
|
||||
|
||||
**Specimen ID Format:** `OrderID + SpecimenTypeCode(3) + ContainerCode(1)`
|
||||
|
||||
**Specimen Conditions:**
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| HEM | Hemolysis |
|
||||
| ITC | Insufficient |
|
||||
| LIP | Lipemic |
|
||||
|
||||
### 4.5 Result Management (P0)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| RES-001 | Numeric result entry | Value with unit, range validation |
|
||||
| RES-002 | Text result entry | Free text or coded values |
|
||||
| RES-003 | Value set result entry | Dropdown selection from valueset |
|
||||
| RES-004 | Range result entry | Min/max with reference |
|
||||
| RES-005 | Reference range validation | Auto-flag abnormal results |
|
||||
| RES-006 | Critical value flagging | Threshold-based alerts |
|
||||
| RES-007 | Result verification (Technical) | First-level verification (VER status) |
|
||||
| RES-008 | Result verification (Clinical) | Pathologist review (REV status) |
|
||||
| RES-009 | Result reporting | Final report status (REP) |
|
||||
| RES-010 | Result rerun | AspCnt tracking for reruns |
|
||||
| RES-011 | Result comments | Support for patresultcomment |
|
||||
| RES-012 | Result details | Multiple details per result (patresultdetail) |
|
||||
|
||||
**Result Verification Workflow:**
|
||||
```
|
||||
Entry → VER (Technical) → REV (Clinical) → REP (Reported)
|
||||
```
|
||||
|
||||
### 4.6 Patient Visit (P1)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| VIS-001 | Create patient visit | Visit linked to patient |
|
||||
| VIS-002 | Visit-to-order linkage | Orders associated with visits |
|
||||
| VIS-003 | ADT tracking | Admission, Discharge, Transfer |
|
||||
| VIS-004 | Diagnosis linking | patdiag table support |
|
||||
| VIS-005 | Insurance tracking | patinsurance table support |
|
||||
|
||||
### 4.7 Instrument Integration (P1)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| EDGE-001 | Receive instrument results | POST /api/edge/results |
|
||||
| EDGE-002 | Store raw results | edgeres table |
|
||||
| EDGE-003 | Status logging | edgestatus tracking |
|
||||
| EDGE-004 | Order acknowledgment | edgeack confirmation |
|
||||
| EDGE-005 | Fetch pending orders | GET /api/edge/orders |
|
||||
| EDGE-006 | Acknowledge order delivery | POST /api/edge/orders/:id/ack |
|
||||
| EDGE-007 | Instrument status updates | POST /api/edge/status |
|
||||
|
||||
**Edge API Flow:**
|
||||
```
|
||||
Instrument → tiny-edge → /api/edge/results → edgeres table → Processing → patresult table
|
||||
```
|
||||
|
||||
### 4.8 Quality Control (P2)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| QC-001 | QC result entry | calres table storage |
|
||||
| QC-002 | Levey-Jennings data | Data preparation endpoints |
|
||||
| QC-003 | QC validation | 2SD auto-validation rules |
|
||||
| QC-004 | Sigma metrics | Calculation endpoint |
|
||||
| QC-005 | QC history | Trend analysis support |
|
||||
|
||||
### 4.9 Calibration (P2)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| CAL-001 | Calibration result entry | Factor tracking |
|
||||
| CAL-002 | Calibration history | Historical query endpoint |
|
||||
| CAL-003 | Calibration validation | Rule-based validation |
|
||||
|
||||
### 4.10 Audit Trail (P2)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| AUD-001 | Audit logging middleware | Automatic change capture |
|
||||
| AUD-002 | What/who/when tracking | Complete change record |
|
||||
| AUD-003 | Security logging | Authentication attempts |
|
||||
| AUD-004 | Audit query endpoint | Searchable audit logs |
|
||||
|
||||
### 4.11 Inventory & Billing (P3)
|
||||
|
||||
| Requirement | Description | Acceptance Criteria |
|
||||
|-------------|-------------|---------------------|
|
||||
| INV-001 | Counter management | ORDER, SID, etc. |
|
||||
| INV-002 | Product catalog | Reagents, consumables |
|
||||
| INV-003 | Reagent tracking | Lot numbers, expiration |
|
||||
| INV-004 | Consumables usage | Usage logging |
|
||||
| INV-005 | Billing export | Tariff-based billing |
|
||||
| INV-006 | Service class selection | Billing tiers |
|
||||
|
||||
---
|
||||
|
||||
## 5. Non-Functional Requirements
|
||||
|
||||
### 5.1 Security
|
||||
| Requirement | Description |
|
||||
|-------------|-------------|
|
||||
| AUTH-001 | JWT-based authentication for all API endpoints |
|
||||
| AUTH-002 | Role-based access control (RBAC) |
|
||||
| AUTH-003 | Password hashing with bcrypt |
|
||||
| AUTH-004 | Login attempt tracking |
|
||||
| AUTH-005 | Device registration (userdevices) |
|
||||
|
||||
### 5.2 Data Integrity
|
||||
| Requirement | Description |
|
||||
|-------------|-------------|
|
||||
| DATA-001 | Soft delete (DelDate) on all transactional tables |
|
||||
| DATA-002 | UTC timezone normalization for all datetime fields |
|
||||
| DATA-003 | Automatic date conversion (UTC ↔ ISO 8601) |
|
||||
| DATA-004 | Referential integrity via foreign keys |
|
||||
| DATA-005 | Transactional data consistency |
|
||||
|
||||
### 5.3 Performance
|
||||
| Requirement | Description | Target |
|
||||
|-------------|-------------|--------|
|
||||
| PERF-001 | Standard query response time | < 2 seconds |
|
||||
| PERF-002 | API endpoint latency | < 500ms (p95) |
|
||||
| PERF-003 | Lookup cache hit rate | > 95% |
|
||||
| PERF-004 | Database indexing | All foreign keys indexed |
|
||||
|
||||
### 5.4 Reliability
|
||||
| Requirement | Description |
|
||||
|-------------|-------------|
|
||||
| REL-001 | Graceful error handling |
|
||||
| REL-002 | Database transaction support |
|
||||
| REL-003 | API rate limiting |
|
||||
| REL-004 | Request validation |
|
||||
|
||||
### 5.5 Maintainability
|
||||
| Requirement | Description |
|
||||
|-------------|-------------|
|
||||
| MAIN-001 | PSR-12 coding standards |
|
||||
| MAIN-002 | RESTful API design |
|
||||
| MAIN-003 | Comprehensive documentation |
|
||||
| MAIN-004 | Unit test coverage > 80% |
|
||||
| MAIN-005 | Migration-based schema management |
|
||||
|
||||
---
|
||||
|
||||
## 6. API Workflow Examples
|
||||
|
||||
### 6.1 Core Laboratory API Workflow
|
||||
|
||||
```
|
||||
POST /api/patient → Create patient
|
||||
POST /api/ordertest → Create order with tests
|
||||
POST /api/specimen → Create specimen from order
|
||||
PATCH /api/specimen/status → Update specimen status
|
||||
POST /api/patresult → Enter results
|
||||
PATCH /api/patresult/status → Verify results (VER → REV → REP)
|
||||
GET /api/patresult/:id → Retrieve final report
|
||||
```
|
||||
|
||||
### 6.2 Order Creation API Sequence
|
||||
|
||||
```bash
|
||||
# 1. Create/Register Patient
|
||||
POST /api/patient
|
||||
{
|
||||
"NameFirst": "John",
|
||||
"NameLast": "Doe",
|
||||
"Sex": "1",
|
||||
"Birthdate": "1990-05-15"
|
||||
}
|
||||
# Response: { "InternalPID": 123, "PatientID": "PT001" }
|
||||
|
||||
# 2. Create Order with Tests
|
||||
POST /api/ordertest
|
||||
{
|
||||
"InternalPID": 123,
|
||||
"Priority": "R",
|
||||
"OrderingProvider": "Dr. Smith",
|
||||
"Tests": [{ "TestID": 10 }, { "TestID": 15 }]
|
||||
}
|
||||
# Response: { "OrderID": "00250112000001", "OrderStatus": "ORD" }
|
||||
|
||||
# 3. Create Specimens (if applicable)
|
||||
POST /api/specimen
|
||||
{
|
||||
"OrderID": "00250112000001",
|
||||
"SpecimenType": "BLD",
|
||||
"SpecimenStatus": "COLLECTED"
|
||||
}
|
||||
# Response: { "SID": "00250112000001BLDA" }
|
||||
```
|
||||
|
||||
### 6.3 Result Entry & Verification API Sequence
|
||||
|
||||
```bash
|
||||
# 1. Enter Results
|
||||
POST /api/patresult
|
||||
{
|
||||
"OrderID": "00250112000001",
|
||||
"TestID": 10,
|
||||
"ResultValue": "5.2",
|
||||
"ResultUnit": "g/dL",
|
||||
"ResultStatus": "ENTRY"
|
||||
}
|
||||
|
||||
# 2. Technical Verification
|
||||
PATCH /api/patresult/status
|
||||
{
|
||||
"ResultID": 456,
|
||||
"ResultStatus": "VER",
|
||||
"VerifierID": 7
|
||||
}
|
||||
|
||||
# 3. Clinical Review
|
||||
PATCH /api/patresult/status
|
||||
{
|
||||
"ResultID": 456,
|
||||
"ResultStatus": "REV",
|
||||
"VerifierID": 8
|
||||
}
|
||||
|
||||
# 4. Report Finalization
|
||||
PATCH /api/patresult/status
|
||||
{
|
||||
"ResultID": 456,
|
||||
"ResultStatus": "REP"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Instrument Integration API Workflow
|
||||
|
||||
```bash
|
||||
# 1. Fetch Pending Orders for Instrument
|
||||
GET /api/edge/orders?instrument=coulter_counter
|
||||
|
||||
# 2. Instrument Acknowledges Order
|
||||
POST /api/edge/orders/123/ack
|
||||
|
||||
# 3. Instrument Sends Results
|
||||
POST /api/edge/results
|
||||
{
|
||||
"SampleID": "00250112000001",
|
||||
"Results": [
|
||||
{ "TestCode": "HGB", "Value": "14.5", "Unit": "g/dL" },
|
||||
{ "TestCode": "WBC", "Value": "7.2", "Unit": "x10^9/L" }
|
||||
]
|
||||
}
|
||||
|
||||
# 4. System Processes Results (automatic or manual)
|
||||
# Results stored in edgeres → processed to patresult
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Technical Architecture
|
||||
|
||||
### 7.1 Technology Stack
|
||||
|
||||
| Layer | Technology | Version |
|
||||
|-------|-----------|---------|
|
||||
| Backend | PHP | 8.1+ |
|
||||
| Framework | CodeIgniter | 4.x |
|
||||
| Database | MySQL | 8.0+ |
|
||||
| Authentication | JWT (firebase/php-jwt) | Latest |
|
||||
| Testing | PHPUnit | 10.5+ |
|
||||
| API Format | JSON | RESTful |
|
||||
|
||||
### 7.2 Architecture Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ API Consumers │
|
||||
│ (Web Apps, Mobile Apps, Desktop Clients, Instruments) │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│ HTTP/HTTPS (JSON)
|
||||
┌────────────────────┴────────────────────────────────────┐
|
||||
│ REST API Layer │
|
||||
│ (Controllers: Patient, Order, Specimen, Result, etc.) │
|
||||
│ - JWT Authentication Filter │
|
||||
│ - Request Validation │
|
||||
│ - Response Formatting │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────┴────────────────────────────────────┐
|
||||
│ Business Logic Layer │
|
||||
│ (Models + Libraries + Services) │
|
||||
│ - ValueSet Library (JSON-based lookups) │
|
||||
│ - Base Model (UTC normalization) │
|
||||
│ - Edge Processing Service │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────┴────────────────────────────────────┐
|
||||
│ Data Access Layer │
|
||||
│ (CodeIgniter Query Builder) │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────┴────────────────────────────────────┐
|
||||
│ MySQL Database │
|
||||
│ (Migration-managed schema) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.3 API Design Principles
|
||||
|
||||
| Principle | Implementation |
|
||||
|-----------|----------------|
|
||||
| **Resource-Based** | URLs represent resources (patients, orders, specimens) |
|
||||
| **HTTP Methods** | GET (read), POST (create), PATCH (update), DELETE (soft delete) |
|
||||
| **Stateless** | Each request contains all necessary context (JWT token) |
|
||||
| **JSON Responses** | All API responses use consistent JSON format |
|
||||
| **Versioning** | API versioning via URL prefix (/api/v1/, /api/v2/) |
|
||||
| **Error Handling** | Standardized error responses with status codes |
|
||||
| **Pagination** | List endpoints support pagination parameters |
|
||||
|
||||
### 7.3 Database Schema
|
||||
|
||||
**Core Transaction Tables:**
|
||||
| Table | Purpose | Key Fields |
|
||||
|-------|---------|------------|
|
||||
| patient | Patient registry | InternalPID, PatientID, NameFirst, NameLast, Sex, Birthdate |
|
||||
| porder | Laboratory orders | OrderID, InternalPID, OrderStatus, Priority |
|
||||
| orderitem | Order tests | OrderID, TestID |
|
||||
| specimen | Specimens | SID, SpecimenID, SpecimenStatus |
|
||||
| patresult | Patient results | ResultID, OrderID, TestID, ResultValue |
|
||||
| patresultdetail | Result details | ResultID, ParameterID, Value |
|
||||
|
||||
**Master Data Tables:**
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| valueset | Static lookup values |
|
||||
| valuesetdef | Value set definitions |
|
||||
| testdefsite | Test definitions by site |
|
||||
| testdeftech | Technical test specifications |
|
||||
| testdefcal | Calculated test formulas |
|
||||
| testdefgrp | Test groups/panels |
|
||||
| refnum | Numeric reference ranges |
|
||||
| reftxt | Text reference ranges |
|
||||
|
||||
**Integration Tables:**
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| edgeres | Raw instrument results |
|
||||
| edgestatus | Instrument status |
|
||||
| edgeack | Order acknowledgment |
|
||||
|
||||
### 7.4 API Specifications
|
||||
|
||||
**Authentication Endpoint:**
|
||||
```
|
||||
POST /api/login
|
||||
Request: { "username": "...", "password": "..." }
|
||||
Response: { "token": "jwt-token", "expires": 3600 }
|
||||
```
|
||||
|
||||
**Demo Order Endpoint (No Auth):**
|
||||
```
|
||||
POST /api/demo/order
|
||||
Request: {
|
||||
"PatientID": "PT001",
|
||||
"NameFirst": "John",
|
||||
"NameLast": "Doe",
|
||||
"Sex": "1",
|
||||
"Birthdate": "1990-05-15",
|
||||
"Priority": "R",
|
||||
"OrderingProvider": "Dr. Smith"
|
||||
}
|
||||
Response: {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"PatientID": "DEMO1736689600",
|
||||
"InternalPID": 1,
|
||||
"OrderID": "00250112000001",
|
||||
"OrderStatus": "ORD"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Order Endpoints:**
|
||||
```
|
||||
GET /api/ordertest - List orders
|
||||
POST /api/ordertest - Create order
|
||||
GET /api/ordertest/:id - Get order
|
||||
PUT /api/ordertest/:id - Update order
|
||||
DELETE /api/ordertest/:id - Delete order
|
||||
POST /api/ordertest/status - Update status
|
||||
```
|
||||
|
||||
**Edge API Endpoints:**
|
||||
```
|
||||
POST /api/edge/results - Receive results
|
||||
GET /api/edge/orders - Fetch pending orders
|
||||
POST /api/edge/orders/:id/ack - Acknowledge order
|
||||
POST /api/edge/status - Log status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Roadmap
|
||||
|
||||
### Phase 0: Master Data Completion (P0)
|
||||
> **Prerequisite:** All master data must be complete before transactional workflows
|
||||
|
||||
| Task | Effort | Dependencies |
|
||||
|------|--------|--------------|
|
||||
| Complete reference range migrations | 2 days | None |
|
||||
| Create reference range CRUD controllers | 3 days | Migrations |
|
||||
| Verify all value sets populated | 1 day | None |
|
||||
| Complete test definitions data entry | 2 days | None |
|
||||
| **Total** | **8 days** | - |
|
||||
|
||||
### Phase 1: Core Lab Workflow (P0)
|
||||
| Task | Effort | Dependencies |
|
||||
|------|--------|--------------|
|
||||
| Order CRUD complete | 5 days | Phase 0 |
|
||||
| Order ID generation | 2 days | Order CRUD |
|
||||
| Order status tracking | 2 days | Order CRUD |
|
||||
| Specimen API complete | 5 days | Phase 0 |
|
||||
| Specimen status workflow | 3 days | Specimen API |
|
||||
| Result CRUD | 5 days | Phase 0 |
|
||||
| Result entry with validation | 4 days | Result CRUD |
|
||||
| Result verification workflow | 4 days | Result CRUD |
|
||||
| Result report generation | 3 days | Result verification |
|
||||
| **Total** | **33 days** | Phase 0 |
|
||||
|
||||
### Phase 2: Instrument Integration (P1)
|
||||
| Task | Effort | Dependencies |
|
||||
|------|--------|--------------|
|
||||
| Edge API results endpoint | 4 days | Phase 1 |
|
||||
| Edge API orders endpoint | 3 days | Phase 1 |
|
||||
| Test mapping management | 3 days | Phase 0 |
|
||||
| Order acknowledgment | 2 days | Edge orders |
|
||||
| Status logging | 2 days | Edge results |
|
||||
| **Total** | **14 days** | Phase 1 |
|
||||
|
||||
### Phase 3: Quality Management (P2)
|
||||
| Task | Effort | Dependencies |
|
||||
|------|--------|--------------|
|
||||
| QC result entry | 4 days | Phase 2 |
|
||||
| QC validation rules | 3 days | QC entry |
|
||||
| Calibration tracking | 3 days | None |
|
||||
| Audit trail implementation | 5 days | Phase 1 |
|
||||
| **Total** | **15 days** | Phase 2 |
|
||||
|
||||
### Phase 4: Additional Features (P3)
|
||||
| Task | Effort | Dependencies |
|
||||
|------|--------|--------------|
|
||||
| Inventory management | 8 days | None |
|
||||
| Billing integration | 6 days | Phase 1 |
|
||||
| Advanced reporting | 5 days | Phase 1 |
|
||||
| **Total** | **19 days** | Phase 1 |
|
||||
|
||||
**Total MVP Timeline:** ~89 days (~4 months)
|
||||
- Phase 0: 2 weeks
|
||||
- Phase 1: 7 weeks
|
||||
- Phase 2: 3 weeks
|
||||
- Phase 3: 3 weeks
|
||||
- Phase 4: 4 weeks
|
||||
|
||||
---
|
||||
|
||||
## 9. Success Criteria
|
||||
|
||||
### 9.1 API Endpoint Acceptance
|
||||
|
||||
A REST API endpoint is considered complete when:
|
||||
- [ ] HTTP methods implemented (GET, POST, PATCH, DELETE)
|
||||
- [ ] Request validation implemented (input sanitization, type checking)
|
||||
- [ ] JSON response format consistent with API standards
|
||||
- [ ] Error responses include status code and error message
|
||||
- [ ] JWT authentication/authorization configured
|
||||
- [ ] Soft delete implemented (where applicable)
|
||||
- [ ] UTC date normalization working
|
||||
- [ ] Unit tests written (PHPUnit)
|
||||
- [ ] API documented (request/response examples)
|
||||
- [ ] Pagination support (list endpoints)
|
||||
- [ ] Filtering/sorting support (where applicable)
|
||||
|
||||
### 9.2 MVP Definition of Done
|
||||
|
||||
The MVP is considered complete when:
|
||||
1. ✅ Patient can be registered and retrieved
|
||||
2. ✅ Order can be created with valid OrderID
|
||||
3. ✅ Specimen can be tracked through all statuses
|
||||
4. ✅ Results can be entered with range validation
|
||||
5. ✅ Results can be verified (VER → REV → REP)
|
||||
6. ✅ Instruments can send results via Edge API
|
||||
7. ✅ All master data is manageable
|
||||
8. ✅ JWT authentication protects all endpoints
|
||||
9. ✅ Soft delete works on all transactions
|
||||
10. ✅ UTC timestamps are normalized
|
||||
|
||||
### 9.3 Performance Benchmarks
|
||||
|
||||
| Metric | Target | Measurement |
|
||||
|--------|--------|-------------|
|
||||
| API response time (p95) | < 500ms | Apache Bench |
|
||||
| Database query time | < 200ms | EXPLAIN analysis |
|
||||
| Lookup cache hit rate | > 95% | Application metrics |
|
||||
| Test coverage | > 80% | PHPUnit --coverage |
|
||||
|
||||
---
|
||||
|
||||
## 10. Open Questions & Risks
|
||||
|
||||
### 10.1 Open Questions
|
||||
| Question | Impact | Target Date |
|
||||
|----------|--------|-------------|
|
||||
| Reference range types (refthold, refvset) - are they needed for MVP? | Medium | Phase 0 |
|
||||
| Multi-site deployment requirements? | High | Phase 0 |
|
||||
| Specific instrument integrations needed? | High | Phase 2 |
|
||||
| Report format requirements (PDF/HTML)? | Medium | Phase 1 |
|
||||
| HL7/FHIR integration requirements? | Low | Phase 4 |
|
||||
|
||||
### 10.2 Technical Risks
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| Edge API protocol mismatches | Medium | High | Early instrument testing |
|
||||
| Reference range complexity | Low | Medium | Start with refnum only |
|
||||
| Performance at scale | Low | Medium | Database indexing, caching |
|
||||
| Test definition data entry | High | Medium | Provide templates/bulk import |
|
||||
|
||||
### 10.3 Business Risks
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| Scope creep | High | High | Strict MVP definition |
|
||||
| Resource constraints | Medium | High | Phased delivery |
|
||||
| Instrument compatibility | Medium | High | Edge API abstraction layer |
|
||||
|
||||
---
|
||||
|
||||
## 11. Appendix
|
||||
|
||||
### 11.1 Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **OrderID** | Laboratory order identifier: LLYYMMDDXXXXX (Lab+Date+Sequence) |
|
||||
| **SID** | Specimen identifier: OrderID + SpecimenType + Container |
|
||||
| **VSetID** | Value set definition ID |
|
||||
| **VID** | Value set value ID |
|
||||
| **Edge API** | Standardized interface for laboratory instrument integration |
|
||||
| **ADT** | Admission, Discharge, Transfer |
|
||||
| **QC** | Quality Control |
|
||||
| **LLYYMMDD** | Lab location (LL) + Year (YY) + Month (MM) + Day (DD) |
|
||||
| **UTC** | Coordinated Universal Time |
|
||||
|
||||
### 11.2 Reference Documents
|
||||
|
||||
| Document | Location |
|
||||
|----------|----------|
|
||||
| Technical Architecture | `CLAUDE.md` |
|
||||
| Implementation Tasks | `TODO.md` |
|
||||
| API Documentation | `README.md` |
|
||||
| Database Migrations | `app/Database/Migrations/` |
|
||||
|
||||
### 11.3 Revision History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-01-28 | Claude | Initial PRD from TODO.md |
|
||||
|
||||
---
|
||||
|
||||
**Document Status:** Draft for Review
|
||||
**Next Review:** Upon Phase 0 completion
|
||||
**Approvals:** Pending
|
||||
235
README.md
235
README.md
@ -1,14 +1,40 @@
|
||||
# CLQMS (Clinical Laboratory Quality Management System)
|
||||
|
||||
> **The core backend engine for modern clinical laboratory workflows.**
|
||||
> **A REST API backend for modern clinical laboratory workflows.**
|
||||
|
||||
CLQMS is a robust, mission-critical API suite designed to streamline laboratory operations, ensure data integrity, and manage complex diagnostic workflows. Built on a foundation of precision and regulatory compliance, this system handles everything from patient registration to high-throughput test resulting.
|
||||
---
|
||||
|
||||
## 🤖 For Claude Code
|
||||
|
||||
**When working in this repository, prioritize Serena MCP tools for code operations:**
|
||||
|
||||
- **Use `find_symbol` / `get_symbols_overview`** instead of `Read` for exploring code structure
|
||||
- **Use `replace_symbol_body`** instead of `Edit` for modifying functions, methods, classes
|
||||
- **Use `insert_before_symbol` / `insert_after_symbol`** for adding new code symbols
|
||||
- **Use `search_for_pattern`** instead of `Grep` for searching code patterns
|
||||
- **Use `list_dir` / `find_file`** instead of `Glob` for file discovery
|
||||
|
||||
This minimizes tool calls and leverages semantic code understanding for this PHP codebase.
|
||||
|
||||
---
|
||||
|
||||
CLQMS is a **headless REST API backend** designed to streamline laboratory operations, ensure data integrity, and manage complex diagnostic workflows. Built on a foundation of precision and regulatory compliance, this system provides comprehensive JSON endpoints for laboratory operations.
|
||||
|
||||
**Key Characteristic:** This is an **API-only system** with no view layer. Frontend applications (web, mobile, desktop) consume these REST endpoints to build laboratory information systems.
|
||||
|
||||
---
|
||||
|
||||
## 🏛️ Core Architecture & Design
|
||||
|
||||
The system is currently undergoing a strategic **Architectural Redesign** to consolidate legacy structures into a high-performance, maintainable schema. This design, spearheaded by leadership, focuses on reducing technical debt and improving data consistency across:
|
||||
CLQMS is a **headless REST API system** following a clean architecture pattern. The system is designed to be consumed by any frontend client (web, mobile, desktop) through comprehensive JSON endpoints.
|
||||
|
||||
**API-First Architecture:**
|
||||
- **No View Layer:** This system provides REST APIs only - no HTML views, no server-side rendering
|
||||
- **Frontend Agnostic:** Any client can consume these APIs (React, Vue, Angular, mobile apps, desktop apps)
|
||||
- **JSON-First:** All requests/responses use JSON format
|
||||
- **Stateless:** Each API request is independent with JWT authentication
|
||||
|
||||
The system is currently undergoing a strategic **Architectural Redesign** to consolidate legacy structures into a high-performance, maintainable schema. This design focuses on reducing technical debt and improving data consistency across:
|
||||
|
||||
- **Unified Test Definitions:** Consolidating technical, calculated, and site-specific test data.
|
||||
- **Reference Range Centralization:** A unified engine for numeric, threshold, text, and coded results.
|
||||
@ -30,23 +56,177 @@ The system is currently undergoing a strategic **Architectural Redesign** to con
|
||||
| Component | Specification |
|
||||
| :------------- | :------------ |
|
||||
| **Language** | PHP 8.1+ (PSR-compliant) |
|
||||
| **Framework** | CodeIgniter 4 |
|
||||
| **Framework** | CodeIgniter 4 (API-only mode) |
|
||||
| **Security** | JWT (JSON Web Tokens) Authorization |
|
||||
| **Database** | MySQL (Optimized Schema Migration in progress) |
|
||||
| **API Format** | RESTful JSON |
|
||||
| **Testing** | PHPUnit 10.5+ |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📂 Documentation & Specifications
|
||||
|
||||
For detailed architectural blueprints and API specifications, please refer to the internal documentation:
|
||||
### Key Documents
|
||||
|
||||
👉 **[Internal Documentation Index](./docs/README.md)**
|
||||
| Document | Location | Description |
|
||||
|----------|----------|-------------|
|
||||
| **PRD** | `PRD.md` | Complete Product Requirements Document (API-focused) |
|
||||
| **Technical Guide** | `CLAUDE.md` | Architecture, coding standards, common commands |
|
||||
| **API Overview** | This file | REST API documentation and endpoints |
|
||||
| **Database Migrations** | `app/Database/Migrations/` | Database schema history |
|
||||
|
||||
Key documents:
|
||||
- [Database Schema Redesign Proposal](./docs/20251216002-Test_OrderTest_RefRange_schema_redesign_proposal.md)
|
||||
- [API Contract: Patient Registration](./docs/api_contract_patient_registration.md)
|
||||
- [Database Design Review (Reference)](./docs/20251212001-database_design_review_sonnet.md)
|
||||
### API Documentation
|
||||
|
||||
All API endpoints follow REST conventions:
|
||||
|
||||
**Base URL:** `/api`
|
||||
|
||||
**Authentication:** JWT token required for most endpoints (except `/api/login`, `/api/demo/*`)
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"status": "success|error",
|
||||
"message": "Human-readable message",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 REST API Overview
|
||||
|
||||
### API Endpoint Categories
|
||||
|
||||
#### Authentication & Authorization
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `POST` | `/api/login` | User login, returns JWT token | No |
|
||||
| `POST` | `/api/logout` | Invalidate JWT token | Yes |
|
||||
| `POST` | `/api/refresh` | Refresh JWT token | Yes |
|
||||
|
||||
#### Patient Management
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `GET` | `/api/patient` | List patients with pagination | Yes |
|
||||
| `GET` | `/api/patient/{id}` | Get patient details | Yes |
|
||||
| `POST` | `/api/patient` | Create new patient | Yes |
|
||||
| `PATCH` | `/api/patient/{id}` | Update patient | Yes |
|
||||
| `DELETE` | `/api/patient/{id}` | Soft delete patient | Yes |
|
||||
|
||||
#### Order Management
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `GET` | `/api/ordertest` | List orders | Yes |
|
||||
| `GET` | `/api/ordertest/{id}` | Get order details | Yes |
|
||||
| `POST` | `/api/ordertest` | Create order | Yes |
|
||||
| `PATCH` | `/api/ordertest/{id}` | Update order | Yes |
|
||||
| `DELETE` | `/api/ordertest/{id}` | Delete order | Yes |
|
||||
| `POST` | `/api/ordertest/status` | Update order status | Yes |
|
||||
|
||||
#### Demo/Test Endpoints (No Auth)
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `POST` | `/api/demo/order` | Create demo order with patient | No |
|
||||
|
||||
#### Specimen Management
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `GET` | `/api/specimen` | List specimens | Yes |
|
||||
| `GET` | `/api/specimen/{id}` | Get specimen details | Yes |
|
||||
| `POST` | `/api/specimen` | Create specimen | Yes |
|
||||
| `PATCH` | `/api/specimen/{id}` | Update specimen | Yes |
|
||||
| `POST` | `/api/specimen/status` | Update specimen status | Yes |
|
||||
|
||||
#### Result Management
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `GET` | `/api/patresult` | List patient results | Yes |
|
||||
| `GET` | `/api/patresult/{id}` | Get result details | Yes |
|
||||
| `POST` | `/api/patresult` | Enter new result | Yes |
|
||||
| `PATCH` | `/api/patresult/{id}` | Update result | Yes |
|
||||
| `POST` | `/api/patresult/status` | Verify result (VER/REV/REP) | Yes |
|
||||
|
||||
#### Edge API (Instrument Integration)
|
||||
|
||||
| Method | Endpoint | Description | Auth Required |
|
||||
|--------|----------|-------------|---------------|
|
||||
| `POST` | `/api/edge/results` | Receive instrument results | API Key |
|
||||
| `GET` | `/api/edge/orders` | Fetch pending orders | API Key |
|
||||
| `POST` | `/api/edge/orders/{id}/ack` | Acknowledge order | API Key |
|
||||
| `POST` | `/api/edge/status` | Log instrument status | API Key |
|
||||
|
||||
### API Response Format
|
||||
|
||||
All API endpoints return JSON in this format:
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Operation completed successfully",
|
||||
"data": {
|
||||
// Response data here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Error description",
|
||||
"errors": [
|
||||
{
|
||||
"field": "field_name",
|
||||
"message": "Validation error message"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
Most endpoints require JWT authentication:
|
||||
|
||||
**Request Headers:**
|
||||
```
|
||||
Authorization: Bearer {jwt_token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Login Request Example:**
|
||||
```bash
|
||||
POST /api/login
|
||||
{
|
||||
"username": "labuser",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
**Login Response:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Login successful",
|
||||
"data": {
|
||||
"token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"expires_in": 3600,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "labuser",
|
||||
"name": "Lab User"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -151,32 +331,12 @@ $labeled = Lookups::transformLabels($patients, [
|
||||
Lookups::clearCache();
|
||||
```
|
||||
|
||||
### Frontend Usage (Alpine.js)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// In your PHP view file
|
||||
use App\Libraries\Lookups;
|
||||
|
||||
$allLookups = Lookups::getAll();
|
||||
?>
|
||||
<script>
|
||||
const LOOKUPS = <?= json_encode($allLookups) ?>;
|
||||
console.log(LOOKUPS.gender);
|
||||
// Output: {"values":[{"key":"1","value":"Female"},{"key":"2","value":"Male"},...]}
|
||||
|
||||
// Convenience accessors
|
||||
const genderValues = LOOKUPS.gender.values;
|
||||
const genderDropdown = LOOKUPS.gender.values.map(v => ({value: v.key, label: v.value}));
|
||||
</script>
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
| Approach | Use Case |
|
||||
|----------|----------|
|
||||
| **Lookups Library** | Static values that rarely change (gender, status, types) - fast, cached |
|
||||
| **API `/api/valueset*`** | Dynamic values managed by admins at runtime |
|
||||
| **Lookups Library** | Server-side static values that rarely change (gender, status, types) - fast, cached |
|
||||
| **API `/api/valueset*`** | Dynamic values managed by admins at runtime, or for frontend clients needing lookup data |
|
||||
|
||||
### Adding New Lookups
|
||||
|
||||
@ -199,9 +359,9 @@ const genderDropdown = LOOKUPS.gender.values.map(v => ({value: v.key, label: v.v
|
||||
|
||||
## 📋 Master Data Management
|
||||
|
||||
CLQMS provides comprehensive master data management for laboratory operations. All master data is accessible via the V2 UI at `/v2/master/*` endpoints.
|
||||
CLQMS provides comprehensive master data management for laboratory operations. All master data is accessible via REST API endpoints.
|
||||
|
||||
### 🧪 Laboratory Tests (`/v2/master/tests`)
|
||||
### 🧪 Laboratory Tests
|
||||
|
||||
The Test Definitions module manages all laboratory test configurations including parameters, calculated tests, and test panels.
|
||||
|
||||
@ -254,7 +414,7 @@ The Test Definitions module manages all laboratory test configurations including
|
||||
}
|
||||
```
|
||||
|
||||
### 📏 Reference Ranges (`/v2/master/refrange`)
|
||||
### 📏 Reference Ranges
|
||||
|
||||
Reference Ranges define normal and critical values for test results. The system supports multiple reference range types based on patient demographics.
|
||||
|
||||
@ -292,7 +452,7 @@ Reference Ranges define normal and critical values for test results. The system
|
||||
| `PATCH` | `/api/refnum` | Update reference range |
|
||||
| `DELETE` | `/api/refnum` | Soft delete reference range |
|
||||
|
||||
### 📑 Value Sets (`/v2/master/valuesets`)
|
||||
### 📑 Value Sets
|
||||
|
||||
Value Sets are configurable dropdown options used throughout the system. Each Value Set Definition (VSetDef) contains multiple Value Set Values (ValueSet).
|
||||
|
||||
@ -386,6 +546,9 @@ Instrument → tiny-edge → POST /api/edge/results → edgeres table → [Manua
|
||||
---
|
||||
|
||||
### 📜 Usage Notice
|
||||
|
||||
**This is an API-only backend system.** There are no views, HTML templates, or server-side rendering components. Frontend applications should consume these REST endpoints to build user interfaces for laboratory operations.
|
||||
|
||||
This repository contains proprietary information intended for the 5Panda Team and authorized collaborators.
|
||||
|
||||
---
|
||||
|
||||
1607
USER_STORIES.md
Normal file
1607
USER_STORIES.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,9 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) {
|
||||
// Public Routes (no auth required)
|
||||
$routes->get('/v2/login', 'PagesController::login');
|
||||
|
||||
// Swagger API Documentation (public - no filters)
|
||||
$routes->add('swagger', 'PagesController::swagger');
|
||||
|
||||
// V2 Auth API Routes (public - no auth required)
|
||||
$routes->group('v2/auth', function ($routes) {
|
||||
$routes->post('login', 'AuthV2Controller::login');
|
||||
|
||||
@ -16,6 +16,8 @@ class AuthController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
|
||||
// ok
|
||||
public function __construct()
|
||||
{
|
||||
@ -170,19 +172,22 @@ class AuthController extends Controller
|
||||
try {
|
||||
// Melakukan Hash terhadap Payload dengan Kunci .env menggunakan Algortima HMAC + SHA-256
|
||||
$jwt = JWT::encode($payload, $key, 'HS256');
|
||||
} catch (Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail('Error generating JWT: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
// Detect if HTTPS is being used
|
||||
$isSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
|
||||
// Kirim Respon ke HttpOnly yg akan disimpan di browser dan tidak akan dapat diakses oleh siapapun
|
||||
$this->response->setCookie([
|
||||
'name' => 'token', // nama token
|
||||
'value' => $jwt, // value dari jwt yg sudah di hash
|
||||
'expire' => 864000, // 10 hari
|
||||
'path' => '/', // valid untuk semua path
|
||||
'secure' => true, // set true kalau sudah HTTPS
|
||||
'secure' => $isSecure,
|
||||
'httponly' => true, // dipakai agar cookie berikut tidak dapat diakses oleh javascript
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
]);
|
||||
|
||||
// Response tanpa token di body
|
||||
@ -214,15 +219,18 @@ class AuthController extends Controller
|
||||
// }
|
||||
public function logout()
|
||||
{
|
||||
// Detect if HTTPS is being used
|
||||
$isSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
|
||||
// Definisikan ini pada cookies browser, harus sama dengan cookies login
|
||||
return $this->response->setCookie([
|
||||
'name' => 'token',
|
||||
'value' => '',
|
||||
'expire' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'secure' => $isSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
|
||||
])->setJSON([
|
||||
'status' => 'success',
|
||||
|
||||
@ -3,7 +3,7 @@ namespace App\Controllers\Contact;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Contact\ContactModel;
|
||||
|
||||
class ContactController extends BaseController {
|
||||
@ -23,11 +23,25 @@ class ContactController extends BaseController {
|
||||
$ContactName = $this->request->getVar('ContactName');
|
||||
$Specialty = $this->request->getVar('Specialty');
|
||||
$rows = $this->model->getContacts($ContactName, $Specialty);
|
||||
|
||||
|
||||
if (empty($rows)) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200);
|
||||
}
|
||||
|
||||
// Transform Specialty and Occupation
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['Specialty'])) {
|
||||
$row['specialty'] = $row['Specialty'];
|
||||
$row['specialtyLabel'] = ValueSet::getLabel('specialty', $row['Specialty']) ?? '';
|
||||
unset($row['Specialty']);
|
||||
}
|
||||
if (isset($row['Occupation'])) {
|
||||
$row['occupation'] = $row['Occupation'];
|
||||
$row['occupationLabel'] = ValueSet::getLabel('occupation', $row['Occupation']) ?? '';
|
||||
unset($row['Occupation']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $rows ], 200);
|
||||
}
|
||||
|
||||
@ -39,6 +53,18 @@ class ContactController extends BaseController {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
// Transform Specialty and Occupation
|
||||
if (isset($row['Specialty'])) {
|
||||
$row['specialty'] = $row['Specialty'];
|
||||
$row['specialtyLabel'] = ValueSet::getLabel('specialty', $row['Specialty']) ?? '';
|
||||
unset($row['Specialty']);
|
||||
}
|
||||
if (isset($row['Occupation'])) {
|
||||
$row['occupation'] = $row['Occupation'];
|
||||
$row['occupationLabel'] = ValueSet::getLabel('occupation', $row['Occupation']) ?? '';
|
||||
unset($row['Occupation']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "fetch success", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,10 @@ namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use App\Models\OrderTest\OrderTestModel;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\OrderTestModel;
|
||||
use App\Models\Patient\PatientModel;
|
||||
use App\Models\PatVisit\PatVisitModel;
|
||||
use App\Models\Patient\PatVisitModel;
|
||||
|
||||
class OrderTestController extends Controller {
|
||||
use ResponseTrait;
|
||||
@ -40,6 +41,20 @@ class OrderTestController extends Controller {
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
// Transform Priority and OrderStatus
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['Priority'])) {
|
||||
$row['priority'] = $row['Priority'];
|
||||
$row['priorityLabel'] = ValueSet::getLabel('priority', $row['Priority']) ?? '';
|
||||
unset($row['Priority']);
|
||||
}
|
||||
if (isset($row['OrderStatus'])) {
|
||||
$row['orderStatus'] = $row['OrderStatus'];
|
||||
$row['orderStatusLabel'] = ValueSet::getLabel('order_status', $row['OrderStatus']) ?? '';
|
||||
unset($row['OrderStatus']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Data fetched successfully',
|
||||
@ -60,6 +75,19 @@ class OrderTestController extends Controller {
|
||||
'data' => null
|
||||
], 200);
|
||||
}
|
||||
|
||||
// Transform Priority and OrderStatus
|
||||
if (isset($row['Priority'])) {
|
||||
$row['priority'] = $row['Priority'];
|
||||
$row['priorityLabel'] = ValueSet::getLabel('priority', $row['Priority']) ?? '';
|
||||
unset($row['Priority']);
|
||||
}
|
||||
if (isset($row['OrderStatus'])) {
|
||||
$row['orderStatus'] = $row['OrderStatus'];
|
||||
$row['orderStatusLabel'] = ValueSet::getLabel('order_status', $row['OrderStatus']) ?? '';
|
||||
unset($row['OrderStatus']);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Data fetched successfully',
|
||||
|
||||
@ -175,4 +175,12 @@ class PagesController extends BaseController
|
||||
'activePage' => ''
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* API Documentation / Swagger UI page
|
||||
*/
|
||||
public function swagger()
|
||||
{
|
||||
return view('swagger');
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ class PatientController extends Controller {
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
use App\Libraries\ValueSet;
|
||||
public function __construct() {
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->model = new PatientModel();
|
||||
@ -58,6 +59,16 @@ class PatientController extends Controller {
|
||||
|
||||
try {
|
||||
$rows = $this->model->getPatients($filters);
|
||||
|
||||
// Transform Sex to sex and sexLabel
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['Sex'])) {
|
||||
$row['sex'] = $row['Sex'];
|
||||
$row['sexLabel'] = ValueSet::getLabel('sex', $row['Sex']) ?? '';
|
||||
unset($row['Sex']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
@ -68,6 +79,14 @@ class PatientController extends Controller {
|
||||
try {
|
||||
$row = $this->model->getPatient($InternalPID);
|
||||
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "data not found.", 'data' => null ], 200); }
|
||||
|
||||
// Transform Sex to sex and sexLabel
|
||||
if (isset($row['Sex'])) {
|
||||
$row['sex'] = $row['Sex'];
|
||||
$row['sexLabel'] = ValueSet::getLabel('sex', $row['Sex']) ?? '';
|
||||
unset($row['Sex']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message' => "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Controllers\Specimen;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Specimen\ContainerDefModel;
|
||||
|
||||
class ContainerDefController extends BaseController {
|
||||
@ -29,6 +30,26 @@ class ContainerDefController extends BaseController {
|
||||
'ConName' => $this->request->getVar('ConName')
|
||||
];
|
||||
$rows = $this->model->getContainers($filter);
|
||||
|
||||
// Transform ConCategory, CapColor, ConSize
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['ConCategory'])) {
|
||||
$row['conCategory'] = $row['ConCategory'];
|
||||
$row['conCategoryLabel'] = ValueSet::getLabel('container_class', $row['ConCategory']) ?? '';
|
||||
unset($row['ConCategory']);
|
||||
}
|
||||
if (isset($row['CapColor'])) {
|
||||
$row['capColor'] = $row['CapColor'];
|
||||
$row['capColorLabel'] = ValueSet::getLabel('container_cap_color', $row['CapColor']) ?? '';
|
||||
unset($row['CapColor']);
|
||||
}
|
||||
if (isset($row['ConSize'])) {
|
||||
$row['conSize'] = $row['ConSize'];
|
||||
$row['conSizeLabel'] = ValueSet::getLabel('container_size', $row['ConSize']) ?? '';
|
||||
unset($row['ConSize']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
@ -41,6 +62,24 @@ class ContainerDefController extends BaseController {
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
// Transform ConCategory, CapColor, ConSize
|
||||
if (isset($row['ConCategory'])) {
|
||||
$row['conCategory'] = $row['ConCategory'];
|
||||
$row['conCategoryLabel'] = ValueSet::getLabel('container_class', $row['ConCategory']) ?? '';
|
||||
unset($row['ConCategory']);
|
||||
}
|
||||
if (isset($row['CapColor'])) {
|
||||
$row['capColor'] = $row['CapColor'];
|
||||
$row['capColorLabel'] = ValueSet::getLabel('container_cap_color', $row['CapColor']) ?? '';
|
||||
unset($row['CapColor']);
|
||||
}
|
||||
if (isset($row['ConSize'])) {
|
||||
$row['conSize'] = $row['ConSize'];
|
||||
$row['conSizeLabel'] = ValueSet::getLabel('container_size', $row['ConSize']) ?? '';
|
||||
unset($row['ConSize']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Controllers\Specimen;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Specimen\SpecimenCollectionModel;
|
||||
|
||||
class SpecimenCollectionController extends BaseController {
|
||||
@ -22,6 +23,26 @@ class SpecimenCollectionController extends BaseController {
|
||||
public function index() {
|
||||
try {
|
||||
$rows = $this->model->findAll();
|
||||
|
||||
// Transform CollectionMethod, Additive, SpecimenRole
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['CollectionMethod'])) {
|
||||
$row['collectionMethod'] = $row['CollectionMethod'];
|
||||
$row['collectionMethodLabel'] = ValueSet::getLabel('collection_method', $row['CollectionMethod']) ?? '';
|
||||
unset($row['CollectionMethod']);
|
||||
}
|
||||
if (isset($row['Additive'])) {
|
||||
$row['additive'] = $row['Additive'];
|
||||
$row['additiveLabel'] = ValueSet::getLabel('additive', $row['Additive']) ?? '';
|
||||
unset($row['Additive']);
|
||||
}
|
||||
if (isset($row['SpecimenRole'])) {
|
||||
$row['specimenRole'] = $row['SpecimenRole'];
|
||||
$row['specimenRoleLabel'] = ValueSet::getLabel('specimen_role', $row['SpecimenRole']) ?? '';
|
||||
unset($row['SpecimenRole']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
@ -34,6 +55,24 @@ class SpecimenCollectionController extends BaseController {
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
// Transform CollectionMethod, Additive, SpecimenRole
|
||||
if (isset($row['CollectionMethod'])) {
|
||||
$row['collectionMethod'] = $row['CollectionMethod'];
|
||||
$row['collectionMethodLabel'] = ValueSet::getLabel('collection_method', $row['CollectionMethod']) ?? '';
|
||||
unset($row['CollectionMethod']);
|
||||
}
|
||||
if (isset($row['Additive'])) {
|
||||
$row['additive'] = $row['Additive'];
|
||||
$row['additiveLabel'] = ValueSet::getLabel('additive', $row['Additive']) ?? '';
|
||||
unset($row['Additive']);
|
||||
}
|
||||
if (isset($row['SpecimenRole'])) {
|
||||
$row['specimenRole'] = $row['SpecimenRole'];
|
||||
$row['specimenRoleLabel'] = ValueSet::getLabel('specimen_role', $row['SpecimenRole']) ?? '';
|
||||
unset($row['SpecimenRole']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Controllers\Specimen;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Specimen\SpecimenModel;
|
||||
|
||||
class SpecimenController extends BaseController {
|
||||
@ -22,6 +23,26 @@ class SpecimenController extends BaseController {
|
||||
public function index() {
|
||||
try {
|
||||
$rows = $this->model->findAll();
|
||||
|
||||
// Transform SpecimenType, SpecimenStatus, BodySite
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['SpecimenType'])) {
|
||||
$row['specimenType'] = $row['SpecimenType'];
|
||||
$row['specimenTypeLabel'] = ValueSet::getLabel('specimen_type', $row['SpecimenType']) ?? '';
|
||||
unset($row['SpecimenType']);
|
||||
}
|
||||
if (isset($row['SpecimenStatus'])) {
|
||||
$row['specimenStatus'] = $row['SpecimenStatus'];
|
||||
$row['specimenStatusLabel'] = ValueSet::getLabel('specimen_status', $row['SpecimenStatus']) ?? '';
|
||||
unset($row['SpecimenStatus']);
|
||||
}
|
||||
if (isset($row['BodySite'])) {
|
||||
$row['bodySite'] = $row['BodySite'];
|
||||
$row['bodySiteLabel'] = ValueSet::getLabel('body_site', $row['BodySite']) ?? '';
|
||||
unset($row['BodySite']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
@ -34,6 +55,24 @@ class SpecimenController extends BaseController {
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
// Transform SpecimenType, SpecimenStatus, BodySite
|
||||
if (isset($row['SpecimenType'])) {
|
||||
$row['specimenType'] = $row['SpecimenType'];
|
||||
$row['specimenTypeLabel'] = ValueSet::getLabel('specimen_type', $row['SpecimenType']) ?? '';
|
||||
unset($row['SpecimenType']);
|
||||
}
|
||||
if (isset($row['SpecimenStatus'])) {
|
||||
$row['specimenStatus'] = $row['SpecimenStatus'];
|
||||
$row['specimenStatusLabel'] = ValueSet::getLabel('specimen_status', $row['SpecimenStatus']) ?? '';
|
||||
unset($row['SpecimenStatus']);
|
||||
}
|
||||
if (isset($row['BodySite'])) {
|
||||
$row['bodySite'] = $row['BodySite'];
|
||||
$row['bodySiteLabel'] = ValueSet::getLabel('body_site', $row['BodySite']) ?? '';
|
||||
unset($row['BodySite']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Controllers\Specimen;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Specimen\SpecimenStatusModel;
|
||||
|
||||
class ContainerDef extends BaseController {
|
||||
@ -22,6 +23,21 @@ class ContainerDef extends BaseController {
|
||||
public function index() {
|
||||
try {
|
||||
$rows = $this->model->findAll();
|
||||
|
||||
// Transform Status and Activity
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['Status'])) {
|
||||
$row['status'] = $row['Status'];
|
||||
$row['statusLabel'] = ValueSet::getLabel('specimen_status', $row['Status']) ?? '';
|
||||
unset($row['Status']);
|
||||
}
|
||||
if (isset($row['Activity'])) {
|
||||
$row['activity'] = $row['Activity'];
|
||||
$row['activityLabel'] = ValueSet::getLabel('specimen_activity', $row['Activity']) ?? '';
|
||||
unset($row['Activity']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $rows ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
@ -34,6 +50,19 @@ class ContainerDef extends BaseController {
|
||||
if (empty($row)) {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
// Transform Status and Activity
|
||||
if (isset($row['Status'])) {
|
||||
$row['status'] = $row['Status'];
|
||||
$row['statusLabel'] = ValueSet::getLabel('specimen_status', $row['Status']) ?? '';
|
||||
unset($row['Status']);
|
||||
}
|
||||
if (isset($row['Activity'])) {
|
||||
$row['activity'] = $row['Activity'];
|
||||
$row['activityLabel'] = ValueSet::getLabel('specimen_activity', $row['Activity']) ?? '';
|
||||
unset($row['Activity']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "data fetched successfully", 'data' => $row ], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception : '.$e->getMessage());
|
||||
|
||||
@ -3,8 +3,9 @@ namespace App\Controllers\Test;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Patient\PatientModel;
|
||||
use App\Models\OrderTest\OrderTestModel;
|
||||
use App\Models\OrderTestModel;
|
||||
|
||||
class DemoOrderController extends Controller {
|
||||
use ResponseTrait;
|
||||
@ -69,6 +70,20 @@ class DemoOrderController extends Controller {
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Transform Priority and OrderStatus
|
||||
foreach ($orders as &$order) {
|
||||
if (isset($order['Priority'])) {
|
||||
$order['priority'] = $order['Priority'];
|
||||
$order['priorityLabel'] = ValueSet::getLabel('priority', $order['Priority']) ?? '';
|
||||
unset($order['Priority']);
|
||||
}
|
||||
if (isset($order['OrderStatus'])) {
|
||||
$order['orderStatus'] = $order['OrderStatus'];
|
||||
$order['orderStatusLabel'] = ValueSet::getLabel('order_status', $order['OrderStatus']) ?? '';
|
||||
unset($order['OrderStatus']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Data fetched successfully',
|
||||
|
||||
@ -3,6 +3,7 @@ namespace App\Controllers\Test;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\ValueSet;
|
||||
use App\Models\Test\TestMapModel;
|
||||
|
||||
class TestMapController extends BaseController {
|
||||
@ -20,13 +21,41 @@ class TestMapController extends BaseController {
|
||||
public function index() {
|
||||
$rows = $this->model->findAll();
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
|
||||
// Transform HostType and ClientType
|
||||
foreach ($rows as &$row) {
|
||||
if (isset($row['HostType'])) {
|
||||
$row['hostType'] = $row['HostType'];
|
||||
$row['hostTypeLabel'] = ValueSet::getLabel('entity_type', $row['HostType']) ?? '';
|
||||
unset($row['HostType']);
|
||||
}
|
||||
if (isset($row['ClientType'])) {
|
||||
$row['clientType'] = $row['ClientType'];
|
||||
$row['clientTypeLabel'] = ValueSet::getLabel('entity_type', $row['ClientType']) ?? '';
|
||||
unset($row['ClientType']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
}
|
||||
|
||||
public function show($id = null) {
|
||||
$row = $this->model->where('TestMapID',$id)->first();
|
||||
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||
|
||||
// Transform HostType and ClientType
|
||||
if (isset($row['HostType'])) {
|
||||
$row['hostType'] = $row['HostType'];
|
||||
$row['hostTypeLabel'] = ValueSet::getLabel('entity_type', $row['HostType']) ?? '';
|
||||
unset($row['HostType']);
|
||||
}
|
||||
if (isset($row['ClientType'])) {
|
||||
$row['clientType'] = $row['ClientType'];
|
||||
$row['clientTypeLabel'] = ValueSet::getLabel('entity_type', $row['ClientType']) ?? '';
|
||||
unset($row['ClientType']);
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "activity_result",
|
||||
"VSName": "Activity Result",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "0", "value": "Failed"},
|
||||
{"key": "1", "value": "Success with note"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "additive",
|
||||
"VSName": "Additive",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "Hep", "value": "Heparin ammonium"},
|
||||
{"key": "Apro", "value": "Aprotinin"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "adt_event",
|
||||
"VSName": "ADT Event",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "A01", "value": "Admit"},
|
||||
{"key": "A02", "value": "Transfer"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "area_class",
|
||||
"VSName": "Area Class",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "PROP", "value": "Propinsi"},
|
||||
{"key": "KAB", "value": "Kabupaten"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "body_site",
|
||||
"VSName": "Body Site",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "LA", "value": "Left Arm"},
|
||||
{"key": "RA", "value": "Right Arm"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "collection_method",
|
||||
"VSName": "Collection Method",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "pcntr", "value": "Puncture"},
|
||||
{"key": "fprk", "value": "Finger-prick sampling"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "container_cap_color",
|
||||
"VSName": "Container Cap Color",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "PRPL", "value": "Purple"},
|
||||
{"key": "RED", "value": "Red"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "container_class",
|
||||
"VSName": "Container Class",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "Pri", "value": "Primary"},
|
||||
{"key": "Sec", "value": "Secondary"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "container_size",
|
||||
"VSName": "Container Size",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "5ml", "value": "5 mL"},
|
||||
{"key": "7ml", "value": "7 mL"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "country",
|
||||
"VSName": "Country",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "AFG", "value": "Afghanistan"},
|
||||
{"key": "ALA", "value": "Åland Islands"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "ethnic",
|
||||
"VSName": "Ethnic",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "PPMLN", "value": "Papua Melanezoid"},
|
||||
{"key": "NGRID", "value": "Negroid"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "fasting_status",
|
||||
"VSName": "Fasting Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "F", "value": "Fasting"},
|
||||
{"key": "NF", "value": "Not Fasting"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "formula_language",
|
||||
"VSName": "Formula Language",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "Phyton", "value": "Phyton"},
|
||||
{"key": "CQL", "value": "Clinical Quality Language"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "identifier_type",
|
||||
"VSName": "Identifier Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "KTP", "value": "Kartu Tanda Penduduk"},
|
||||
{"key": "PASS", "value": "Passport"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "location_type",
|
||||
"VSName": "Location Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "FCLT", "value": "Facility"},
|
||||
{"key": "BLDG", "value": "Building"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "marital_status",
|
||||
"VSName": "Marital Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "A", "value": "Separated"},
|
||||
{"key": "D", "value": "Divorced"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "math_sign",
|
||||
"VSName": "Math Sign",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "=", "value": "Equal"},
|
||||
{"key": "<", "value": "Less than"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "numeric_ref_type",
|
||||
"VSName": "Numeric Reference Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "RANGE", "value": "Range"},
|
||||
{"key": "THOLD", "value": "Threshold"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "order_priority",
|
||||
"VSName": "Order Priority",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "S", "value": "Stat"},
|
||||
{"key": "A", "value": "ASAP"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "order_status",
|
||||
"VSName": "Order Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "A", "value": "Some, not all results available"},
|
||||
{"key": "CA", "value": "Order is cancelled"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "priority",
|
||||
"VSName": "Priority",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "S", "value": "Stat"},
|
||||
{"key": "A", "value": "ASAP"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "race",
|
||||
"VSName": "Race (Ethnicity)",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "JAWA", "value": "Jawa"},
|
||||
{"key": "SUNDA", "value": "Sunda"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "range_type",
|
||||
"VSName": "Range Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "REF", "value": "Reference Range"},
|
||||
{"key": "CRTC", "value": "Critical Range"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "reference_type",
|
||||
"VSName": "Reference Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "NMRC", "value": "Numeric"},
|
||||
{"key": "TEXT", "value": "Text"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "religion",
|
||||
"VSName": "Religion",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "ISLAM", "value": "Islam"},
|
||||
{"key": "KRSTN", "value": "Kristen"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "request_status",
|
||||
"VSName": "Request Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "STC", "value": "To be collected"},
|
||||
{"key": "SCFld", "value": "Collection failed"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "result_status",
|
||||
"VSName": "Result Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "PRELIMINARY", "value": "Preliminary"},
|
||||
{"key": "FINAL", "value": "Final"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "result_type",
|
||||
"VSName": "Result Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "NMRIC", "value": "Numeric"},
|
||||
{"key": "RANGE", "value": "Range"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "result_unit",
|
||||
"VSName": "Result Unit",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "g/dL", "value": "g/dL"},
|
||||
{"key": "g/L", "value": "g/L"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "sex",
|
||||
"VSName": "Sex",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "1", "value": "Female"},
|
||||
{"key": "2", "value": "Male"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "site_class",
|
||||
"VSName": "Site Class",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "A", "value": "Kelas A"},
|
||||
{"key": "B", "value": "Kelas B"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "site_type",
|
||||
"VSName": "Site Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "GH", "value": "Government Hospital"},
|
||||
{"key": "PH", "value": "Private Hospital"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "specimen_activity",
|
||||
"VSName": "Specimen Activity",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "SColl", "value": "Collection"},
|
||||
{"key": "STran", "value": "Transport"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "specimen_condition",
|
||||
"VSName": "Specimen Condition",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "HEM", "value": "Hemolyzed"},
|
||||
{"key": "ITC", "value": "Icteric"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "specimen_role",
|
||||
"VSName": "Specimen Role",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "P", "value": "Patient"},
|
||||
{"key": "B", "value": "Blind Sample"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "specimen_status",
|
||||
"VSName": "Specimen Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "STC", "value": "To be collected"},
|
||||
{"key": "SCFld", "value": "Collection failed"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "specimen_type",
|
||||
"VSName": "Specimen Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "BLD", "value": "Whole blood"},
|
||||
{"key": "BLDA", "value": "Blood arterial"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "test_activity",
|
||||
"VSName": "Test Activity",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "ORD", "value": "Order"},
|
||||
{"key": "ANA", "value": "Analyse"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "test_status",
|
||||
"VSName": "Test Status",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "PENDING", "value": "Waiting for Results"},
|
||||
{"key": "IN_PROCESS", "value": "Analyzing"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "test_type",
|
||||
"VSName": "Test Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "TEST", "value": "Test"},
|
||||
{"key": "PARAM", "value": "Parameter"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "text_ref_type",
|
||||
"VSName": "Text Reference Type",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "VSET", "value": "Value Set"},
|
||||
{"key": "TEXT", "value": "Text"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{"name": "unit",
|
||||
"VSName": "Unit",
|
||||
"VCategory": "User-defined",
|
||||
"VCategory": "System",
|
||||
"values": [
|
||||
{"key": "L", "value": "Liter"},
|
||||
{"key": "mL", "value": "Mili Liter"},
|
||||
|
||||
405
app/Views/swagger.php
Normal file
405
app/Views/swagger.php
Normal file
@ -0,0 +1,405 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Documentation - CLQMS</title>
|
||||
|
||||
<!-- Swagger UI CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
||||
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Webkit */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
display: flex; /* Sidebar layout */
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
#custom-sidebar {
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow-y: auto;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 20px 0;
|
||||
z-index: 1000;
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
#custom-sidebar h2 {
|
||||
padding: 0 20px;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #3b4151;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
display: block;
|
||||
padding: 8px 20px;
|
||||
color: #3b4151;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-link:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.sidebar-link.active {
|
||||
border-left-color: #49cc90; /* Swagger Green */
|
||||
background: rgba(73, 204, 144, 0.1);
|
||||
font-weight: 600;
|
||||
color: #49cc90;
|
||||
}
|
||||
|
||||
/* Main Content Adjustments */
|
||||
#swagger-ui {
|
||||
margin-left: 280px; /* Width of sidebar */
|
||||
width: calc(100% - 280px);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#swagger-ui .information-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
[data-theme="dark"] body {
|
||||
background: #0d1117;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #custom-sidebar {
|
||||
background: #161b22;
|
||||
border-right: 1px solid #30363d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #custom-sidebar h2 {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-link {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-link:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-link.active {
|
||||
border-left-color: #58a6ff;
|
||||
background: rgba(88, 166, 255, 0.1);
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #swagger-ui {
|
||||
--swagger-ui-color: #8b949e;
|
||||
--swagger-ui-bg: #0d1117;
|
||||
--swagger-ui-primary: #58a6ff;
|
||||
--swagger-ui-text: #c9d1d9;
|
||||
--swagger-ui-border: #30363d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .info {
|
||||
margin: 20px 0;
|
||||
background: #161b22;
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
|
||||
border: 1px solid #30363d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .info .title {
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .info .description {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock {
|
||||
background: #0d1117;
|
||||
border: 1px solid #30363d;
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock .opblock-summary {
|
||||
border-color: #30363d;
|
||||
background: #161b22;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock .opblock-summary-description {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock.opblock-get {
|
||||
border-color: #3fb950;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock.opblock-post {
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock.opblock-put {
|
||||
border-color: #d29922;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock.opblock-delete {
|
||||
border-color: #f85149;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .opblock.opblock-patch {
|
||||
border-color: #a371f7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .scheme-container {
|
||||
background: #161b22;
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .loading-container {
|
||||
background: #0d1117;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .btn {
|
||||
background: #21262d;
|
||||
border-color: #30363d;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .btn:hover {
|
||||
background: #30363d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui input[type="text"],
|
||||
[data-theme="dark"] .swagger-ui textarea {
|
||||
background: #0d1117;
|
||||
border-color: #30363d;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui select {
|
||||
background: #0d1117;
|
||||
border-color: #30363d;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .model-title {
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .property-name {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .tab li {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .tab li.active {
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .responses-inner h4,
|
||||
[data-theme="dark"] .swagger-ui .responses-inner h5 {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .response-col_status {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .swagger-ui .response-col_description {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
display: block; /* Stack on mobile */
|
||||
}
|
||||
|
||||
#custom-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 200px;
|
||||
position: relative;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#swagger-ui {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#swagger-ui .information-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#swagger-ui .info {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="custom-sidebar">
|
||||
<h2>Resources</h2>
|
||||
<div id="sidebar-links"></div>
|
||||
</div>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<!-- Swagger UI Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
||||
|
||||
<script>
|
||||
// Custom Sidebar Logic
|
||||
function initCustomSidebar(ui) {
|
||||
const sidebarLinks = document.getElementById('sidebar-links');
|
||||
|
||||
// Wait for Swagger UI to render tags
|
||||
// We'll poll every 100ms for up to 5 seconds
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
attempts++;
|
||||
const tags = document.querySelectorAll('.opblock-tag-section');
|
||||
|
||||
if (tags.length > 0 || attempts >= maxAttempts) {
|
||||
clearInterval(interval);
|
||||
buildSidebar(tags);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function buildSidebar(tags) {
|
||||
const sidebarLinksContainer = document.getElementById('sidebar-links');
|
||||
sidebarLinksContainer.innerHTML = ''; // Clear existing
|
||||
|
||||
if (tags.length === 0) {
|
||||
sidebarLinksContainer.innerHTML = '<div style="padding:0 20px;color:#888;">No resources found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tags.forEach(tagSection => {
|
||||
const tagHeader = tagSection.querySelector('.opblock-tag');
|
||||
if (!tagHeader) return;
|
||||
|
||||
const tagName = tagHeader.getAttribute('data-tag');
|
||||
const tagText = tagHeader.querySelector('a span') ? tagHeader.querySelector('a span').innerText : tagName;
|
||||
const tagId = tagHeader.id; // e.g., operations-tag-Auth
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.className = 'sidebar-link';
|
||||
link.textContent = tagText;
|
||||
link.href = '#' + tagId;
|
||||
|
||||
link.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
const target = document.getElementById(tagId);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth' });
|
||||
// Update active state
|
||||
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
|
||||
link.classList.add('active');
|
||||
}
|
||||
};
|
||||
|
||||
sidebarLinksContainer.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
// Get JWT token from localStorage
|
||||
// JWT token handled via HttpOnly cookie automatically
|
||||
|
||||
// Initialize Swagger UI
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "<?= base_url('api-docs.yaml') ?>",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout",
|
||||
defaultModelsExpandDepth: 1,
|
||||
defaultModelExpandDepth: 1,
|
||||
displayOperationId: false,
|
||||
displayRequestDuration: true,
|
||||
docExpansion: "list",
|
||||
filter: true,
|
||||
showRequestHeaders: true,
|
||||
showCommonExtensions: true,
|
||||
tryItOutEnabled: true,
|
||||
persistAuthorization: true,
|
||||
withCredentials: true, // Enable cookies
|
||||
syntaxHighlight: {
|
||||
activate: true,
|
||||
theme: "monokai"
|
||||
},
|
||||
validatorUrl: null,
|
||||
|
||||
onComplete: () => {
|
||||
// Initialize Custom Sidebar
|
||||
initCustomSidebar(ui);
|
||||
},
|
||||
|
||||
// Request interceptor to ensure cookies are sent
|
||||
requestInterceptor: (request) => {
|
||||
// Ensure credentials (cookies) are included in the request
|
||||
request.credentials = 'include';
|
||||
return request;
|
||||
}
|
||||
});
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -182,10 +182,10 @@
|
||||
<span x-show="sidebarOpen">Value Sets</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<!-- Settings -->
|
||||
<li class="mt-4">
|
||||
<a href="<?= base_url('/v2/settings') ?>"
|
||||
<a href="<?= base_url('/v2/settings') ?>"
|
||||
:class="isActive('settings') ? 'active' : ''"
|
||||
class="group">
|
||||
<i class="fa-solid fa-cog w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||
|
||||
@ -1,645 +0,0 @@
|
||||
# Migration Plan: Valueset VID → VValue
|
||||
|
||||
## Overview
|
||||
|
||||
Transition from using database `valueset` table (VID as primary key) to the new `App\Libraries\ValueSet` library (VValue as key). This eliminates database joins for lookup values and uses JSON-based static lookup files.
|
||||
|
||||
## Current State
|
||||
|
||||
- Database `valueset` table with columns: `VID` (PK, INT), `VValue` (VARCHAR), `VDesc` (VARCHAR)
|
||||
- 30+ places using `->join('valueset', 'valueset.VID = ...')`
|
||||
- Selects use `valueset.VValue` and `valueset.VDesc` for display text
|
||||
|
||||
## Target State
|
||||
|
||||
- Use `App\Libraries\ValueSet` library with `getLabel(lookupName, key)` method
|
||||
- Lookup names use table-prefixed PascalCase (e.g., `patient_Sex`, `test_TestType`, `container_ContainerCapColor`)
|
||||
- All fields store `VValue` codes directly (e.g., '1', '2', 'M', 'F', 'TEST')
|
||||
- Remove all `valueset` table joins from queries
|
||||
- Keep raw field values for codes; use `ValueSet::getLabel()` for display text
|
||||
- JSON files in `app/Libraries/Data/valuesets/` are already populated
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: JSON Files Rename
|
||||
|
||||
Rename all JSON files in `app/Libraries/Data/valuesets/` to use table-prefixed PascalCase format:
|
||||
|
||||
| Old Name | New Name | Source Table | Field |
|
||||
|----------|----------|--------------|-------|
|
||||
| `gender.json` | `patient_Sex.json` | patient | Gender |
|
||||
| `country.json` | `patient_Country.json` | patient | Country |
|
||||
| `race.json` | `patient_Race.json` | patient | Race |
|
||||
| `religion.json` | `patient_Religion.json` | patient | Religion |
|
||||
| `ethnic.json` | `patient_Ethnic.json` | patient | Ethnic |
|
||||
| `marital_status.json` | `patient_MaritalStatus.json` | patient | MaritalStatus |
|
||||
| `death_indicator.json` | `patient_DeathIndicator.json` | patient | DeathIndicator |
|
||||
| `test_type.json` | `test_TestType.json` | testdefsite | TestType |
|
||||
| `container_cap_color.json` | `container_ContainerCapColor.json` | containerdef | Color |
|
||||
| `container_class.json` | `container_ContainerClass.json` | containerdef | ConClass |
|
||||
| `additive.json` | `container_Additive.json` | containerdef | Additive |
|
||||
| `location_type.json` | `location_LocationType.json` | location | LocType |
|
||||
| `ws_type.json` | `organization_WorkstationType.json` | workstation | Type |
|
||||
| `enable_disable.json` | `organization_EnableDisable.json` | workstation | Enable |
|
||||
| `site_type.json` | `organization_SiteType.json` | site | SiteTypeID |
|
||||
| `site_class.json` | `organization_SiteClass.json` | site | SiteClassID |
|
||||
| `numeric_ref_type.json` | `ref_NumericRefType.json` | refnum | NumRefType |
|
||||
| `range_type.json` | `ref_RangeType.json` | refnum | RangeType |
|
||||
| `text_ref_type.json` | `ref_TextRefType.json` | reftxt | TxtRefType |
|
||||
| `reference_type.json` | `test_ReferenceType.json` | testdeftech | RefType |
|
||||
| `math_sign.json` | `ref_MathSign.json` | refnum | LowSign, HighSign |
|
||||
| `country.json` | `account_Country.json` | account | Country |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
All lookup names use `{table}_{Field}` format for clarity and namespace isolation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Database Schema Migration
|
||||
|
||||
### Files to DELETE
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `app\Database\Seeds\ValueSetSeeder.php` | DELETE |
|
||||
| `app\Database\Seeds\ValueSetCountrySeeder.php` | DELETE |
|
||||
| `app\Database\Seeds\MinimalMasterDataSeeder.php` | DELETE |
|
||||
| `app\Database\Seeds\PatientSeeder.php` | DELETE |
|
||||
| `app\Database\Migrations\2025-09-15-130122_ValueSet.php` | DELETE |
|
||||
|
||||
### Migration: Modify Columns INT → VARCHAR(10)
|
||||
|
||||
**File:** `app\Database\Migrations\2026-01-12-000001_ValuesetVidToVvalue.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class ValuesetVidToVvalue extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// patient table
|
||||
$this->forge->modifyColumn('patient', [
|
||||
'Gender' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'Race' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'Religion' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'Ethnic' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'MaritalStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'DeathIndicator' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// testdefsite table
|
||||
$this->forge->modifyColumn('testdefsite', [
|
||||
'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
|
||||
]);
|
||||
|
||||
// containerdef table
|
||||
$this->forge->modifyColumn('containerdef', [
|
||||
'Additive' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'ConClass' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'Color' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// location table
|
||||
$this->forge->modifyColumn('location', [
|
||||
'LocType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// workstation table
|
||||
$this->forge->modifyColumn('workstation', [
|
||||
'Type' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'Enable' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// site table
|
||||
$this->forge->modifyColumn('site', [
|
||||
'SiteTypeID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'SiteClassID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// account table
|
||||
$this->forge->modifyColumn('account', [
|
||||
'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// refnum table
|
||||
$this->forge->modifyColumn('refnum', [
|
||||
'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'NumRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'RangeType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'LowSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'HighSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// reftxt table
|
||||
$this->forge->modifyColumn('reftxt', [
|
||||
'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
'TxtRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true],
|
||||
]);
|
||||
|
||||
// orderstatus table
|
||||
$this->forge->modifyColumn('orderstatus', [
|
||||
'OrderStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false],
|
||||
]);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// Revert to INT
|
||||
$this->forge->modifyColumn('patient', [
|
||||
'Gender' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'Race' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'Religion' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'Ethnic' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'MaritalStatus' => ['type' => 'TINYINT', 'null' => true],
|
||||
'DeathIndicator' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('testdefsite', [
|
||||
'TestType' => ['type' => 'INT', 'null' => false],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('containerdef', [
|
||||
'Additive' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'ConClass' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'Color' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('location', [
|
||||
'LocType' => ['type' => 'INT', 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('workstation', [
|
||||
'Type' => ['type' => 'TINYINT', 'null' => true],
|
||||
'Enable' => ['type' => 'INT', 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('site', [
|
||||
'SiteTypeID' => ['type' => 'INT', 'null' => true],
|
||||
'SiteClassID' => ['type' => 'INT', 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('account', [
|
||||
'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('refnum', [
|
||||
'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'NumRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'RangeType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'LowSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'HighSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('reftxt', [
|
||||
'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
'TxtRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->modifyColumn('orderstatus', [
|
||||
'OrderStatus' => ['type' => 'INT', 'null' => false],
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** No data migration needed - dummy data will be lost. This is acceptable for development/testing environments.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Library & Model Updates
|
||||
|
||||
### ValueSet Library - Update to read from JSON files
|
||||
|
||||
**File:** `app/Libraries/ValueSet.php`
|
||||
|
||||
Ensure the library reads from JSON files in `app/Libraries/Data/valuesets/`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
class ValueSet
|
||||
{
|
||||
private static $cache = [];
|
||||
|
||||
private static function loadFile(string $name): array
|
||||
{
|
||||
if (!isset(self::$cache[$name])) {
|
||||
$path = APPPATH . 'Libraries/Data/valuesets/' . $name . '.json';
|
||||
if (file_exists($path)) {
|
||||
$content = file_get_contents($path);
|
||||
self::$cache[$name] = json_decode($content, true)['values'] ?? [];
|
||||
} else {
|
||||
self::$cache[$name] = [];
|
||||
}
|
||||
}
|
||||
return self::$cache[$name];
|
||||
}
|
||||
|
||||
public static function getLabel(string $lookupName, string $key): ?string
|
||||
{
|
||||
$values = self::loadFile($lookupName);
|
||||
foreach ($values as $item) {
|
||||
if (($item['key'] ?? $item['value'] ?? null) === $key) {
|
||||
return $item['value'] ?? $item['label'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getOptions(string $lookupName): array
|
||||
{
|
||||
$values = self::loadFile($lookupName);
|
||||
return array_map(function ($item) {
|
||||
return [
|
||||
'key' => $item['key'] ?? '',
|
||||
'value' => $item['value'] ?? $item['label'] ?? '',
|
||||
];
|
||||
}, $values);
|
||||
}
|
||||
|
||||
public static function transformLabels(array $data, array $fieldMappings): array
|
||||
{
|
||||
foreach ($data as &$row) {
|
||||
foreach ($fieldMappings as $field => $lookupName) {
|
||||
if (isset($row[$field]) && $row[$field] !== null) {
|
||||
$row[$field . 'Text'] = self::getLabel($lookupName, $row[$field]) ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ValueSetModel - Deprecate or Repurpose
|
||||
|
||||
**File:** `app/Models/ValueSet/ValueSetModel.php`
|
||||
|
||||
Options:
|
||||
1. **Deprecate entirely** - No longer needed after migration
|
||||
2. **Repurpose for JSON file management** - Read/write to JSON files
|
||||
3. **Keep as-is for backward compatibility** - If database valuesets are still needed
|
||||
|
||||
Recommended: Deprecate and remove references after migration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Model Changes
|
||||
|
||||
### Pattern for Model Updates
|
||||
|
||||
**Before:**
|
||||
```php
|
||||
$this->select("..., gender.VValue as Gender, gender.VDesc as GenderText")
|
||||
->join('valueset gender', 'gender.VID = patient.Gender', 'left')
|
||||
```
|
||||
|
||||
**After:**
|
||||
```php
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
$this->select("..., patient.Gender");
|
||||
// After fetching:
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'Gender' => 'patient_Sex',
|
||||
'Country' => 'patient_Country',
|
||||
'Race' => 'patient_Race',
|
||||
'Religion' => 'patient_Religion',
|
||||
'Ethnic' => 'patient_Ethnic',
|
||||
'DeathIndicator' => 'patient_DeathIndicator',
|
||||
'MaritalStatus' => 'patient_MaritalStatus',
|
||||
]);
|
||||
```
|
||||
|
||||
### Models to Modify (8 files)
|
||||
|
||||
#### 1. `app/Models/Patient/PatientModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Line 27: `$this->join('valueset vs', 'vs.vid = Gender', 'left');`
|
||||
- Lines 52-64: All `*.VID as *VID` aliases
|
||||
- Lines 75-81: All `valueset.*` joins
|
||||
|
||||
**Add transformation in `getPatient()`:**
|
||||
```php
|
||||
$patient = ValueSet::transformLabels([$patient], [
|
||||
'Gender' => 'patient_gender',
|
||||
'Country' => 'patient_country',
|
||||
'Race' => 'patient_race',
|
||||
'Religion' => 'patient_religion',
|
||||
'Ethnic' => 'patient_ethnic',
|
||||
'DeathIndicator' => 'patient_death_indicator',
|
||||
'MaritalStatus' => 'patient_marital_status',
|
||||
])[0];
|
||||
```
|
||||
|
||||
#### 2. `app/Models/Location/LocationModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Lines 18, 30: `->join("valueset v", "v.VID=location.loctype", ...)`
|
||||
|
||||
**Add transformation:**
|
||||
```php
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'LocType' => 'location_LocationType',
|
||||
]);
|
||||
```
|
||||
|
||||
#### 3. `app/Models/Test/TestDefSiteModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Lines 42, 75, 103: `->join("valueset", "valueset.VID=...")`
|
||||
|
||||
**Add transformation:**
|
||||
```php
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'TestType' => 'test_TestType',
|
||||
]);
|
||||
```
|
||||
|
||||
#### 4. `app/Models/Test/TestDefGrpModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Line 32: `->join('valueset vs', 'vs.VID=t.TestType', 'left')`
|
||||
|
||||
#### 5. `app/Models/Specimen/ContainerDefModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Lines 20-22, 37-39: All 6 `valueset.*` joins
|
||||
|
||||
**Add transformation:**
|
||||
```php
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'Color' => 'container_ContainerCapColor',
|
||||
'ConClass' => 'container_ContainerClass',
|
||||
'Additive' => 'container_Additive',
|
||||
]);
|
||||
```
|
||||
|
||||
#### 6. `app/Models/Organization/SiteModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Lines 38-39: `->join('valueset sitetype'...)` and `->join('valueset siteclass'...)`
|
||||
|
||||
**Add transformation:**
|
||||
```php
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'SiteTypeID' => 'organization_SiteType',
|
||||
'SiteClassID' => 'organization_SiteClass',
|
||||
])[0];
|
||||
```
|
||||
|
||||
#### 7. `app/Models/Organization/AccountModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Line 41: `->join('valueset country'...)`
|
||||
|
||||
**Remove from select:**
|
||||
- Line 36: `country.VID as country`
|
||||
|
||||
**Add transformation in controller if needed:**
|
||||
```php
|
||||
$rows = ValueSet::transformLabels($rows, [
|
||||
'Country' => 'account_Country',
|
||||
]);
|
||||
```
|
||||
|
||||
#### 8. `app/Models/Organization/WorkstationModel.php`
|
||||
|
||||
**Remove:**
|
||||
- Lines 36-37: `->join('valueset wstype'...)` and `->join('valueset enable'...)`
|
||||
|
||||
**Add transformation:**
|
||||
```php
|
||||
$row = ValueSet::transformLabels([$row], [
|
||||
'Type' => 'organization_WorkstationType',
|
||||
'Enable' => 'organization_EnableDisable',
|
||||
])[0];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Controller Changes
|
||||
|
||||
### `app/Controllers/TestsController.php`
|
||||
|
||||
**Remove:**
|
||||
- Line 69: `->join("valueset", "valueset.VID=testdefsite.TestType", "left")`
|
||||
- Line 111: `->join("valueset", "valueset.VID=testdefsite.TestType", "left")`
|
||||
- Line 140: `->join('valueset vs', 'vs.VID=t.TestType', 'left')`
|
||||
|
||||
**Replace `getVValue()` method:**
|
||||
```php
|
||||
private function getVValue($vsetID, $vid) {
|
||||
// DEPRECATED - Use ValueSet::getLabel() instead
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
**Update references from `getVValue()` to `ValueSet::getLabel()`:**
|
||||
```php
|
||||
// Before:
|
||||
'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']),
|
||||
|
||||
// After:
|
||||
'NumRefTypeVValue' => \App\Libraries\ValueSet::getLabel('ref_NumericRefType', $r['NumRefType']),
|
||||
```
|
||||
|
||||
**VSetID to Lookup Name Mapping:**
|
||||
| VSetID | Constant | Lookup Name |
|
||||
|--------|----------|-------------|
|
||||
| 44 | `VALUESET_REF_TYPE` | `test_ReferenceType` |
|
||||
| 45 | `VALUESET_RANGE_TYPE` | `ref_RangeType` |
|
||||
| 46 | `VALUESET_NUM_REF_TYPE` | `ref_NumericRefType` |
|
||||
| 47 | `VALUESET_TXT_REF_TYPE` | `ref_TextRefType` |
|
||||
| 3 | `VALUESET_SEX` | `patient_Sex` |
|
||||
| 41 | `VALUESET_MATH_SIGN` | `ref_MathSign` |
|
||||
|
||||
**Update `getValuesetOptions()` to use JSON:**
|
||||
```php
|
||||
private function getValuesetOptions($lookupName)
|
||||
{
|
||||
return \App\Libraries\ValueSet::getOptions($lookupName);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: API Endpoints - Replace with JSON-based endpoints
|
||||
|
||||
### New API Controller: `app/Controllers/ValueSetApiController.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\ValueSet;
|
||||
|
||||
class ValueSetApiController extends \CodeIgniter\Controller
|
||||
{
|
||||
use \CodeIgniter\API\ResponseTrait;
|
||||
|
||||
public function index(string $lookupName)
|
||||
{
|
||||
$data = ValueSet::getOptions($lookupName);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'data' => $data
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
$dir = APPPATH . 'Libraries/Data/valuesets/';
|
||||
$files = glob($dir . '*.json');
|
||||
$result = [];
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file, '.json');
|
||||
$result[] = [
|
||||
'name' => $name,
|
||||
'options' => ValueSet::getOptions($name)
|
||||
];
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'data' => $result
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Routes: `app/Config/Routes.php`
|
||||
|
||||
```php
|
||||
$routes->group('api', function ($routes) {
|
||||
$routes->get('valueset/(:segment)', 'ValueSetApiController::index/$1');
|
||||
$routes->get('valueset', 'ValueSetApiController::all');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: View Updates
|
||||
|
||||
### 1. `app/Views/v2/master/valuesets/valuesets_index.php`
|
||||
|
||||
Repurpose to manage JSON-based valuesets instead of database table.
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `fetch(...api/valueset...)` | `fetch(...api/valueset/lookupName...)` |
|
||||
| Database CRUD operations | File-based CRUD operations |
|
||||
|
||||
### 2. `app/Views/v2/master/valuesets/valueset_nested_crud.php`
|
||||
|
||||
Repurpose for JSON file management.
|
||||
|
||||
### 3. `app/Views/v2/master/valuesets/valueset_dialog.php`
|
||||
|
||||
Update for JSON file format.
|
||||
|
||||
### 4. `app/Views/v2/master/tests/tests_index.php`
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `type?.VID` | `type?.key` |
|
||||
| `type?.VValue` | `type?.value` |
|
||||
| `type?.VDesc` | `type?.label` |
|
||||
| `{ VID: 1, VValue: 'TEST', ... }` | `{ key: 'TEST', value: 'Test', ... }` |
|
||||
| `getTypeName(vid)` | `getTypeName(value)` |
|
||||
| `api/valuesetdef/27` | `api/valueset/test_TestType` |
|
||||
| Hardcoded fallback: `{ VID: 1, VValue: 'TEST', VDesc: 'Test' }` | `{ key: 'TEST', value: 'Test' }` |
|
||||
|
||||
### 5. Additional Views to Update
|
||||
|
||||
| View File | Fields to Update | Lookup Name |
|
||||
|-----------|------------------|-------------|
|
||||
| `app/Views/v2/patients/patients_index.php` | Gender, Country, Race, Religion, Ethnic, MaritalStatus, DeathIndicator | `patient_*` |
|
||||
| `app/Views/v2/master/specimen/containers_index.php` | Color, ConClass, Additive | `container_*` |
|
||||
| `app/Views/v2/master/organization/sites_index.php` | SiteTypeID, SiteClassID | `organization_*` |
|
||||
| `app/Views/v2/master/organization/workstations_index.php` | Type, Enable | `organization_*` |
|
||||
| `app/Views/v2/master/organization/accounts_index.php` | Country | `account_Country` |
|
||||
| `app/Views/v2/master/organization/locations_index.php` | LocType | `location_LocationType` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Test Files Update
|
||||
|
||||
### `tests/feature/ValueSet/ValueSetApiControllerTest.php`
|
||||
|
||||
Update tests to use new JSON-based API endpoints.
|
||||
|
||||
### `tests/_support/v2/MasterTestCase.php`
|
||||
|
||||
Update any valueset-related test data setup.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
1. **Unit Tests**
|
||||
- Test `ValueSet::getLabel('patient_Sex', '1')` returns 'Female'
|
||||
- Test `ValueSet::getLabel('test_TestType', 'TEST')` returns 'Test'
|
||||
- Test `ValueSet::getOptions('container_ContainerCapColor')` returns correct format
|
||||
- Test `ValueSet::transformLabels()` with table-prefixed field mappings
|
||||
|
||||
2. **Integration Tests**
|
||||
- Patient CRUD (create, read, update, delete)
|
||||
- Test definition CRUD
|
||||
- Location CRUD
|
||||
- Container definition CRUD
|
||||
- Organization (site, account, workstation) CRUD
|
||||
|
||||
3. **Manual Testing**
|
||||
- Verify all dropdowns display correct labels
|
||||
- Verify filtering by valueset fields works
|
||||
- Verify form submissions save correct VValue codes
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
1. Run migration `down()` to revert column types
|
||||
2. Restore deleted seeders from git if needed
|
||||
3. Restore deleted migration file from git
|
||||
|
||||
---
|
||||
|
||||
## Files Summary
|
||||
|
||||
| Phase | Action | Files |
|
||||
|-------|--------|-------|
|
||||
| 1 | RENAME | ~50 JSON files in `app/Libraries/Data/valuesets/` |
|
||||
| 2 | DELETE | 5 seeders, 1 migration |
|
||||
| 2 | CREATE | 1 migration (column changes) |
|
||||
| 3 | UPDATE | 1 library (ValueSet.php), ValueSetModel.php deprecation |
|
||||
| 4 | UPDATE | 8 Model files |
|
||||
| 5 | UPDATE | 1 Controller, Routes |
|
||||
| 6 | CREATE | 1 Controller (ValueSetApiController.php) |
|
||||
| 6 | UPDATE | ~6+ View files |
|
||||
| 8 | UPDATE | 2+ Test files |
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Phase 1 (JSON Rename): 15 minutes
|
||||
- Phase 2 (Migration): 30 minutes
|
||||
- Phase 3 (Library + Model): 30 minutes
|
||||
- Phase 4 (Models): 1.5 hours
|
||||
- Phase 5 (Controller): 30 minutes
|
||||
- Phase 6 (API + Views): 2 hours
|
||||
- Phase 8 (Tests): 30 minutes
|
||||
- Testing: 1 hour
|
||||
|
||||
**Total Estimated Time: ~7 hours**
|
||||
File diff suppressed because it is too large
Load Diff
2751
public/api-docs.yaml
Normal file
2751
public/api-docs.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -355,8 +355,8 @@ class ValueSetTest extends CIUnitTestCase
|
||||
public function testTransformLabels()
|
||||
{
|
||||
$data = [
|
||||
['Gender' => '1', 'Country' => 'ID'],
|
||||
['Gender' => '2', 'Country' => 'US']
|
||||
['Gender' => '1', 'Country' => 'IDN'],
|
||||
['Gender' => '2', 'Country' => 'USA']
|
||||
];
|
||||
|
||||
$result = ValueSet::transformLabels($data, [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user