refactor: Rename controllers to follow CodeIgniter 4 naming convention
- Rename all controllers from X.php to XController.php format - Add new RefTxtModel for text-based reference ranges - Rename group_dialog.php to grp_dialog.php and remove title_dialog.php - Add comprehensive test suite for v2/master/TestDef module - Update Routes.php to reflect controller renames - Remove obsolete data files (clqms_v2.sql, lab.dbml)
This commit is contained in:
parent
9e0b01e7e2
commit
cd65e91db1
161
README.md
161
README.md
@ -66,6 +66,167 @@ When working on UI components or dropdowns, **always check for existing ValueSet
|
||||
|
||||
---
|
||||
|
||||
## 📋 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.
|
||||
|
||||
### 🧪 Laboratory Tests (`/v2/master/tests`)
|
||||
|
||||
The Test Definitions module manages all laboratory test configurations including parameters, calculated tests, and test panels.
|
||||
|
||||
#### Test Types
|
||||
|
||||
| Type Code | Description | Table |
|
||||
|-----------|-------------|-------|
|
||||
| `TEST` | Individual laboratory test with technical specs | `testdefsite` + `testdeftech` |
|
||||
| `PARAM` | Parameter value (non-lab measurement) | `testdefsite` + `testdeftech` |
|
||||
| `CALC` | Calculated test with formula | `testdefsite` + `testdefcal` |
|
||||
| `GROUP` | Panel/profile containing multiple tests | `testdefsite` + `testdefgrp` |
|
||||
| `TITLE` | Section title for report organization | `testdefsite` |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/tests` | List all tests with optional filtering |
|
||||
| `GET` | `/api/tests/{id}` | Get test details with type-specific data |
|
||||
| `POST` | `/api/tests` | Create new test definition |
|
||||
| `PATCH` | `/api/tests` | Update existing test |
|
||||
| `DELETE` | `/api/tests` | Soft delete test (sets EndDate) |
|
||||
|
||||
#### Filtering Parameters
|
||||
|
||||
- `TestSiteName` - Search by test name (partial match)
|
||||
- `TestType` - Filter by test type VID (1-5)
|
||||
- `VisibleScr` - Filter by screen visibility (0/1)
|
||||
- `VisibleRpt` - Filter by report visibility (0/1)
|
||||
|
||||
#### Test Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Data fetched successfully",
|
||||
"data": [
|
||||
{
|
||||
"TestSiteID": 1,
|
||||
"TestSiteCode": "CBC",
|
||||
"TestSiteName": "Complete Blood Count",
|
||||
"TestType": 4,
|
||||
"TypeCode": "GROUP",
|
||||
"TypeName": "Group Test",
|
||||
"SeqScr": 50,
|
||||
"VisibleScr": 1,
|
||||
"VisibleRpt": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 📏 Reference Ranges (`/v2/master/refrange`)
|
||||
|
||||
Reference Ranges define normal and critical values for test results. The system supports multiple reference range types based on patient demographics.
|
||||
|
||||
#### Reference Range Types
|
||||
|
||||
| Type | Table | Description |
|
||||
|------|-------|-------------|
|
||||
| Numeric | `refnum` | Numeric ranges with age/sex criteria |
|
||||
| Threshold | `refthold` | Critical threshold values |
|
||||
| Text | `reftxt` | Text-based reference values |
|
||||
| Value Set | `refvset` | Coded reference values |
|
||||
|
||||
#### Numeric Reference Range Structure
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `NumRefType` | Type: REF (Reference), CRTC (Critical), VAL (Validation), RERUN |
|
||||
| `RangeType` | RANGE or THOLD |
|
||||
| `Sex` | Gender filter (0=All, 1=Female, 2=Male) |
|
||||
| `AgeStart` | Minimum age (years) |
|
||||
| `AgeEnd` | Maximum age (years) |
|
||||
| `LowSign` | Low boundary sign (=, <, <=) |
|
||||
| `Low` | Low boundary value |
|
||||
| `HighSign` | High boundary sign (=, >, >=) |
|
||||
| `High` | High boundary value |
|
||||
| `Flag` | Result flag (H, L, A, etc.) |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/refnum` | List numeric reference ranges |
|
||||
| `GET` | `/api/refnum/{id}` | Get reference range details |
|
||||
| `POST` | `/api/refnum` | Create reference range |
|
||||
| `PATCH` | `/api/refnum` | Update reference range |
|
||||
| `DELETE` | `/api/refnum` | Soft delete reference range |
|
||||
|
||||
### 📑 Value Sets (`/v2/master/valuesets`)
|
||||
|
||||
Value Sets are configurable dropdown options used throughout the system. Each Value Set Definition (VSetDef) contains multiple Value Set Values (ValueSet).
|
||||
|
||||
#### Value Set Hierarchy
|
||||
|
||||
```
|
||||
valuesetdef (VSetDefID, VSName, VSDesc)
|
||||
└── valueset (VID, VSetID, VValue, VDesc, VOrder, VCategory)
|
||||
```
|
||||
|
||||
#### Common Value Sets
|
||||
|
||||
| VSetDefID | Name | Example Values |
|
||||
|-----------|------|----------------|
|
||||
| 1 | Priority | STAT (S), ASAP (A), Routine (R), Preop (P) |
|
||||
| 2 | Enable/Disable | Disabled (0), Enabled (1) |
|
||||
| 3 | Gender | Female (1), Male (2), Unknown (3) |
|
||||
| 10 | Order Status | STC, SCtd, SArrv, SRcvd, SAna, etc. |
|
||||
| 15 | Specimen Type | BLD, SER, PLAS, UR, CSF, etc. |
|
||||
| 16 | Unit | L, mL, g/dL, mg/dL, etc. |
|
||||
| 27 | Test Type | TEST, PARAM, CALC, GROUP, TITLE |
|
||||
| 28 | Result Unit | g/dL, g/L, mg/dL, x10^6/mL, etc. |
|
||||
| 35 | Test Activity | Order, Analyse, VER, REV, REP |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/valuesetdef` | List all value set definitions |
|
||||
| `GET` | `/api/valuesetdef/{id}` | Get valueset with all values |
|
||||
| `GET` | `/api/valuesetdef/{id}/values` | Get values for specific valueset |
|
||||
| `POST` | `/api/valuesetdef` | Create new valueset definition |
|
||||
| `PATCH` | `/api/valuesetdef` | Update valueset definition |
|
||||
| `DELETE` | `/api/valuesetdef` | Delete valueset definition |
|
||||
|
||||
#### Value Set Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"VSetDefID": 27,
|
||||
"VSName": "Test Type",
|
||||
"VSDesc": "testdefsite.TestType",
|
||||
"values": [
|
||||
{ "VID": 1, "VValue": "TEST", "VDesc": "Test", "VOrder": 1 },
|
||||
{ "VID": 2, "VValue": "PARAM", "VDesc": "Parameter", "VOrder": 2 },
|
||||
{ "VID": 3, "VValue": "CALC", "VDesc": "Calculated Test", "VOrder": 3 },
|
||||
{ "VID": 4, "VValue": "GROUP", "VDesc": "Group Test", "VOrder": 4 },
|
||||
{ "VID": 5, "VValue": "TITLE", "VDesc": "Title", "VOrder": 5 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 📊 Database Tables Summary
|
||||
|
||||
| Category | Tables | Purpose |
|
||||
|----------|--------|---------|
|
||||
| Tests | `testdefsite`, `testdeftech`, `testdefcal`, `testdefgrp`, `testmap` | Test definitions |
|
||||
| Reference Ranges | `refnum`, `refthold`, `reftxt`, `refvset` | Result validation |
|
||||
| Value Sets | `valuesetdef`, `valueset` | Configurable options |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Edge API - Instrument Integration
|
||||
|
||||
The **Edge API** provides endpoints for integrating laboratory instruments via the `tiny-edge` middleware. Results from instruments are staged in the `edgeres` table before processing into the main patient results (`patres`).
|
||||
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\Router\RouteCollection;
|
||||
/**
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->get('/', function() {
|
||||
$routes->get('/', function () {
|
||||
return redirect()->to('/v2');
|
||||
});
|
||||
|
||||
@ -13,10 +13,10 @@ $routes->options('(:any)', function () {
|
||||
return '';
|
||||
});
|
||||
|
||||
$routes->group('api', ['filter' => 'auth'], function($routes) {
|
||||
$routes->get('dashboard', 'Dashboard::index');
|
||||
$routes->get('result', 'Result::index');
|
||||
$routes->get('sample', 'Sample::index');
|
||||
$routes->group('api', ['filter' => 'auth'], function ($routes) {
|
||||
$routes->get('dashboard', 'DashboardController::index');
|
||||
$routes->get('result', 'ResultController::index');
|
||||
$routes->get('sample', 'SampleController::index');
|
||||
});
|
||||
|
||||
// Public Routes (no auth required)
|
||||
@ -24,10 +24,10 @@ $routes->get('/v2/login', 'PagesController::login');
|
||||
|
||||
// V2 Auth API Routes (public - no auth required)
|
||||
$routes->group('v2/auth', function ($routes) {
|
||||
$routes->post('login', 'AuthV2::login');
|
||||
$routes->post('register', 'AuthV2::register');
|
||||
$routes->get('check', 'AuthV2::checkAuth');
|
||||
$routes->post('logout', 'AuthV2::logout');
|
||||
$routes->post('login', 'AuthV2Controller::login');
|
||||
$routes->post('register', 'AuthV2Controller::register');
|
||||
$routes->get('check', 'AuthV2Controller::checkAuth');
|
||||
$routes->post('logout', 'AuthV2Controller::logout');
|
||||
});
|
||||
|
||||
// Protected Page Routes - V2 (requires auth)
|
||||
@ -37,18 +37,18 @@ $routes->group('v2', ['filter' => 'auth'], function ($routes) {
|
||||
$routes->get('patients', 'PagesController::patients');
|
||||
$routes->get('requests', 'PagesController::requests');
|
||||
$routes->get('settings', 'PagesController::settings');
|
||||
|
||||
|
||||
// Master Data - Organization
|
||||
$routes->get('master/organization/accounts', 'PagesController::masterOrgAccounts');
|
||||
$routes->get('master/organization/sites', 'PagesController::masterOrgSites');
|
||||
$routes->get('master/organization/disciplines', 'PagesController::masterOrgDisciplines');
|
||||
$routes->get('master/organization/departments', 'PagesController::masterOrgDepartments');
|
||||
$routes->get('master/organization/workstations', 'PagesController::masterOrgWorkstations');
|
||||
|
||||
|
||||
// Master Data - Specimen
|
||||
$routes->get('master/specimen/containers', 'PagesController::masterSpecimenContainers');
|
||||
$routes->get('master/specimen/preparations', 'PagesController::masterSpecimenPreparations');
|
||||
|
||||
|
||||
// Master Data - Tests & ValueSets
|
||||
$routes->get('master/tests', 'PagesController::masterTests');
|
||||
$routes->get('master/valuesets', 'PagesController::masterValueSets');
|
||||
@ -60,36 +60,36 @@ $routes->get('faker/faker-patient/(:num)', 'faker\FakerPatient::sendMany/$1');
|
||||
$routes->group('api', function ($routes) {
|
||||
// Auth
|
||||
$routes->group('auth', function ($routes) {
|
||||
$routes->post('login', 'Auth::login');
|
||||
$routes->post('change_pass', 'Auth::change_pass');
|
||||
$routes->post('register', 'Auth::register');
|
||||
$routes->get('check', 'Auth::checkAuth');
|
||||
$routes->post('logout', 'Auth::logout');
|
||||
$routes->post('login', 'AuthController::login');
|
||||
$routes->post('change_pass', 'AuthController::change_pass');
|
||||
$routes->post('register', 'AuthController::register');
|
||||
$routes->get('check', 'AuthController::checkAuth');
|
||||
$routes->post('logout', 'AuthController::logout');
|
||||
});
|
||||
|
||||
// Patient
|
||||
$routes->group('patient', function ($routes) {
|
||||
$routes->get('/', 'Patient\Patient::index');
|
||||
$routes->post('/', 'Patient\Patient::create');
|
||||
$routes->get('(:num)', 'Patient\Patient::show/$1');
|
||||
$routes->delete('/', 'Patient\Patient::delete');
|
||||
$routes->patch('/', 'Patient\Patient::update');
|
||||
$routes->get('check', 'Patient\Patient::patientCheck');
|
||||
$routes->get('/', 'Patient\PatientController::index');
|
||||
$routes->post('/', 'Patient\PatientController::create');
|
||||
$routes->get('(:num)', 'Patient\PatientController::show/$1');
|
||||
$routes->delete('/', 'Patient\PatientController::delete');
|
||||
$routes->patch('/', 'Patient\PatientController::update');
|
||||
$routes->get('check', 'Patient\PatientController::patientCheck');
|
||||
});
|
||||
|
||||
// PatVisit
|
||||
$routes->group('patvisit', function ($routes) {
|
||||
$routes->get('/', 'PatVisit::index');
|
||||
$routes->post('/', 'PatVisit::create');
|
||||
$routes->get('patient/(:num)', 'PatVisit::showByPatient/$1');
|
||||
$routes->get('(:any)', 'PatVisit::show/$1');
|
||||
$routes->delete('/', 'PatVisit::delete');
|
||||
$routes->patch('/', 'PatVisit::update');
|
||||
$routes->get('/', 'PatVisitController::index');
|
||||
$routes->post('/', 'PatVisitController::create');
|
||||
$routes->get('patient/(:num)', 'PatVisitController::showByPatient/$1');
|
||||
$routes->get('(:any)', 'PatVisitController::show/$1');
|
||||
$routes->delete('/', 'PatVisitController::delete');
|
||||
$routes->patch('/', 'PatVisitController::update');
|
||||
});
|
||||
|
||||
$routes->group('patvisitadt', function ($routes) {
|
||||
$routes->post('/', 'PatVisit::createADT');
|
||||
$routes->patch('/', 'PatVisit::updateADT');
|
||||
$routes->post('/', 'PatVisitController::createADT');
|
||||
$routes->patch('/', 'PatVisitController::updateADT');
|
||||
});
|
||||
|
||||
// Master Data
|
||||
@ -115,169 +115,169 @@ $routes->group('api', function ($routes) {
|
||||
|
||||
// Location
|
||||
$routes->group('location', function ($routes) {
|
||||
$routes->get('/', 'Location::index');
|
||||
$routes->get('(:num)', 'Location::show/$1');
|
||||
$routes->post('/', 'Location::create');
|
||||
$routes->patch('/', 'Location::update');
|
||||
$routes->delete('/', 'Location::delete');
|
||||
$routes->get('/', 'LocationController::index');
|
||||
$routes->get('(:num)', 'LocationController::show/$1');
|
||||
$routes->post('/', 'LocationController::create');
|
||||
$routes->patch('/', 'LocationController::update');
|
||||
$routes->delete('/', 'LocationController::delete');
|
||||
});
|
||||
|
||||
// Contact
|
||||
$routes->group('contact', function ($routes) {
|
||||
$routes->get('/', 'Contact\Contact::index');
|
||||
$routes->get('(:num)', 'Contact\Contact::show/$1');
|
||||
$routes->post('/', 'Contact\Contact::create');
|
||||
$routes->patch('/', 'Contact\Contact::update');
|
||||
$routes->delete('/', 'Contact\Contact::delete');
|
||||
$routes->get('/', 'Contact\ContactController::index');
|
||||
$routes->get('(:num)', 'Contact\ContactController::show/$1');
|
||||
$routes->post('/', 'Contact\ContactController::create');
|
||||
$routes->patch('/', 'Contact\ContactController::update');
|
||||
$routes->delete('/', 'Contact\ContactController::delete');
|
||||
});
|
||||
|
||||
$routes->group('occupation', function ($routes) {
|
||||
$routes->get('/', 'Contact\Occupation::index');
|
||||
$routes->get('(:num)', 'Contact\Occupation::show/$1');
|
||||
$routes->post('/', 'Contact\Occupation::create');
|
||||
$routes->patch('/', 'Contact\Occupation::update');
|
||||
//$routes->delete('/', 'Contact\Occupation::delete');
|
||||
$routes->get('/', 'Contact\OccupationController::index');
|
||||
$routes->get('(:num)', 'Contact\OccupationController::show/$1');
|
||||
$routes->post('/', 'Contact\OccupationController::create');
|
||||
$routes->patch('/', 'Contact\OccupationController::update');
|
||||
//$routes->delete('/', 'Contact\OccupationController::delete');
|
||||
});
|
||||
|
||||
$routes->group('medicalspecialty', function ($routes) {
|
||||
$routes->get('/', 'Contact\MedicalSpecialty::index');
|
||||
$routes->get('(:num)', 'Contact\MedicalSpecialty::show/$1');
|
||||
$routes->post('/', 'Contact\MedicalSpecialty::create');
|
||||
$routes->patch('/', 'Contact\MedicalSpecialty::update');
|
||||
$routes->get('/', 'Contact\MedicalSpecialtyController::index');
|
||||
$routes->get('(:num)', 'Contact\MedicalSpecialtyController::show/$1');
|
||||
$routes->post('/', 'Contact\MedicalSpecialtyController::create');
|
||||
$routes->patch('/', 'Contact\MedicalSpecialtyController::update');
|
||||
});
|
||||
|
||||
// ValueSet
|
||||
$routes->group('valueset', function ($routes) {
|
||||
$routes->get('/', 'ValueSet\ValueSet::index');
|
||||
$routes->get('(:num)', 'ValueSet\ValueSet::show/$1');
|
||||
$routes->get('valuesetdef/(:num)', 'ValueSet\ValueSet::showByValueSetDef/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSet::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSet::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSet::delete');
|
||||
$routes->get('/', 'ValueSet\ValueSetController::index');
|
||||
$routes->get('(:num)', 'ValueSet\ValueSetController::show/$1');
|
||||
$routes->get('valuesetdef/(:num)', 'ValueSet\ValueSetController::showByValueSetDef/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSetController::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSetController::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSetController::delete');
|
||||
});
|
||||
|
||||
$routes->group('valuesetdef', function ($routes) {
|
||||
$routes->get('/', 'ValueSet\ValueSetDef::index');
|
||||
$routes->get('(:segment)', 'ValueSet\ValueSetDef::show/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSetDef::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSetDef::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSetDef::delete');
|
||||
$routes->get('/', 'ValueSet\ValueSetDefController::index');
|
||||
$routes->get('(:segment)', 'ValueSet\ValueSetDefController::show/$1');
|
||||
$routes->post('/', 'ValueSet\ValueSetDefController::create');
|
||||
$routes->patch('/', 'ValueSet\ValueSetDefController::update');
|
||||
$routes->delete('/', 'ValueSet\ValueSetDefController::delete');
|
||||
});
|
||||
|
||||
// Counter
|
||||
$routes->group('counter', function ($routes) {
|
||||
$routes->get('/', 'Counter::index');
|
||||
$routes->get('(:num)', 'Counter::show/$1');
|
||||
$routes->post('/', 'Counter::create');
|
||||
$routes->patch('/', 'Counter::update');
|
||||
$routes->delete('/', 'Counter::delete');
|
||||
$routes->get('/', 'CounterController::index');
|
||||
$routes->get('(:num)', 'CounterController::show/$1');
|
||||
$routes->post('/', 'CounterController::create');
|
||||
$routes->patch('/', 'CounterController::update');
|
||||
$routes->delete('/', 'CounterController::delete');
|
||||
});
|
||||
|
||||
// AreaGeo
|
||||
$routes->group('areageo', function ($routes) {
|
||||
$routes->get('/', 'AreaGeo::index');
|
||||
$routes->get('provinces', 'AreaGeo::getProvinces');
|
||||
$routes->get('cities', 'AreaGeo::getCities');
|
||||
$routes->get('/', 'AreaGeoController::index');
|
||||
$routes->get('provinces', 'AreaGeoController::getProvinces');
|
||||
$routes->get('cities', 'AreaGeoController::getCities');
|
||||
});
|
||||
|
||||
// Organization
|
||||
$routes->group('organization', function ($routes) {
|
||||
// Account
|
||||
$routes->group('account', function ($routes) {
|
||||
$routes->get('/', 'Organization\Account::index');
|
||||
$routes->get('(:num)', 'Organization\Account::show/$1');
|
||||
$routes->post('/', 'Organization\Account::create');
|
||||
$routes->patch('/', 'Organization\Account::update');
|
||||
$routes->delete('/', 'Organization\Account::delete');
|
||||
$routes->get('/', 'Organization\AccountController::index');
|
||||
$routes->get('(:num)', 'Organization\AccountController::show/$1');
|
||||
$routes->post('/', 'Organization\AccountController::create');
|
||||
$routes->patch('/', 'Organization\AccountController::update');
|
||||
$routes->delete('/', 'Organization\AccountController::delete');
|
||||
});
|
||||
|
||||
// Site
|
||||
$routes->group('site', function ($routes) {
|
||||
$routes->get('/', 'Organization\Site::index');
|
||||
$routes->get('(:num)', 'Organization\Site::show/$1');
|
||||
$routes->post('/', 'Organization\Site::create');
|
||||
$routes->patch('/', 'Organization\Site::update');
|
||||
$routes->delete('/', 'Organization\Site::delete');
|
||||
$routes->get('/', 'Organization\SiteController::index');
|
||||
$routes->get('(:num)', 'Organization\SiteController::show/$1');
|
||||
$routes->post('/', 'Organization\SiteController::create');
|
||||
$routes->patch('/', 'Organization\SiteController::update');
|
||||
$routes->delete('/', 'Organization\SiteController::delete');
|
||||
});
|
||||
|
||||
// Discipline
|
||||
$routes->group('discipline', function ($routes) {
|
||||
$routes->get('/', 'Organization\Discipline::index');
|
||||
$routes->get('(:num)', 'Organization\Discipline::show/$1');
|
||||
$routes->post('/', 'Organization\Discipline::create');
|
||||
$routes->patch('/', 'Organization\Discipline::update');
|
||||
$routes->delete('/', 'Organization\Discipline::delete');
|
||||
$routes->get('/', 'Organization\DisciplineController::index');
|
||||
$routes->get('(:num)', 'Organization\DisciplineController::show/$1');
|
||||
$routes->post('/', 'Organization\DisciplineController::create');
|
||||
$routes->patch('/', 'Organization\DisciplineController::update');
|
||||
$routes->delete('/', 'Organization\DisciplineController::delete');
|
||||
});
|
||||
|
||||
// Department
|
||||
$routes->group('department', function ($routes) {
|
||||
$routes->get('/', 'Organization\Department::index');
|
||||
$routes->get('(:num)', 'Organization\Department::show/$1');
|
||||
$routes->post('/', 'Organization\Department::create');
|
||||
$routes->patch('/', 'Organization\Department::update');
|
||||
$routes->delete('/', 'Organization\Department::delete');
|
||||
$routes->get('/', 'Organization\DepartmentController::index');
|
||||
$routes->get('(:num)', 'Organization\DepartmentController::show/$1');
|
||||
$routes->post('/', 'Organization\DepartmentController::create');
|
||||
$routes->patch('/', 'Organization\DepartmentController::update');
|
||||
$routes->delete('/', 'Organization\DepartmentController::delete');
|
||||
});
|
||||
|
||||
// Workstation
|
||||
$routes->group('workstation', function ($routes) {
|
||||
$routes->get('/', 'Organization\Workstation::index');
|
||||
$routes->get('(:num)', 'Organization\Workstation::show/$1');
|
||||
$routes->post('/', 'Organization\Workstation::create');
|
||||
$routes->patch('/', 'Organization\Workstation::update');
|
||||
$routes->delete('/', 'Organization\Workstation::delete');
|
||||
$routes->get('/', 'Organization\WorkstationController::index');
|
||||
$routes->get('(:num)', 'Organization\WorkstationController::show/$1');
|
||||
$routes->post('/', 'Organization\WorkstationController::create');
|
||||
$routes->patch('/', 'Organization\WorkstationController::update');
|
||||
$routes->delete('/', 'Organization\WorkstationController::delete');
|
||||
});
|
||||
});
|
||||
|
||||
// Specimen
|
||||
$routes->group('specimen', function ($routes) {
|
||||
$routes->group('containerdef', function ($routes) {
|
||||
$routes->get('/', 'Specimen\ContainerDef::index');
|
||||
$routes->get('(:num)', 'Specimen\ContainerDef::show/$1');
|
||||
$routes->post('/', 'Specimen\ContainerDef::create');
|
||||
$routes->patch('/', 'Specimen\ContainerDef::update');
|
||||
$routes->get('/', 'Specimen\ContainerDefController::index');
|
||||
$routes->get('(:num)', 'Specimen\ContainerDefController::show/$1');
|
||||
$routes->post('/', 'Specimen\ContainerDefController::create');
|
||||
$routes->patch('/', 'Specimen\ContainerDefController::update');
|
||||
});
|
||||
|
||||
$routes->group('prep', function ($routes) {
|
||||
$routes->get('/', 'Specimen\Prep::index');
|
||||
$routes->get('(:num)', 'Specimen\Prep::show/$1');
|
||||
$routes->post('/', 'Specimen\Prep::create');
|
||||
$routes->patch('/', 'Specimen\Prep::update');
|
||||
$routes->get('/', 'Specimen\SpecimenPrepController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenPrepController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenPrepController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenPrepController::update');
|
||||
});
|
||||
|
||||
$routes->group('status', function ($routes) {
|
||||
$routes->get('/', 'Specimen\Status::index');
|
||||
$routes->get('(:num)', 'Specimen\Status::show/$1');
|
||||
$routes->post('/', 'Specimen\Status::create');
|
||||
$routes->patch('/', 'Specimen\Status::update');
|
||||
$routes->get('/', 'Specimen\SpecimenStatusController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenStatusController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenStatusController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenStatusController::update');
|
||||
});
|
||||
|
||||
$routes->group('collection', function ($routes) {
|
||||
$routes->get('/', 'Specimen\Collection::index');
|
||||
$routes->get('(:num)', 'Specimen\Collection::show/$1');
|
||||
$routes->post('/', 'Specimen\Collection::create');
|
||||
$routes->patch('/', 'Specimen\Collection::update');
|
||||
$routes->get('/', 'Specimen\SpecimenCollectionController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenCollectionController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenCollectionController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenCollectionController::update');
|
||||
});
|
||||
|
||||
$routes->get('/', 'Specimen\Specimen::index');
|
||||
$routes->get('(:num)', 'Specimen\Specimen::show/$1');
|
||||
$routes->post('/', 'Specimen\Specimen::create');
|
||||
$routes->patch('/', 'Specimen\Specimen::update');
|
||||
$routes->get('/', 'Specimen\SpecimenController::index');
|
||||
$routes->get('(:num)', 'Specimen\SpecimenController::show/$1');
|
||||
$routes->post('/', 'Specimen\SpecimenController::create');
|
||||
$routes->patch('/', 'Specimen\SpecimenController::update');
|
||||
});
|
||||
|
||||
// Tests
|
||||
$routes->group('tests', function ($routes) {
|
||||
$routes->get('/', 'Tests::index');
|
||||
$routes->get('(:any)', 'Tests::show/$1');
|
||||
$routes->post('/', 'Tests::create');
|
||||
$routes->patch('/', 'Tests::update');
|
||||
$routes->get('/', 'TestsController::index');
|
||||
$routes->get('(:num)', 'TestsController::show/$1');
|
||||
$routes->post('/', 'TestsController::create');
|
||||
$routes->patch('/', 'TestsController::update');
|
||||
});
|
||||
|
||||
// Edge API - Integration with tiny-edge
|
||||
$routes->group('edge', function ($routes) {
|
||||
$routes->post('results', 'Edge::results');
|
||||
$routes->get('orders', 'Edge::orders');
|
||||
$routes->post('orders/(:num)/ack', 'Edge::ack/$1');
|
||||
$routes->post('status', 'Edge::status');
|
||||
$routes->post('results', 'EdgeController::results');
|
||||
$routes->get('orders', 'EdgeController::orders');
|
||||
$routes->post('orders/(:num)/ack', 'EdgeController::ack/$1');
|
||||
$routes->post('status', 'EdgeController::status');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\AreaGeoModel;
|
||||
|
||||
class AreaGeo extends BaseController {
|
||||
class AreaGeoController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -44,4 +44,4 @@ class AreaGeo extends BaseController {
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -12,23 +12,26 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Auth extends Controller {
|
||||
class AuthController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
// ok
|
||||
public function __construct() {
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
// ok
|
||||
public function checkAuth() {
|
||||
public function checkAuth()
|
||||
{
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Jika token FE tidak ada langsung kabarkan failed
|
||||
if (!$token) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'No token found'
|
||||
], 401);
|
||||
}
|
||||
@ -38,37 +41,37 @@ class Auth extends Controller {
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
|
||||
} catch (ExpiredException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Token expired',
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
|
||||
} catch (SignatureInvalidException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid token signature',
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
|
||||
} catch (BeforeValidException $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Token not valid yet',
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid token: ' . $e->getMessage(),
|
||||
'data' => []
|
||||
'data' => []
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
@ -122,7 +125,7 @@ class Auth extends Controller {
|
||||
// // 'httponly' => true, // dipakai agar cookie berikut tidak dapat diakses oleh javascript
|
||||
// // 'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
// ]);
|
||||
|
||||
|
||||
|
||||
// // Response tanpa token di body
|
||||
// return $this->respond([
|
||||
@ -131,7 +134,8 @@ class Auth extends Controller {
|
||||
// 'message' => 'Login successful'
|
||||
// ], 200);
|
||||
// }
|
||||
public function login() {
|
||||
public function login()
|
||||
{
|
||||
|
||||
// Ambil dari JSON Form dan Key .env
|
||||
$username = $this->request->getVar('username');
|
||||
@ -146,7 +150,9 @@ class Auth extends Controller {
|
||||
$query = $this->db->query($sql);
|
||||
$row = $query->getResultArray();
|
||||
|
||||
if (!$row) { return $this->fail('User not found.', 401); }
|
||||
if (!$row) {
|
||||
return $this->fail('User not found.', 401);
|
||||
}
|
||||
$row = $row[0];
|
||||
if (!password_verify($password, $row['password'])) {
|
||||
return $this->fail('Invalid password.', 401);
|
||||
@ -155,10 +161,10 @@ class Auth extends Controller {
|
||||
// Buat JWT payload
|
||||
$exp = time() + 864000;
|
||||
$payload = [
|
||||
'userid' => $row['id'],
|
||||
'userid' => $row['id'],
|
||||
'roleid' => $row['role_id'],
|
||||
'username' => $row['username'],
|
||||
'exp' => $exp
|
||||
'exp' => $exp
|
||||
];
|
||||
|
||||
try {
|
||||
@ -170,18 +176,18 @@ class Auth extends Controller {
|
||||
|
||||
// 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
|
||||
'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
|
||||
'httponly' => true, // dipakai agar cookie berikut tidak dapat diakses oleh javascript
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
]);
|
||||
|
||||
// Response tanpa token di body
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Login successful'
|
||||
], 200);
|
||||
@ -199,33 +205,35 @@ class Auth extends Controller {
|
||||
// 'secure' => $isSecure,
|
||||
// 'httponly' => true,
|
||||
// 'samesite' => $isSecure ? Cookie::SAMESITE_NONE : Cookie::SAMESITE_LAX
|
||||
|
||||
|
||||
// ])->setJSON([
|
||||
// 'status' => 'success',
|
||||
// 'code' => 200,
|
||||
// 'message' => 'Logout successful'
|
||||
// ], 200);
|
||||
// }
|
||||
public function logout() {
|
||||
public function logout()
|
||||
{
|
||||
// Definisikan ini pada cookies browser, harus sama dengan cookies login
|
||||
return $this->response->setCookie([
|
||||
'name' => 'token',
|
||||
'value' => '',
|
||||
'expire' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'name' => 'token',
|
||||
'value' => '',
|
||||
'expire' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => Cookie::SAMESITE_NONE
|
||||
|
||||
|
||||
])->setJSON([
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Logout successful'
|
||||
], 200);
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Logout successful'
|
||||
], 200);
|
||||
}
|
||||
|
||||
// ok
|
||||
public function register() {
|
||||
public function register()
|
||||
{
|
||||
|
||||
$username = strtolower($this->request->getJsonVar('username'));
|
||||
$password = $this->request->getJsonVar('password');
|
||||
@ -233,7 +241,7 @@ class Auth extends Controller {
|
||||
// Validasi Awal Dari BE
|
||||
if (empty($username) || empty($password)) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'status' => 'failed',
|
||||
'code' => 400,
|
||||
'message' => 'Username and password are required'
|
||||
], 400); // Gunakan 400 Bad Request
|
||||
@ -242,11 +250,11 @@ class Auth extends Controller {
|
||||
// Cek Duplikasi Username
|
||||
$exists = $this->db->query("SELECT id FROM users WHERE username = ?", [$username])->getRow();
|
||||
if ($exists) {
|
||||
return $this->respond(['status' => 'failed', 'code'=>409,'message' => 'Username already exists'], 409);
|
||||
return $this->respond(['status' => 'failed', 'code' => 409, 'message' => 'Username already exists'], 409);
|
||||
}
|
||||
|
||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
|
||||
// Mulai transaksi Insert
|
||||
$this->db->transStart();
|
||||
$this->db->query(
|
||||
@ -258,8 +266,8 @@ class Auth extends Controller {
|
||||
// Cek status transaksi
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'code' => 500,
|
||||
'status' => 'error',
|
||||
'code' => 500,
|
||||
'message' => 'Failed to create user. Please try again later.'
|
||||
], 500);
|
||||
}
|
||||
@ -269,7 +277,7 @@ class Auth extends Controller {
|
||||
'status' => 'success',
|
||||
'code' => 201,
|
||||
'message' => 'User ' . $username . ' successfully created.'
|
||||
], 201);
|
||||
], 201);
|
||||
|
||||
}
|
||||
|
||||
@ -294,19 +302,20 @@ class Auth extends Controller {
|
||||
// return $this->respond($response);
|
||||
// }
|
||||
|
||||
public function coba() {
|
||||
public function coba()
|
||||
{
|
||||
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Decode Token dengan Key yg ada di .env
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -18,7 +18,7 @@ use CodeIgniter\Cookie\Cookie;
|
||||
* Handles authentication for V2 UI
|
||||
* Separate from the main Auth controller to avoid conflicts
|
||||
*/
|
||||
class AuthV2 extends Controller
|
||||
class AuthV2Controller extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Contact\ContactModel;
|
||||
|
||||
class Contact extends BaseController {
|
||||
class ContactController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -76,4 +76,4 @@ class Contact extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Contact\MedicalSpecialtyModel;
|
||||
|
||||
class MedicalSpecialty extends BaseController {
|
||||
class MedicalSpecialtyController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -61,4 +61,4 @@ class MedicalSpecialty extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Contact\OccupationModel;
|
||||
|
||||
class Occupation extends BaseController {
|
||||
class OccupationController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -61,4 +61,4 @@ class Occupation extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\CounterModel;
|
||||
|
||||
class Counter extends BaseController {
|
||||
class CounterController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -62,4 +62,4 @@ class Counter extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,23 +12,25 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Sample extends Controller {
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
public function index()
|
||||
{
|
||||
|
||||
$token = $this->request->getCookie('token');
|
||||
$key = getenv('JWT_SECRET');
|
||||
$key = getenv('JWT_SECRET');
|
||||
|
||||
// Decode Token dengan Key yg ada di .env
|
||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'message' => 'Authenticated',
|
||||
'data' => $decodedPayload
|
||||
'data' => $decodedPayload
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -5,13 +5,15 @@ namespace App\Controllers;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class Edge extends Controller {
|
||||
class EdgeController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $edgeResModel;
|
||||
|
||||
public function __construct() {
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->edgeResModel = new \App\Models\EdgeResModel();
|
||||
}
|
||||
@ -20,7 +22,8 @@ class Edge extends Controller {
|
||||
* POST /api/edge/results
|
||||
* Receive results from tiny-edge
|
||||
*/
|
||||
public function results() {
|
||||
public function results()
|
||||
{
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
@ -70,7 +73,8 @@ class Edge extends Controller {
|
||||
* GET /api/edge/orders
|
||||
* Return pending orders for an instrument
|
||||
*/
|
||||
public function orders() {
|
||||
public function orders()
|
||||
{
|
||||
try {
|
||||
$instrumentId = $this->request->getGet('instrument');
|
||||
|
||||
@ -95,7 +99,8 @@ class Edge extends Controller {
|
||||
* POST /api/edge/orders/:id/ack
|
||||
* Acknowledge order delivery
|
||||
*/
|
||||
public function ack($orderId = null) {
|
||||
public function ack($orderId = null)
|
||||
{
|
||||
try {
|
||||
if (!$orderId) {
|
||||
return $this->failValidationErrors('Order ID is required');
|
||||
@ -129,7 +134,8 @@ class Edge extends Controller {
|
||||
* POST /api/edge/status
|
||||
* Log instrument status
|
||||
*/
|
||||
public function status() {
|
||||
public function status()
|
||||
{
|
||||
try {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
@ -12,7 +12,7 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Home extends Controller {
|
||||
class HomeController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Location\LocationModel;
|
||||
|
||||
class Location extends BaseController {
|
||||
class LocationController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -72,4 +72,4 @@ class Location extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
|
||||
class OrderTest extends Controller {
|
||||
class OrderTestController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function __construct() {
|
||||
@ -214,4 +214,4 @@ class OrderTest extends Controller {
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\AccountModel;
|
||||
|
||||
class Account extends BaseController {
|
||||
class AccountController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -76,4 +76,4 @@ class Account extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\DepartmentModel;
|
||||
|
||||
class Department extends BaseController {
|
||||
class DepartmentController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -72,4 +72,4 @@ class Department extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\DisciplineModel;
|
||||
|
||||
class Discipline extends BaseController {
|
||||
class DisciplineController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -79,4 +79,4 @@ class Discipline extends BaseController {
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\SiteModel;
|
||||
|
||||
class Site extends BaseController {
|
||||
class SiteController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -75,4 +75,4 @@ class Site extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\Organization\WorkstationModel;
|
||||
|
||||
class Workstation extends BaseController {
|
||||
class WorkstationController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -73,4 +73,4 @@ class Workstation extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use App\Controllers\BaseController;
|
||||
use App\Models\PatVisit\PatVisitModel;
|
||||
use App\Models\PatVisit\PatVisitADTModel;
|
||||
|
||||
class PatVisit extends BaseController {
|
||||
class PatVisitController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -82,4 +82,4 @@ class PatVisit extends BaseController {
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\Controller;
|
||||
|
||||
use App\Models\Patient\PatientModel;
|
||||
|
||||
class Patient extends Controller {
|
||||
class PatientController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -214,4 +214,4 @@ class Patient extends Controller {
|
||||
return $this->failServerError('Something went wrong.'.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Dashboard extends Controller {
|
||||
class ResultController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
@ -12,7 +12,7 @@ use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
|
||||
class Result extends Controller {
|
||||
class SampleController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
public function index() {
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\ContainerDefModel;
|
||||
|
||||
class ContainerDef extends BaseController {
|
||||
class ContainerDefController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -69,4 +69,4 @@ class ContainerDef extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\SpecimenCollectionModel;
|
||||
|
||||
class SpecimenCollection extends BaseController {
|
||||
class SpecimenCollectionController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -62,4 +62,4 @@ class SpecimenCollection extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\SpecimenModel;
|
||||
|
||||
class Specimen extends BaseController {
|
||||
class SpecimenController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -62,4 +62,4 @@ class Specimen extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Specimen\SpecimenPrepModel;
|
||||
|
||||
class SpecimenPrep extends BaseController {
|
||||
class SpecimenPrepController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -62,4 +62,4 @@ class SpecimenPrep extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -62,4 +62,4 @@ class ContainerDef extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\Test\TestMapModel;
|
||||
|
||||
class TestMap extends BaseController {
|
||||
class TestMapController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -53,4 +53,4 @@ class TestMap extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,523 +0,0 @@
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class Tests extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $rules;
|
||||
protected $model;
|
||||
protected $modelCal;
|
||||
protected $modelTech;
|
||||
protected $modelGrp;
|
||||
protected $modelMap;
|
||||
protected $modelValueSet;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->model = new \App\Models\Test\TestDefSiteModel;
|
||||
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
||||
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
||||
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
||||
$this->modelMap = new \App\Models\Test\TestMapModel;
|
||||
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
||||
|
||||
// Validation rules for main test definition
|
||||
$this->rules = [
|
||||
'TestSiteCode' => 'required|min_length[3]|max_length[6]',
|
||||
'TestSiteName' => 'required',
|
||||
'TestType' => 'required',
|
||||
'SiteID' => 'required'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests
|
||||
* GET /v1/tests/site
|
||||
* List all tests with optional filtering
|
||||
*/
|
||||
public function index() {
|
||||
$siteId = $this->request->getGet('SiteID');
|
||||
$testType = $this->request->getGet('TestType');
|
||||
$visibleScr = $this->request->getGet('VisibleScr');
|
||||
$visibleRpt = $this->request->getGet('VisibleRpt');
|
||||
$keyword = $this->request->getGet('TestSiteName');
|
||||
|
||||
$builder = $this->db->table('testdefsite')
|
||||
->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where('testdefsite.EndDate IS NULL');
|
||||
|
||||
if ($siteId) {
|
||||
$builder->where('testdefsite.SiteID', $siteId);
|
||||
}
|
||||
|
||||
if ($testType) {
|
||||
$builder->where('testdefsite.TestType', $testType);
|
||||
}
|
||||
|
||||
if ($visibleScr !== null) {
|
||||
$builder->where('testdefsite.VisibleScr', $visibleScr);
|
||||
}
|
||||
|
||||
if ($visibleRpt !== null) {
|
||||
$builder->where('testdefsite.VisibleRpt', $visibleRpt);
|
||||
}
|
||||
|
||||
if ($keyword) {
|
||||
$builder->like('testdefsite.TestSiteName', $keyword);
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('testdefsite.SeqScr', 'ASC')->get()->getResultArray();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests/{id}
|
||||
* GET /v1/tests/site/{id}
|
||||
* Get single test by ID with all related details
|
||||
*/
|
||||
public function show($id = null) {
|
||||
if (!$id) return $this->failValidationErrors('TestSiteID is required');
|
||||
|
||||
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where("testdefsite.TestSiteID", $id)
|
||||
->find($id);
|
||||
|
||||
if (!$row) {
|
||||
return $this->respond([ 'status' => 'success', 'message' => "No data.", 'data' => null ], 200);
|
||||
}
|
||||
|
||||
// Load related details based on TestType
|
||||
$typeCode = $row['TypeCode'] ?? '';
|
||||
|
||||
if ($typeCode === 'CALC') {
|
||||
// Load calculation details
|
||||
$row['testdefcal'] = $this->db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left')
|
||||
->where('testdefcal.TestSiteID', $id)
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
// Load group members with test details
|
||||
$row['testdefgrp'] = $this->db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $id)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'TITLE') {
|
||||
// Load test mappings only for TITLE type
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} else {
|
||||
// TEST or PARAM - load technical details
|
||||
$row['testdeftech'] = $this->db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $id)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
}
|
||||
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/tests
|
||||
* POST /v1/tests/site
|
||||
* Create new test definition
|
||||
*/
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (!$this->validateData($input, $this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Insert into Main Table (testdefsite)
|
||||
$testSiteData = [
|
||||
'SiteID' => $input['SiteID'],
|
||||
'TestSiteCode' => $input['TestSiteCode'],
|
||||
'TestSiteName' => $input['TestSiteName'],
|
||||
'TestType' => $input['TestType'],
|
||||
'Description' => $input['Description'] ?? null,
|
||||
'SeqScr' => $input['SeqScr'] ?? 0,
|
||||
'SeqRpt' => $input['SeqRpt'] ?? 0,
|
||||
'IndentLeft' => $input['IndentLeft'] ?? 0,
|
||||
'FontStyle' => $input['FontStyle'] ?? null,
|
||||
'VisibleScr' => $input['VisibleScr'] ?? 1,
|
||||
'VisibleRpt' => $input['VisibleRpt'] ?? 1,
|
||||
'CountStat' => $input['CountStat'] ?? 1,
|
||||
'StartDate' => $input['StartDate'] ?? date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$id = $this->model->insert($testSiteData);
|
||||
if (!$id) {
|
||||
throw new \Exception("Failed to insert main test definition");
|
||||
}
|
||||
|
||||
// 2. Handle Details based on TestType
|
||||
$this->handleDetails($id, $input, 'insert');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'created',
|
||||
'message' => "Test created successfully",
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT/PATCH /v1/tests/{id}
|
||||
* PUT/PATCH /v1/tests/site/{id}
|
||||
* Update existing test definition
|
||||
*/
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) { $id = $input["TestSiteID"]; }
|
||||
if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); }
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Update Main Table
|
||||
$testSiteData = [];
|
||||
$allowedUpdateFields = ['TestSiteCode', 'TestSiteName', 'TestType', 'Description',
|
||||
'SeqScr', 'SeqRpt', 'IndentLeft', 'FontStyle',
|
||||
'VisibleScr', 'VisibleRpt', 'CountStat', 'StartDate'];
|
||||
|
||||
foreach ($allowedUpdateFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$testSiteData[$field] = $input[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($testSiteData)) {
|
||||
$this->model->update($id, $testSiteData);
|
||||
}
|
||||
|
||||
// 2. Handle Details
|
||||
$this->handleDetails($id, $input, 'update');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test updated successfully",
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /v1/tests/{id}
|
||||
* DELETE /v1/tests/site/{id}
|
||||
* Soft delete test by setting EndDate
|
||||
*/
|
||||
public function delete($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) { $id = $input["TestSiteID"]; }
|
||||
if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); }
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
// Check if already disabled
|
||||
if (!empty($existing['EndDate'])) {
|
||||
return $this->failValidationErrors('Test is already disabled');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// 1. Soft delete main record
|
||||
$this->model->update($id, ['EndDate' => $now]);
|
||||
|
||||
// 2. Get TestType to handle related records
|
||||
$testType = $existing['TestType'];
|
||||
$vs = $this->modelValueSet->find($testType);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// 3. Soft delete related records based on TestType
|
||||
if ($typeCode === 'CALC') {
|
||||
$this->db->table('testdefcal')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
$this->db->table('testdeftech')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
}
|
||||
|
||||
// 4. Soft delete test mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test disabled successfully",
|
||||
'data' => ['TestSiteId' => $id, 'EndDate' => $now]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to handle inserting/updating sub-tables based on TestType
|
||||
*/
|
||||
private function handleDetails($testSiteID, $input, $action) {
|
||||
$testTypeID = $input['TestType'] ?? null;
|
||||
|
||||
// If update and TestType not in payload, fetch from DB
|
||||
if (!$testTypeID && $action === 'update') {
|
||||
$existing = $this->model->find($testSiteID);
|
||||
$testTypeID = $existing['TestType'] ?? null;
|
||||
}
|
||||
|
||||
if (!$testTypeID) return;
|
||||
|
||||
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
||||
$vs = $this->modelValueSet->find($testTypeID);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// Get details data from input
|
||||
$details = $input['details'] ?? $input;
|
||||
$details['TestSiteID'] = $testSiteID;
|
||||
$details['SiteID'] = $input['SiteID'] ?? 1;
|
||||
|
||||
switch ($typeCode) {
|
||||
case 'CALC':
|
||||
$this->saveCalcDetails($testSiteID, $details, $action);
|
||||
break;
|
||||
|
||||
case 'GROUP':
|
||||
$this->saveGroupDetails($testSiteID, $details, $input, $action);
|
||||
break;
|
||||
|
||||
case 'TITLE':
|
||||
// TITLE type only has testdefsite, no additional details needed
|
||||
// But we should save test mappings if provided
|
||||
if (isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TEST':
|
||||
case 'PARAM':
|
||||
default:
|
||||
$this->saveTechDetails($testSiteID, $details, $action, $typeCode);
|
||||
break;
|
||||
}
|
||||
|
||||
// Save test mappings for TEST and CALC types as well
|
||||
if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save technical details for TEST and PARAM types
|
||||
*/
|
||||
private function saveTechDetails($testSiteID, $data, $action, $typeCode) {
|
||||
$techData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'ResultType' => $data['ResultType'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? null,
|
||||
'VSet' => $data['VSet'] ?? null,
|
||||
'ReqQty' => $data['ReqQty'] ?? null,
|
||||
'ReqQtyUnit' => $data['ReqQtyUnit'] ?? null,
|
||||
'Unit1' => $data['Unit1'] ?? null,
|
||||
'Factor' => $data['Factor'] ?? null,
|
||||
'Unit2' => $data['Unit2'] ?? null,
|
||||
'Decimal' => $data['Decimal'] ?? 2,
|
||||
'CollReq' => $data['CollReq'] ?? null,
|
||||
'Method' => $data['Method'] ?? null,
|
||||
'ExpectedTAT' => $data['ExpectedTAT'] ?? null
|
||||
];
|
||||
|
||||
if ($action === 'update') {
|
||||
$exists = $this->db->table('testdeftech')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
$this->modelTech->update($exists['TestTechID'], $techData);
|
||||
} else {
|
||||
$this->modelTech->insert($techData);
|
||||
}
|
||||
} else {
|
||||
$this->modelTech->insert($techData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save calculation details for CALC type
|
||||
*/
|
||||
private function saveCalcDetails($testSiteID, $data, $action) {
|
||||
$calcData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'FormulaInput' => $data['FormulaInput'] ?? null,
|
||||
'FormulaCode' => $data['FormulaCode'] ?? $data['Formula'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? 'NMRC',
|
||||
'Unit1' => $data['Unit1'] ?? $data['ResultUnit'] ?? null,
|
||||
'Factor' => $data['Factor'] ?? null,
|
||||
'Unit2' => $data['Unit2'] ?? null,
|
||||
'Decimal' => $data['Decimal'] ?? 2,
|
||||
'Method' => $data['Method'] ?? null
|
||||
];
|
||||
|
||||
if ($action === 'update') {
|
||||
$exists = $this->db->table('testdefcal')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
$this->modelCal->update($exists['TestCalID'], $calcData);
|
||||
} else {
|
||||
$this->modelCal->insert($calcData);
|
||||
}
|
||||
} else {
|
||||
$this->modelCal->insert($calcData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save group details for GROUP type
|
||||
*/
|
||||
private function saveGroupDetails($testSiteID, $data, $input, $action) {
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing members
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
// Get members from details or input
|
||||
$members = $data['members'] ?? ($input['Members'] ?? []);
|
||||
|
||||
if (is_array($members)) {
|
||||
foreach ($members as $m) {
|
||||
$memberID = is_array($m) ? ($m['Member'] ?? ($m['TestSiteID'] ?? null)) : $m;
|
||||
if ($memberID) {
|
||||
$this->modelGrp->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'Member' => $memberID
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save test mappings
|
||||
*/
|
||||
private function saveTestMap($testSiteID, $mappings, $action) {
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
if (is_array($mappings)) {
|
||||
foreach ($mappings as $map) {
|
||||
$mapData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'HostType' => $map['HostType'] ?? null,
|
||||
'HostID' => $map['HostID'] ?? null,
|
||||
'HostDataSource' => $map['HostDataSource'] ?? null,
|
||||
'HostTestCode' => $map['HostTestCode'] ?? null,
|
||||
'HostTestName' => $map['HostTestName'] ?? null,
|
||||
'ClientType' => $map['ClientType'] ?? null,
|
||||
'ClientID' => $map['ClientID'] ?? null,
|
||||
'ClientDataSource' => $map['ClientDataSource'] ?? null,
|
||||
'ConDefID' => $map['ConDefID'] ?? null,
|
||||
'ClientTestCode' => $map['ClientTestCode'] ?? null,
|
||||
'ClientTestName' => $map['ClientTestName'] ?? null
|
||||
];
|
||||
$this->modelMap->insert($mapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
741
app/Controllers/TestsController.php
Normal file
741
app/Controllers/TestsController.php
Normal file
@ -0,0 +1,741 @@
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class TestsController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
protected $rules;
|
||||
protected $model;
|
||||
protected $modelCal;
|
||||
protected $modelTech;
|
||||
protected $modelGrp;
|
||||
protected $modelMap;
|
||||
protected $modelValueSet;
|
||||
protected $modelRefNum;
|
||||
protected $modelRefTxt;
|
||||
|
||||
// Valueset ID constants
|
||||
const VALUESET_REF_TYPE = 44; // testdeftech.RefType
|
||||
const VALUESET_RANGE_TYPE = 45; // refnum.RangeType
|
||||
const VALUESET_NUM_REF_TYPE = 46; // refnum.NumRefType
|
||||
const VALUESET_TXT_REF_TYPE = 47; // reftxt.TxtRefType
|
||||
const VALUESET_SEX = 3; // Sex values
|
||||
const VALUESET_MATH_SIGN = 41; // LowSign, HighSign
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->model = new \App\Models\Test\TestDefSiteModel;
|
||||
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
||||
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
||||
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
||||
$this->modelMap = new \App\Models\Test\TestMapModel;
|
||||
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
||||
$this->modelRefNum = new \App\Models\RefRange\RefNumModel;
|
||||
$this->modelRefTxt = new \App\Models\RefRange\RefTxtModel;
|
||||
|
||||
// Validation rules for main test definition
|
||||
$this->rules = [
|
||||
'TestSiteCode' => 'required|min_length[3]|max_length[6]',
|
||||
'TestSiteName' => 'required',
|
||||
'TestType' => 'required',
|
||||
'SiteID' => 'required'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests
|
||||
* GET /v1/tests/site
|
||||
* List all tests with optional filtering
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$siteId = $this->request->getGet('SiteID');
|
||||
$testType = $this->request->getGet('TestType');
|
||||
$visibleScr = $this->request->getGet('VisibleScr');
|
||||
$visibleRpt = $this->request->getGet('VisibleRpt');
|
||||
$keyword = $this->request->getGet('TestSiteName');
|
||||
|
||||
$builder = $this->db->table('testdefsite')
|
||||
->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where('testdefsite.EndDate IS NULL');
|
||||
|
||||
if ($siteId) {
|
||||
$builder->where('testdefsite.SiteID', $siteId);
|
||||
}
|
||||
|
||||
if ($testType) {
|
||||
$builder->where('testdefsite.TestType', $testType);
|
||||
}
|
||||
|
||||
if ($visibleScr !== null) {
|
||||
$builder->where('testdefsite.VisibleScr', $visibleScr);
|
||||
}
|
||||
|
||||
if ($visibleRpt !== null) {
|
||||
$builder->where('testdefsite.VisibleRpt', $visibleRpt);
|
||||
}
|
||||
|
||||
if ($keyword) {
|
||||
$builder->like('testdefsite.TestSiteName', $keyword);
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('testdefsite.SeqScr', 'ASC')->get()->getResultArray();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/tests/{id}
|
||||
* GET /v1/tests/site/{id}
|
||||
* Get single test by ID with all related details
|
||||
*/
|
||||
public function show($id = null)
|
||||
{
|
||||
if (!$id)
|
||||
return $this->failValidationErrors('TestSiteID is required');
|
||||
|
||||
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
->where("testdefsite.TestSiteID", $id)
|
||||
->find($id);
|
||||
|
||||
if (!$row) {
|
||||
return $this->respond(['status' => 'success', 'message' => "No data.", 'data' => null], 200);
|
||||
}
|
||||
|
||||
// Load related details based on TestType
|
||||
$typeCode = $row['TypeCode'] ?? '';
|
||||
|
||||
if ($typeCode === 'CALC') {
|
||||
// Load calculation details
|
||||
$row['testdefcal'] = $this->db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left')
|
||||
->where('testdefcal.TestSiteID', $id)
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
// Load group members with test details
|
||||
$row['testdefgrp'] = $this->db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $id)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} elseif ($typeCode === 'TITLE') {
|
||||
// Load test mappings only for TITLE type
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
} else {
|
||||
// TEST or PARAM - load technical details
|
||||
$row['testdeftech'] = $this->db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $id)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Load test mappings
|
||||
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||
|
||||
// Load refnum/reftxt based on RefType
|
||||
if (!empty($row['testdeftech'])) {
|
||||
$techData = $row['testdeftech'][0];
|
||||
$refType = (int) $techData['RefType'];
|
||||
|
||||
// Load refnum for NMRC type (RefType = 1)
|
||||
if ($refType === 1) {
|
||||
$refnumData = $this->modelRefNum
|
||||
->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('Display', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// Add VValue for display
|
||||
$row['refnum'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefNumID' => $r['RefNumID'],
|
||||
'NumRefType' => (int) $r['NumRefType'],
|
||||
'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']),
|
||||
'RangeType' => (int) $r['RangeType'],
|
||||
'RangeTypeVValue' => $this->getVValue(45, $r['RangeType']),
|
||||
'Sex' => (int) $r['Sex'],
|
||||
'SexVValue' => $this->getVValue(3, $r['Sex']),
|
||||
'AgeStart' => (int) $r['AgeStart'],
|
||||
'AgeEnd' => (int) $r['AgeEnd'],
|
||||
'LowSign' => $r['LowSign'] !== null ? (int) $r['LowSign'] : null,
|
||||
'LowSignVValue' => $this->getVValue(41, $r['LowSign']),
|
||||
'Low' => $r['Low'] !== null ? (int) $r['Low'] : null,
|
||||
'HighSign' => $r['HighSign'] !== null ? (int) $r['HighSign'] : null,
|
||||
'HighSignVValue' => $this->getVValue(41, $r['HighSign']),
|
||||
'High' => $r['High'] !== null ? (int) $r['High'] : null,
|
||||
'Flag' => $r['Flag']
|
||||
];
|
||||
}, $refnumData ?? []);
|
||||
|
||||
$row['numRefTypeOptions'] = $this->getValuesetOptions(46);
|
||||
$row['rangeTypeOptions'] = $this->getValuesetOptions(45);
|
||||
}
|
||||
|
||||
// Load reftxt for TEXT type (RefType = 2)
|
||||
if ($refType === 2) {
|
||||
$reftxtData = $this->modelRefTxt
|
||||
->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('RefTxtID', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$row['reftxt'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefTxtID' => $r['RefTxtID'],
|
||||
'TxtRefType' => (int) $r['TxtRefType'],
|
||||
'TxtRefTypeVValue' => $this->getVValue(47, $r['TxtRefType']),
|
||||
'Sex' => (int) $r['Sex'],
|
||||
'SexVValue' => $this->getVValue(3, $r['Sex']),
|
||||
'AgeStart' => (int) $r['AgeStart'],
|
||||
'AgeEnd' => (int) $r['AgeEnd'],
|
||||
'RefTxt' => $r['RefTxt'],
|
||||
'Flag' => $r['Flag']
|
||||
];
|
||||
}, $reftxtData ?? []);
|
||||
|
||||
$row['txtRefTypeOptions'] = $this->getValuesetOptions(47);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include valueset options for dropdowns
|
||||
$row['refTypeOptions'] = $this->getValuesetOptions(self::VALUESET_REF_TYPE);
|
||||
$row['sexOptions'] = $this->getValuesetOptions(self::VALUESET_SEX);
|
||||
$row['mathSignOptions'] = $this->getValuesetOptions(self::VALUESET_MATH_SIGN);
|
||||
|
||||
return $this->respond(['status' => 'success', 'message' => "Data fetched successfully", 'data' => $row], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/tests
|
||||
* POST /v1/tests/site
|
||||
* Create new test definition
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (!$this->validateData($input, $this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Insert into Main Table (testdefsite)
|
||||
$testSiteData = [
|
||||
'SiteID' => $input['SiteID'],
|
||||
'TestSiteCode' => $input['TestSiteCode'],
|
||||
'TestSiteName' => $input['TestSiteName'],
|
||||
'TestType' => $input['TestType'],
|
||||
'Description' => $input['Description'] ?? null,
|
||||
'SeqScr' => $input['SeqScr'] ?? 0,
|
||||
'SeqRpt' => $input['SeqRpt'] ?? 0,
|
||||
'IndentLeft' => $input['IndentLeft'] ?? 0,
|
||||
'FontStyle' => $input['FontStyle'] ?? null,
|
||||
'VisibleScr' => $input['VisibleScr'] ?? 1,
|
||||
'VisibleRpt' => $input['VisibleRpt'] ?? 1,
|
||||
'CountStat' => $input['CountStat'] ?? 1,
|
||||
'StartDate' => $input['StartDate'] ?? date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$id = $this->model->insert($testSiteData);
|
||||
if (!$id) {
|
||||
throw new \Exception("Failed to insert main test definition");
|
||||
}
|
||||
|
||||
// 2. Handle Details based on TestType
|
||||
$this->handleDetails($id, $input, 'insert');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'created',
|
||||
'message' => "Test created successfully",
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT/PATCH /v1/tests/{id}
|
||||
* PUT/PATCH /v1/tests/site/{id}
|
||||
* Update existing test definition
|
||||
*/
|
||||
public function update($id = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) {
|
||||
$id = $input["TestSiteID"];
|
||||
}
|
||||
if (!$id) {
|
||||
return $this->failValidationErrors('TestSiteID is required.');
|
||||
}
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
// 1. Update Main Table
|
||||
$testSiteData = [];
|
||||
$allowedUpdateFields = [
|
||||
'TestSiteCode',
|
||||
'TestSiteName',
|
||||
'TestType',
|
||||
'Description',
|
||||
'SeqScr',
|
||||
'SeqRpt',
|
||||
'IndentLeft',
|
||||
'FontStyle',
|
||||
'VisibleScr',
|
||||
'VisibleRpt',
|
||||
'CountStat',
|
||||
'StartDate'
|
||||
];
|
||||
|
||||
foreach ($allowedUpdateFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$testSiteData[$field] = $input[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($testSiteData)) {
|
||||
$this->model->update($id, $testSiteData);
|
||||
}
|
||||
|
||||
// 2. Handle Details
|
||||
$this->handleDetails($id, $input, 'update');
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test updated successfully",
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /v1/tests/{id}
|
||||
* DELETE /v1/tests/site/{id}
|
||||
* Soft delete test by setting EndDate
|
||||
*/
|
||||
public function delete($id = null)
|
||||
{
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
// Determine ID
|
||||
if (!$id && isset($input["TestSiteID"])) {
|
||||
$id = $input["TestSiteID"];
|
||||
}
|
||||
if (!$id) {
|
||||
return $this->failValidationErrors('TestSiteID is required.');
|
||||
}
|
||||
|
||||
// Verify record exists
|
||||
$existing = $this->model->find($id);
|
||||
if (!$existing) {
|
||||
return $this->failNotFound('Test not found');
|
||||
}
|
||||
|
||||
// Check if already disabled
|
||||
if (!empty($existing['EndDate'])) {
|
||||
return $this->failValidationErrors('Test is already disabled');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
try {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// 1. Soft delete main record
|
||||
$this->model->update($id, ['EndDate' => $now]);
|
||||
|
||||
// 2. Get TestType to handle related records
|
||||
$testType = $existing['TestType'];
|
||||
$vs = $this->modelValueSet->find($testType);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// 3. Soft delete related records based on TestType
|
||||
if ($typeCode === 'CALC') {
|
||||
$this->db->table('testdefcal')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
$this->db->table('testdeftech')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
|
||||
// Soft delete refnum and reftxt records
|
||||
$this->modelRefNum->where('TestSiteID', $id)->set('EndDate', $now)->update();
|
||||
$this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update();
|
||||
}
|
||||
|
||||
// 4. Soft delete test mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $id)
|
||||
->update(['EndDate' => $now]);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->failServerError('Transaction failed');
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test disabled successfully",
|
||||
'data' => ['TestSiteId' => $id, 'EndDate' => $now]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get valueset options
|
||||
*/
|
||||
private function getValuesetOptions($vsetID)
|
||||
{
|
||||
return $this->db->table('valueset')
|
||||
->select('VID as vid, VValue as vvalue, VDesc as vdesc')
|
||||
->where('VSetID', $vsetID)
|
||||
->orderBy('VOrder', 'ASC')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get VValue from VID for display
|
||||
*/
|
||||
private function getVValue($vsetID, $vid)
|
||||
{
|
||||
if ($vid === null || $vid === '')
|
||||
return null;
|
||||
$row = $this->db->table('valueset')
|
||||
->select('VValue as vvalue')
|
||||
->where('VSetID', $vsetID)
|
||||
->where('VID', (int) $vid)
|
||||
->get()->getRowArray();
|
||||
return $row ? $row['vvalue'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to handle inserting/updating sub-tables based on TestType
|
||||
*/
|
||||
private function handleDetails($testSiteID, $input, $action)
|
||||
{
|
||||
$testTypeID = $input['TestType'] ?? null;
|
||||
|
||||
// If update and TestType not in payload, fetch from DB
|
||||
if (!$testTypeID && $action === 'update') {
|
||||
$existing = $this->model->find($testSiteID);
|
||||
$testTypeID = $existing['TestType'] ?? null;
|
||||
}
|
||||
|
||||
if (!$testTypeID)
|
||||
return;
|
||||
|
||||
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
||||
$vs = $this->modelValueSet->find($testTypeID);
|
||||
$typeCode = $vs['VValue'] ?? '';
|
||||
|
||||
// Get details data from input
|
||||
$details = $input['details'] ?? $input;
|
||||
$details['TestSiteID'] = $testSiteID;
|
||||
$details['SiteID'] = $input['SiteID'] ?? 1;
|
||||
|
||||
switch ($typeCode) {
|
||||
case 'CALC':
|
||||
$this->saveCalcDetails($testSiteID, $details, $action);
|
||||
break;
|
||||
|
||||
case 'GROUP':
|
||||
$this->saveGroupDetails($testSiteID, $details, $input, $action);
|
||||
break;
|
||||
|
||||
case 'TITLE':
|
||||
// TITLE type only has testdefsite, no additional details needed
|
||||
// But we should save test mappings if provided
|
||||
if (isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TEST':
|
||||
case 'PARAM':
|
||||
default:
|
||||
$this->saveTechDetails($testSiteID, $details, $action, $typeCode);
|
||||
|
||||
// Save refnum/reftxt for TEST/PARAM types
|
||||
if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) {
|
||||
$refType = (int) $details['RefType'];
|
||||
|
||||
// Save refnum for NMRC type (RefType = 1)
|
||||
if ($refType === 1 && isset($input['refnum']) && is_array($input['refnum'])) {
|
||||
$this->saveRefNumRanges($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1);
|
||||
}
|
||||
|
||||
// Save reftxt for TEXT type (RefType = 2)
|
||||
if ($refType === 2 && isset($input['reftxt']) && is_array($input['reftxt'])) {
|
||||
$this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, $input['SiteID'] ?? 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Save test mappings for TEST and CALC types as well
|
||||
if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) {
|
||||
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save technical details for TEST and PARAM types
|
||||
*/
|
||||
private function saveTechDetails($testSiteID, $data, $action, $typeCode)
|
||||
{
|
||||
$techData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'ResultType' => $data['ResultType'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? null,
|
||||
'VSet' => $data['VSet'] ?? null,
|
||||
'ReqQty' => $data['ReqQty'] ?? null,
|
||||
'ReqQtyUnit' => $data['ReqQtyUnit'] ?? null,
|
||||
'Unit1' => $data['Unit1'] ?? null,
|
||||
'Factor' => $data['Factor'] ?? null,
|
||||
'Unit2' => $data['Unit2'] ?? null,
|
||||
'Decimal' => $data['Decimal'] ?? 2,
|
||||
'CollReq' => $data['CollReq'] ?? null,
|
||||
'Method' => $data['Method'] ?? null,
|
||||
'ExpectedTAT' => $data['ExpectedTAT'] ?? null
|
||||
];
|
||||
|
||||
if ($action === 'update') {
|
||||
$exists = $this->db->table('testdeftech')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
$this->modelTech->update($exists['TestTechID'], $techData);
|
||||
} else {
|
||||
$this->modelTech->insert($techData);
|
||||
}
|
||||
} else {
|
||||
$this->modelTech->insert($techData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save refnum ranges for NMRC type
|
||||
*/
|
||||
private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
$this->modelRefNum->where('TestSiteID', $testSiteID)
|
||||
->set('EndDate', date('Y-m-d H:i:s'))
|
||||
->update();
|
||||
}
|
||||
|
||||
foreach ($ranges as $index => $range) {
|
||||
$this->modelRefNum->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'NumRefType' => (int) $range['NumRefType'],
|
||||
'RangeType' => (int) $range['RangeType'],
|
||||
'Sex' => (int) $range['Sex'],
|
||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
||||
'LowSign' => !empty($range['LowSign']) ? (int) $range['LowSign'] : null,
|
||||
'Low' => !empty($range['Low']) ? (int) $range['Low'] : null,
|
||||
'HighSign' => !empty($range['HighSign']) ? (int) $range['HighSign'] : null,
|
||||
'High' => !empty($range['High']) ? (int) $range['High'] : null,
|
||||
'Flag' => $range['Flag'] ?? null,
|
||||
'Display' => $index,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save reftxt ranges for TEXT type
|
||||
*/
|
||||
private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
$this->modelRefTxt->where('TestSiteID', $testSiteID)
|
||||
->set('EndDate', date('Y-m-d H:i:s'))
|
||||
->update();
|
||||
}
|
||||
|
||||
foreach ($ranges as $range) {
|
||||
$this->modelRefTxt->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'TxtRefType' => (int) $range['TxtRefType'],
|
||||
'Sex' => (int) $range['Sex'],
|
||||
'AgeStart' => (int) ($range['AgeStart'] ?? 0),
|
||||
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
|
||||
'RefTxt' => $range['RefTxt'] ?? '',
|
||||
'Flag' => $range['Flag'] ?? null,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save calculation details for CALC type
|
||||
*/
|
||||
private function saveCalcDetails($testSiteID, $data, $action)
|
||||
{
|
||||
$calcData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'FormulaInput' => $data['FormulaInput'] ?? null,
|
||||
'FormulaCode' => $data['FormulaCode'] ?? $data['Formula'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? 'NMRC',
|
||||
'Unit1' => $data['Unit1'] ?? $data['ResultUnit'] ?? null,
|
||||
'Factor' => $data['Factor'] ?? null,
|
||||
'Unit2' => $data['Unit2'] ?? null,
|
||||
'Decimal' => $data['Decimal'] ?? 2,
|
||||
'Method' => $data['Method'] ?? null
|
||||
];
|
||||
|
||||
if ($action === 'update') {
|
||||
$exists = $this->db->table('testdefcal')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
$this->modelCal->update($exists['TestCalID'], $calcData);
|
||||
} else {
|
||||
$this->modelCal->insert($calcData);
|
||||
}
|
||||
} else {
|
||||
$this->modelCal->insert($calcData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save group details for GROUP type
|
||||
*/
|
||||
private function saveGroupDetails($testSiteID, $data, $input, $action)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing members
|
||||
$this->db->table('testdefgrp')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
// Get members from details or input
|
||||
$members = $data['members'] ?? ($input['Members'] ?? []);
|
||||
|
||||
if (is_array($members)) {
|
||||
foreach ($members as $m) {
|
||||
$memberID = is_array($m) ? ($m['Member'] ?? ($m['TestSiteID'] ?? null)) : $m;
|
||||
if ($memberID) {
|
||||
$this->modelGrp->insert([
|
||||
'TestSiteID' => $testSiteID,
|
||||
'Member' => $memberID
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save test mappings
|
||||
*/
|
||||
private function saveTestMap($testSiteID, $mappings, $action)
|
||||
{
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing mappings
|
||||
$this->db->table('testmap')
|
||||
->where('TestSiteID', $testSiteID)
|
||||
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
if (is_array($mappings)) {
|
||||
foreach ($mappings as $map) {
|
||||
$mapData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'HostType' => $map['HostType'] ?? null,
|
||||
'HostID' => $map['HostID'] ?? null,
|
||||
'HostDataSource' => $map['HostDataSource'] ?? null,
|
||||
'HostTestCode' => $map['HostTestCode'] ?? null,
|
||||
'HostTestName' => $map['HostTestName'] ?? null,
|
||||
'ClientType' => $map['ClientType'] ?? null,
|
||||
'ClientID' => $map['ClientID'] ?? null,
|
||||
'ClientDataSource' => $map['ClientDataSource'] ?? null,
|
||||
'ConDefID' => $map['ConDefID'] ?? null,
|
||||
'ClientTestCode' => $map['ClientTestCode'] ?? null,
|
||||
'ClientTestName' => $map['ClientTestName'] ?? null
|
||||
];
|
||||
$this->modelMap->insert($mapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ValueSet\ValueSetModel;
|
||||
|
||||
class ValueSet extends BaseController {
|
||||
class ValueSetController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -94,4 +94,4 @@ class ValueSet extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ValueSet\ValueSetDefModel;
|
||||
|
||||
class ValueSetDef extends BaseController {
|
||||
class ValueSetDefController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $db;
|
||||
@ -70,4 +70,4 @@ class ValueSetDef extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SyncCRM\ZonesModel;
|
||||
|
||||
class Zones extends BaseController {
|
||||
class ZonesController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
@ -92,4 +92,4 @@ class Zones extends BaseController {
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
*/
|
||||
@ -4,17 +4,36 @@ namespace App\Models\RefRange;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class RefNumModel extends BaseModel {
|
||||
class RefNumModel extends BaseModel
|
||||
{
|
||||
protected $table = 'refnum';
|
||||
protected $primaryKey = 'RefNumID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'SpcType', 'Sex', 'AgeStart', 'AgeEnd',
|
||||
'CriticalLow', 'Low', 'High', 'CriticalHigh',
|
||||
'CreateDate', 'EndDate'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteID',
|
||||
'SpcType',
|
||||
'Sex',
|
||||
'Criteria',
|
||||
'AgeStart',
|
||||
'AgeEnd',
|
||||
'NumRefType',
|
||||
'RangeType',
|
||||
'LowSign',
|
||||
'Low',
|
||||
'HighSign',
|
||||
'High',
|
||||
'Display',
|
||||
'Flag',
|
||||
'Interpretation',
|
||||
'Notes',
|
||||
'CreateDate',
|
||||
'StartDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
33
app/Models/RefRange/RefTxtModel.php
Normal file
33
app/Models/RefRange/RefTxtModel.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\RefRange;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class RefTxtModel extends BaseModel
|
||||
{
|
||||
protected $table = 'reftxt';
|
||||
protected $primaryKey = 'RefTxtID';
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteID',
|
||||
'SpcType',
|
||||
'Sex',
|
||||
'Criteria',
|
||||
'AgeStart',
|
||||
'AgeEnd',
|
||||
'TxtRefType',
|
||||
'RefTxt',
|
||||
'Flag',
|
||||
'Notes',
|
||||
'CreateDate',
|
||||
'StartDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
}
|
||||
@ -1,257 +1,375 @@
|
||||
<!-- Calculated Test Form Modal -->
|
||||
<div
|
||||
x-show="showModal && currentDialogType === 'CALC'"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Calculation Dialog (for CALC type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'CALC' || form.TypeCode === 'CALC')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-calculator" style="color: rgb(var(--color-secondary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Calculated Test' : 'New Calculated Test'"></span>
|
||||
</h3>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center">
|
||||
<i class="fa-solid fa-calculator text-amber-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Calculated Test' : 'New Calculated Test'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Derived/Calculated Test Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Calculation Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-warning gap-1">
|
||||
<i class="fa-solid fa-calculator"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'formula' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'formula'">
|
||||
<i class="fa-solid fa-calculator mr-1"></i> Formula
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-flask mr-1"></i> Results
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'reference' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'reference'">
|
||||
<i class="fa-solid fa-ruler-combined mr-1"></i> Ref
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName"
|
||||
placeholder="Absolute Neutrophils"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteName"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.TestSiteCode && 'input-error'"
|
||||
x-model="form.TestSiteCode"
|
||||
placeholder="ANC"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Type & Result Unit -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Type</span>
|
||||
</label>
|
||||
<input type="text" class="input input-disabled bg-base-200" x-model="form.TestTypeName" readonly />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Result Unit</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Unit1"
|
||||
placeholder="% or cells/µL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab: Basic Information (includes Org and Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Formula Section -->
|
||||
<div class="border rounded-xl p-4 bg-base-50">
|
||||
<h4 class="font-semibold flex items-center gap-2 mb-4">
|
||||
<i class="fa-solid fa-function"></i>
|
||||
Calculation Formula
|
||||
</h4>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-amber-500"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Code <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode"
|
||||
placeholder="e.g., BMI, eGFR, LDL_C" maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Body Mass Index, eGFR" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Test name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Formula Expression <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
<span class="label-text-alt text-base-content/60">Use test codes as variables in curly braces</span>
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input font-mono h-20"
|
||||
:class="errors.FormulaCode && 'input-error'"
|
||||
x-model="form.FormulaCode"
|
||||
placeholder="({WBC} * {NEU%}) / 100"
|
||||
></textarea>
|
||||
<label class="label" x-show="errors.FormulaCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.FormulaCode"></span>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="e.g., Calculated based on weight and height..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-building text-amber-500"></i> Organization
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Discipline</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<option value="1">Hematology</option>
|
||||
<option value="2">Chemistry</option>
|
||||
<option value="3">Microbiology</option>
|
||||
<option value="4">Urinalysis</option>
|
||||
<option value="10">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Department</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<option value="1">Lab Hematology</option>
|
||||
<option value="2">Lab Chemistry</option>
|
||||
<option value="3">Lab Microbiology</option>
|
||||
<option value="4">Lab Urinalysis</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-amber-500"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formula Variables/Tests -->
|
||||
<!-- Tab: Formula Configuration -->
|
||||
<div x-show="activeTab === 'formula'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Input Parameters</span>
|
||||
<span class="label-text-alt text-base-content/60">Tests referenced in formula</span>
|
||||
<span class="label-text font-medium text-sm">Formula Input Variables <span
|
||||
class="text-error">*</span></span>
|
||||
<span class="label-text-alt text-xs">Comma-separated test codes (these become variable names)</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-if="!form.FormulaInput || form.FormulaInput.length === 0">
|
||||
<span class="text-sm text-base-content/50 italic">No parameters defined.</span>
|
||||
</template>
|
||||
<template x-for="(v, idx) in (form.FormulaInput ? form.FormulaInput.split('^') : [])" :key="idx">
|
||||
<span class="badge badge-primary gap-1">
|
||||
<code x-text="v"></code>
|
||||
</span>
|
||||
</template>
|
||||
<input type="text" class="input input-bordered font-mono w-full" x-model="form.FormulaInput"
|
||||
placeholder="e.g., WEIGHT,HEIGHT,AGE,SCR" />
|
||||
<p class="text-xs mt-1" style="color: rgb(var(--color-text-muted));">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i>
|
||||
Enter test codes that will be used as input variables. Use comma to separate multiple variables.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Formula Expression <span class="text-error">*</span></span>
|
||||
<span class="label-text-alt text-xs">JavaScript expression using variable names</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered font-mono text-sm w-full" x-model="form.FormulaCode"
|
||||
placeholder="e.g., WEIGHT / ((HEIGHT/100) * (HEIGHT/100))" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Available Functions Help -->
|
||||
<div class="p-3 rounded bg-opacity-20" style="background: rgb(var(--color-bg-tertiary));">
|
||||
<h5 class="font-semibold text-sm mb-2">Available Functions:</h5>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs font-mono" style="color: rgb(var(--color-text-muted));">
|
||||
<div><code>ABS(x)</code> - Absolute value</div>
|
||||
<div><code>ROUND(x, d)</code> - Round to d decimals</div>
|
||||
<div><code>MIN(a, b, ...)</code> - Minimum value</div>
|
||||
<div><code>MAX(a, b, ...)</code> - Maximum value</div>
|
||||
<div><code>IF(cond, t, f)</code> - Conditional</div>
|
||||
<div><code>MEAN(a, b, ...)</code> - Average</div>
|
||||
<div><code>SQRT(x)</code> - Square root</div>
|
||||
<div><code>POW(x, y)</code> - Power (x^y)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Variable -->
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-bordered flex-1" x-model="form.newFormulaVar">
|
||||
<option value="">Select test variable...</option>
|
||||
<template x-for="(t, idx) in availableTests" :key="idx">
|
||||
<option :value="t.TestSiteCode" x-text="t.TestSiteCode + ' - ' + t.TestSiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button class="btn btn-outline" @click="addFormulaVar()">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Options -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Decimal Places</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input text-center"
|
||||
x-model="form.Decimal"
|
||||
min="0"
|
||||
max="10"
|
||||
placeholder="2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Conversion Factor</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Factor"
|
||||
placeholder="Optional conversion"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Secondary Unit</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Unit2"
|
||||
placeholder="Optional secondary unit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-16 pt-2"
|
||||
x-model="form.Description"
|
||||
placeholder="Calculation description..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Sequence & Site -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqScr" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqRpt" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
<!-- Formula Preview -->
|
||||
<div class="p-3 rounded border"
|
||||
style="background: rgb(var(--color-bg-secondary)); border-color: rgb(var(--color-border));">
|
||||
<h5 class="font-semibold text-sm mb-2 flex items-center gap-2">
|
||||
<i class="fa-solid fa-eye text-amber-500"></i>
|
||||
Formula Preview
|
||||
</h5>
|
||||
<template x-if="form.FormulaInput || form.FormulaCode">
|
||||
<div class="font-mono text-sm space-y-1">
|
||||
<div class="flex gap-2">
|
||||
<span class="opacity-60">Inputs:</span>
|
||||
<span x-text="form.FormulaInput || '(none)'"></span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="opacity-60">Formula:</span>
|
||||
<code x-text="form.FormulaCode || '(none)'"></code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</select>
|
||||
<template x-if="!form.FormulaInput && !form.FormulaCode">
|
||||
<span class="text-sm opacity-50 italic">Enter formula inputs and expression above</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="flex items-center gap-6 p-4 rounded-xl border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Screen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Report</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.CountStat" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Count in Statistics</span>
|
||||
</label>
|
||||
<!-- Tab: Result Configuration (includes Sample) -->
|
||||
<div x-show="activeTab === 'results'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Result Configuration -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask text-amber-500"></i> Result Configuration
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Result Unit</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit1"
|
||||
placeholder="e.g., kg/m2, mg/dL, %" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Decimal Places</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.Decimal" placeholder="2"
|
||||
min="0" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Expected TAT (min)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.ExpectedTAT"
|
||||
placeholder="5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-amber-500"></i> Sample
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., Automated calculation" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Reference Range -->
|
||||
<div x-show="activeTab === 'reference'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="25"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-secondary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-warning flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Test'"></span>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Calculated Test' : 'Create Calculated Test')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,211 +0,0 @@
|
||||
<!-- Group Test Form Modal -->
|
||||
<div
|
||||
x-show="showModal && currentDialogType === 'GROUP'"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-5 max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-layer-group" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Test Group' : 'New Test Group'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-3">
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Group Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered"
|
||||
:class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName"
|
||||
placeholder="CBC Panel"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Group Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered font-mono"
|
||||
:class="errors.TestSiteCode && 'input-error'"
|
||||
x-model="form.TestSiteCode"
|
||||
placeholder="CBC"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Type & Default Specimen -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Test Type</span>
|
||||
</label>
|
||||
<input type="text" class="input input-sm input-bordered bg-base-200" x-model="form.TestTypeName" readonly />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Default Specimen</span>
|
||||
</label>
|
||||
<select class="select select-sm select-bordered" x-model="form.SpcType">
|
||||
<option value="">Select</option>
|
||||
<template x-for="s in specimenTypesList" :key="s.VID || s.id">
|
||||
<option :value="s.VValue" x-text="s.VDesc || s.VValue"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Members Selection -->
|
||||
<div class="border rounded-lg p-3 bg-base-50">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-semibold text-sm flex items-center gap-1">
|
||||
<i class="fa-solid fa-list-check text-sm"></i>
|
||||
Group Members
|
||||
</h4>
|
||||
<button class="btn btn-primary btn-xs" @click="openTestSelector()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Selected Members List -->
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<template x-if="!form.members || form.members.length === 0">
|
||||
<div class="text-center py-4 text-base-content/50">
|
||||
<i class="fa-solid fa-inbox text-xl mb-1"></i>
|
||||
<p class="text-xs">No tests added</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-for="(member, index) in form.members" :key="index">
|
||||
<div class="grid grid-cols-[1fr_auto] gap-2 p-2 bg-white rounded border items-center">
|
||||
<span class="text-xs font-medium truncate" x-text="member.TestSiteCode+' - '+member.TestSiteName"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="number" class="input input-xs py-1 text-center w-8" x-model="member.SeqScr" placeholder="#" title="Order"/>
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error" @click="removeMember(index)">
|
||||
<i class="fa-solid fa-times text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequence & Site - Very Compact -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-base-content/60">Scr:</span>
|
||||
<input type="number" class="input input-xs w-12 text-center" x-model="form.SeqScr" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-base-content/60">Rpt:</span>
|
||||
<input type="number" class="input input-xs w-12 text-center" x-model="form.SeqRpt" />
|
||||
</div>
|
||||
<select class="select select-xs flex-1" x-model="form.SiteID">
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Options - Compact -->
|
||||
<div class="flex items-center gap-3 p-2 rounded border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" x-model="form.CountStat" :true-value="1" :false-value="0" />
|
||||
<span class="label-text text-xs">Stats</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 mt-4 pt-3" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-sm btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-xs"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-1"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Selector Modal -->
|
||||
<div x-show="showTestSelector" x-cloak class="modal-overlay" style="z-index: 100;">
|
||||
<div class="modal-content p-4 max-w-lg" @click.stop>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-bold">Select Tests</h4>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="showTestSelector = false">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm mb-2"
|
||||
placeholder="Search tests..."
|
||||
x-model="testSearch"
|
||||
/>
|
||||
|
||||
<div class="max-h-48 overflow-y-auto space-y-1">
|
||||
<template x-for="test in availableTests" :key="test.TestSiteID">
|
||||
<label class="flex items-center gap-2 p-2 hover:bg-base-200 rounded-lg cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
:checked="isTestSelected(test.TestSiteID)"
|
||||
@change="toggleTestSelection(test)"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm" x-text="test.TestSiteName"></div>
|
||||
<div class="text-xs text-base-content/60 font-mono" x-text="test.TestSiteCode"></div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-3 pt-2" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-sm btn-ghost flex-1" @click="showTestSelector = false">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary flex-1" @click="confirmTestSelection()">
|
||||
<i class="fa-solid fa-check mr-1"></i> Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
305
app/Views/v2/master/tests/grp_dialog.php
Normal file
305
app/Views/v2/master/tests/grp_dialog.php
Normal file
@ -0,0 +1,305 @@
|
||||
<!-- Group Dialog (for GROUP type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'GROUP' || form.TypeCode === 'GROUP')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary bg-opacity-20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-layer-group text-primary text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Test Group' : 'New Test Group'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Group/Panel Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Group Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-primary gap-1">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-primary text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'members' ? 'bg-primary text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'members'">
|
||||
<i class="fa-solid fa-users mr-1"></i> Members
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Tab: Basic Information (includes Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-primary"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Group Code <span class="text-error">*</span></span>
|
||||
<span class="label-text-alt text-xs">Auto-generated from name</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode" placeholder="Auto-generated"
|
||||
maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Group Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Lipid Profile, CBC Panel" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Group name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="e.g., Comprehensive lipid analysis panel..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-primary"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Group Members -->
|
||||
<div x-show="activeTab === 'members'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-primary bg-opacity-10"
|
||||
style="border: 1px solid rgb(var(--color-primary));">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-users text-primary"></i>
|
||||
<span class="font-medium text-sm">Group Members (<span
|
||||
x-text="form.groupMembers?.length || 0"></span>)</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" @click="showMemberSelector = true">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Member List -->
|
||||
<template x-if="!form.groupMembers || form.groupMembers.length === 0">
|
||||
<div class="text-center py-8 rounded-lg border border-dashed"
|
||||
style="border-color: rgb(var(--color-border));">
|
||||
<i class="fa-solid fa-inbox text-3xl opacity-40 mb-2"></i>
|
||||
<p class="opacity-60">No members added yet</p>
|
||||
<p class="text-xs opacity-50">Click "Add Member" to add tests to this group</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="form.groupMembers && form.groupMembers.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Seq</th>
|
||||
<th class="w-10">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(member, index) in form.groupMembers" :key="index">
|
||||
<tr class="hover">
|
||||
<td><code class="text-xs" x-text="member.TestSiteCode"></code></td>
|
||||
<td x-text="member.TestSiteName"></td>
|
||||
<td>
|
||||
<span class="badge badge-xs" :class="{
|
||||
'badge-info': member.MemberTypeCode === 'TEST',
|
||||
'badge-success': member.MemberTypeCode === 'PARAM'
|
||||
}" x-text="member.MemberTypeCode || 'TEST'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="input input-xs w-16" x-model.number="member.SeqScr"
|
||||
placeholder="0" />
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error" @click="removeMember(index)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Quick Add Common Tests -->
|
||||
<div class="p-3 rounded-lg border border-dashed" style="border-color: rgb(var(--color-border));">
|
||||
<h4 class="font-medium text-sm mb-2 flex items-center gap-2">
|
||||
<i class="fa-solid fa-bolt text-amber-500"></i>
|
||||
Quick Add Common Tests
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HBA1C', 'TEST')">HbA1c</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('GLU_R', 'TEST')">Glucose (Random)</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('GLU_F', 'TEST')">Glucose
|
||||
(Fasting)</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('CHOL', 'TEST')">Cholesterol</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('TG', 'TEST')">Triglycerides</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HDL', 'TEST')">HDL</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('LDL', 'TEST')">LDL</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('VLDL', 'TEST')">VLDL</button>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('RBC', 'PARAM')">RBC</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('WBC', 'PARAM')">WBC</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HGB', 'PARAM')">HGB</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('HCT', 'PARAM')">HCT</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('PLT', 'PARAM')">PLT</button>
|
||||
<button class="btn btn-xs btn-outline" @click="addCommonMember('MCV', 'PARAM')">MCV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Member Selector Modal (outside tabs) -->
|
||||
<div x-show="showMemberSelector" x-cloak class="modal-overlay" x-transition>
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[80vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="font-bold text-lg">Select Test Members</h4>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="showMemberSelector = false">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<input type="text" class="input input-bordered w-full" placeholder="Search tests..." x-model="memberSearch" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<template
|
||||
x-for="test in availableTests.filter(t => t.TestSiteName?.toLowerCase().includes(memberSearch?.toLowerCase() || ''))"
|
||||
:key="test.TestSiteID">
|
||||
<label
|
||||
class="flex items-center gap-3 p-3 rounded-lg border cursor-pointer hover:bg-opacity-50 transition-colors"
|
||||
style="background: rgb(var(--color-bg-secondary)); border-color: rgb(var(--color-border));">
|
||||
<input type="checkbox" class="checkbox checkbox-sm"
|
||||
:checked="form.groupMembers?.some(m => m.TestSiteID === test.TestSiteID)"
|
||||
@change="toggleMember(test)" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium" x-text="test.TestSiteName"></div>
|
||||
<div class="text-xs flex items-center gap-2">
|
||||
<code x-text="test.TestSiteCode"></code>
|
||||
<span class="badge badge-xs" :class="{
|
||||
'badge-info': test.TypeCode === 'TEST',
|
||||
'badge-success': test.TypeCode === 'PARAM'
|
||||
}" x-text="test.TypeCode"></span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost" @click="showMemberSelector = false">Cancel</button>
|
||||
<button class="btn btn-primary" @click="showMemberSelector = false; $forceUpdate()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Group' : 'Create Group')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,223 +1,417 @@
|
||||
<!-- Parameter Test Form Modal -->
|
||||
<div
|
||||
x-show="showModal && currentDialogType === 'PARAM'"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Parameter Dialog (for PARAM type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'PARAM' || form.TypeCode === 'PARAM')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-flask" style="color: rgb(var(--color-info));"></i>
|
||||
<span x-text="isEditing ? 'Edit Parameter Test' : 'New Parameter Test'"></span>
|
||||
</h3>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<i class="fa-solid fa-sliders text-emerald-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Parameter' : 'New Parameter'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Parameter/Component Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Parameter Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-success gap-1">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-flask mr-1"></i> Results
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'reference' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'reference'">
|
||||
<i class="fa-solid fa-ruler-combined mr-1"></i> Ref
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'valueset' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'valueset'">
|
||||
<i class="fa-solid fa-list-ul mr-1"></i> VSet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName"
|
||||
placeholder="WBC Count"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteName"></span>
|
||||
</label>
|
||||
|
||||
<!-- Tab: Basic Information (includes Org, Sample, and Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-emerald-500"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Parameter Code <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode"
|
||||
placeholder="e.g., RBC, WBC, HGB" maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Parameter Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Red Blood Cell Count" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Parameter name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="Optional description..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-building text-emerald-500"></i> Organization
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Discipline</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<option value="1">Hematology</option>
|
||||
<option value="2">Chemistry</option>
|
||||
<option value="3">Microbiology</option>
|
||||
<option value="4">Urinalysis</option>
|
||||
<option value="5">Immunology</option>
|
||||
<option value="6">Serology</option>
|
||||
<option value="10">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Department</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<option value="1">Lab Hematology</option>
|
||||
<option value="2">Lab Chemistry</option>
|
||||
<option value="3">Lab Microbiology</option>
|
||||
<option value="4">Lab Urinalysis</option>
|
||||
<option value="5">Lab Immunology</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-emerald-500"></i> Sample
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., Automated Cell Counter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.TestSiteCode && 'input-error'"
|
||||
x-model="form.TestSiteCode"
|
||||
placeholder="WBC"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-emerald-500"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Type & Method -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Type</span>
|
||||
</label>
|
||||
<input type="text" class="input input-disabled bg-base-200" x-model="form.TestTypeName" readonly />
|
||||
<!-- Tab: Result Configuration (includes Sample & Method) -->
|
||||
<div x-show="activeTab === 'results'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Result Configuration -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask text-emerald-500"></i> Result Configuration
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Result Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.ResultType">
|
||||
<option value="">Select Result Type</option>
|
||||
<option value="NMRIC">Numeric</option>
|
||||
<option value="TEXT">Text</option>
|
||||
<option value="VSET">Value Set (Select)</option>
|
||||
<option value="RANGE">Range with Reference</option>
|
||||
<option value="DTTM">Date/Time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 1</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit1"
|
||||
placeholder="e.g., 10^6/µL, g/dL" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 2 (SI)</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit2"
|
||||
placeholder="e.g., 10^12/L, g/L" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Decimal Places</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.Decimal" placeholder="2"
|
||||
min="0" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Expected TAT (min)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.ExpectedTAT"
|
||||
placeholder="30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample & Method -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Method</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.Method">
|
||||
<option value="">Select Method</option>
|
||||
<template x-for="m in methodsList" :key="m.VID">
|
||||
<option :value="m.VValue" x-text="m.VDesc || m.VValue"></option>
|
||||
</template>
|
||||
</select>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-emerald-500"></i> Sample & Method
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., Automated Cell Counter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Specimen & Container -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Specimen Type</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SpcType">
|
||||
<option value="">Select Specimen</option>
|
||||
<template x-for="s in specimenTypesList" :key="s.VID || s.id">
|
||||
<option :value="s.VValue" x-text="s.VDesc || s.VValue"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Container</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.ConDefID">
|
||||
<option value="">Select Container</option>
|
||||
<template x-for="c in containersList" :key="c.ConDefID || c.id">
|
||||
<option :value="c.ConDefID" x-text="c.ConCode || c.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Volume & Unit -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Volume Required</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
class="input"
|
||||
x-model="form.VolumeRequired"
|
||||
placeholder="2.0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Volume Unit</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.VolumeUnit">
|
||||
<option value="mL">mL</option>
|
||||
<option value="uL">µL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Result Unit</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ResultUnit"
|
||||
placeholder="cells/µL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-20 pt-2"
|
||||
x-model="form.Description"
|
||||
placeholder="Test description..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Sequence & Visibility -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<!-- Tab: Reference Range -->
|
||||
<div x-show="activeTab === 'reference'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Screen)</span>
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqScr" />
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Report)</span>
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqRpt" />
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow"
|
||||
placeholder="Alert low value" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh"
|
||||
placeholder="Alert high value" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
<span class="label-text font-medium text-sm">Reference Text (for text-based reference)</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText"
|
||||
placeholder="e.g., Negative/Positive, Normal/Abnormal" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="flex items-center gap-6 p-4 rounded-xl border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Screen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Report</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.CountStat" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Count in Statistics</span>
|
||||
</label>
|
||||
<!-- Tab: Value Set Selection -->
|
||||
<div x-show="activeTab === 'valueset'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<template x-if="form.ResultType === 'VSET'">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Value Set</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.ValueSetID">
|
||||
<option value="">Select Value Set</option>
|
||||
<option value="1">Positive/Negative</option>
|
||||
<option value="2">+1 to +4</option>
|
||||
<option value="3">Absent/Present</option>
|
||||
<option value="4">Normal/Abnormal</option>
|
||||
<option value="5">Trace/+/++/+++</option>
|
||||
<option value="6">Yes/No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Default Value</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.DefaultValue"
|
||||
placeholder="Default selection" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="form.ResultType !== 'VSET'">
|
||||
<div class="p-8 text-center rounded-lg border bg-opacity-30"
|
||||
style="background: rgb(var(--color-bg-secondary)); border-color: rgb(var(--color-border));">
|
||||
<i class="fa-solid fa-list-ul text-4xl opacity-40 mb-2"></i>
|
||||
<p class="opacity-60">Value Set configuration is only available when Result Type is "Value Set (Select)"</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-info flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-success flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Test'"></span>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Parameter' : 'Create Parameter')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,262 +1,375 @@
|
||||
<!-- Lab Test Form Modal -->
|
||||
<div
|
||||
x-show="showModal && currentDialogType === 'TEST'"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Test Dialog (Base - for TEST type) -->
|
||||
<div x-show="showModal && (getTypeCode(form.TestType) === 'TEST' || form.TypeCode === 'TEST')" x-cloak
|
||||
class="modal-overlay" @click.self="closeModal()" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal-content p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto" @click.stop
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-microscope" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Lab Test' : 'New Lab Test'"></span>
|
||||
</h3>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center">
|
||||
<i class="fa-solid fa-flask text-indigo-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));">
|
||||
<span x-text="isEditing ? 'Edit Test' : 'New Test'"></span>
|
||||
</h3>
|
||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Laboratory Test Definition</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b mb-6" style="border-color: rgb(var(--color-border));">
|
||||
<button
|
||||
class="px-6 py-2 font-medium text-sm transition-colors border-b-2"
|
||||
:class="form.dialogTab === 'general' ? 'border-primary text-primary' : 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||
style="--tw-text-opacity: 1; border-color: transition;"
|
||||
@click="form.dialogTab = 'general'"
|
||||
:style="form.dialogTab === 'general' ? 'border-color: rgb(var(--color-primary)); color: rgb(var(--color-primary));' : ''"
|
||||
>
|
||||
General
|
||||
<!-- Test Type Badge -->
|
||||
<div class="mb-4">
|
||||
<span class="badge badge-info gap-1">
|
||||
<i class="fa-solid fa-flask"></i>
|
||||
<span x-text="form.TypeCode || getTypeName(form.TestType)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex flex-wrap gap-1 mb-4 p-1 rounded-lg"
|
||||
style="background: rgb(var(--color-bg-secondary)); border: 1px solid rgb(var(--color-border));">
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'basic' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'basic'">
|
||||
<i class="fa-solid fa-info-circle mr-1"></i> Basic
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-2 font-medium text-sm transition-colors border-b-2"
|
||||
:class="form.dialogTab === 'reff' ? 'border-primary text-primary' : 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||
@click="form.dialogTab = 'reff'"
|
||||
:style="form.dialogTab === 'reff' ? 'border-color: rgb(var(--color-primary)); color: rgb(var(--color-primary));' : ''"
|
||||
>
|
||||
Reff
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-flask mr-1"></i> Results
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'reference' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'reference'">
|
||||
<i class="fa-solid fa-ruler-combined mr-1"></i> Ref
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form Content -->
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- General Tab -->
|
||||
<div x-show="form.dialogTab === 'general'" class="space-y-4">
|
||||
|
||||
<!-- Tab: Basic Information (includes Org, Sample, and Seq) -->
|
||||
<div x-show="activeTab === 'basic'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Basic Info Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-info-circle text-indigo-500"></i> Basic Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Code <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered font-mono uppercase w-full"
|
||||
:class="errors.TestSiteCode && 'input-error'" x-model="form.TestSiteCode"
|
||||
placeholder="e.g., CBC, GLU, HB" maxlength="10" />
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt text-error text-xs" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Test Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" :class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName" placeholder="e.g., Complete Blood Count" />
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt text-error text-xs">Test name is required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Description</span>
|
||||
</label>
|
||||
<textarea class="textarea textarea-bordered w-full" x-model="form.Description"
|
||||
placeholder="Optional description..." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-building text-indigo-500"></i> Organization
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Discipline</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DisciplineID">
|
||||
<option value="">Select Discipline</option>
|
||||
<option value="1">Hematology</option>
|
||||
<option value="2">Chemistry</option>
|
||||
<option value="3">Microbiology</option>
|
||||
<option value="4">Urinalysis</option>
|
||||
<option value="5">Immunology</option>
|
||||
<option value="6">Serology</option>
|
||||
<option value="10">General</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Department</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.DepartmentID">
|
||||
<option value="">Select Department</option>
|
||||
<option value="1">Lab Hematology</option>
|
||||
<option value="2">Lab Chemistry</option>
|
||||
<option value="3">Lab Microbiology</option>
|
||||
<option value="4">Lab Urinalysis</option>
|
||||
<option value="5">Lab Immunology</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample & Method Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-indigo-500"></i> Sample & Method
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., CBC Analyzer, Hexokinase" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequencing Section -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName"
|
||||
placeholder="Glucose Fasting"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteName"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-ol text-indigo-500"></i> Sequencing & Visibility
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqScr" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.SeqRpt" placeholder="0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Indent</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.TestSiteCode && 'input-error'"
|
||||
x-model="form.TestSiteCode"
|
||||
placeholder="GLUC"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteCode"></span>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Type <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<select class="select" x-model="form.TestType" :class="errors.TestType && 'input-error'">
|
||||
<option value="">Select Type</option>
|
||||
<template x-for="t in typesList" :key="t.VID">
|
||||
<option :value="t.VID" x-text="t.VDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
<label class="label" x-show="errors.TestType">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestType"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-20 pt-2"
|
||||
x-model="form.Description"
|
||||
placeholder="Internal test description..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Scr)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqScr" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Rpt)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqRpt" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 p-4 rounded-xl border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Screen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Report</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.CountStat" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reff Tab -->
|
||||
<div x-show="form.dialogTab === 'reff'" class="space-y-4" x-cloak>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Ref Type</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.RefType">
|
||||
<option value="">Select Ref Type</option>
|
||||
<template x-for="rt in refTypesList" :key="rt.VID">
|
||||
<option :value="rt.VValue" x-text="rt.VDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
<!-- Tab: Result Configuration (includes Sample & Method) -->
|
||||
<div x-show="activeTab === 'results'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
|
||||
<!-- Result Configuration -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-flask text-indigo-500"></i> Result Configuration
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Result Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.ResultType">
|
||||
<option value="">Select Result Type</option>
|
||||
<option value="NMRIC">Numeric</option>
|
||||
<option value="TEXT">Text</option>
|
||||
<option value="VSET">Value Set (Select)</option>
|
||||
<option value="RANGE">Range with Reference</option>
|
||||
<option value="CALC">Calculated</option>
|
||||
<option value="DTTM">Date/Time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 1</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit1"
|
||||
placeholder="e.g., mg/dL, U/L, %" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 2</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Unit2"
|
||||
placeholder="e.g., mmol/L (optional)" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Decimal Places</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.Decimal" placeholder="2"
|
||||
min="0" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Expected TAT (min)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.ExpectedTAT"
|
||||
placeholder="60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Numeric Reference Range -->
|
||||
<template x-if="form.RefType === 'NMRC'">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Ref Low</span></label>
|
||||
<input type="text" class="input" x-model="form.RefLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Ref High</span></label>
|
||||
<input type="text" class="input" x-model="form.RefHigh" placeholder="10.00" />
|
||||
</div>
|
||||
<!-- Sample & Method -->
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-indigo-500"></i> Sample & Method
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 border-t pt-4" style="border-color: rgb(var(--color-border));">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium text-error">Crit Low</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium text-error">Crit High</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritHigh" placeholder="20.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Unit</span></label>
|
||||
<input type="text" class="input" x-model="form.Unit1" placeholder="mg/dL" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Decimals</span></label>
|
||||
<input type="number" class="input" x-model="form.Decimal" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., CBC Analyzer, Hexokinase" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Descriptive Text -->
|
||||
<template x-if="form.RefType === 'TEXT'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Default Reference Text</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-32 pt-2"
|
||||
x-model="form.RefText"
|
||||
placeholder="e.g. Negative"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- List / Value Set -->
|
||||
<template x-if="form.RefType === 'LIST'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Select Value Set</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.RefVSet">
|
||||
<option value="">Select a value set...</option>
|
||||
<template x-for="v in vsetDefsList" :key="v.VSetDefID">
|
||||
<option :value="v.VSetDefID" x-text="v.VSDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="mt-4 p-4 rounded-lg bg-blue-50 border border-blue-100 flex items-start gap-3">
|
||||
<i class="fa-solid fa-circle-info text-blue-500 mt-0.5"></i>
|
||||
<p class="text-xs text-blue-700 leading-relaxed">
|
||||
Selecting a value set will restrict result entry to predefined values and use them for reference matching.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Reference Range -->
|
||||
<div x-show="activeTab === 'reference'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText"
|
||||
placeholder="e.g., 70-100 mg/dL (for text reference type)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<div class="flex gap-3 mt-6 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">
|
||||
<i class="fa-solid fa-times mr-2"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<span x-show="saving" class="loading loading-spinner loading-sm mr-2"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Test'"></span>
|
||||
<span x-text="saving ? 'Saving...' : (isEditing ? 'Update Test' : 'Create Test')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,120 +0,0 @@
|
||||
<!-- Title Test Form Modal -->
|
||||
<div
|
||||
x-show="showModal && currentDialogType === 'TITLE'"
|
||||
x-cloak
|
||||
class="modal-overlay"
|
||||
@click.self="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-2xl w-full"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-heading" style="color: rgb(var(--color-warning));"></i>
|
||||
<span x-text="isEditing ? 'Edit Report Title' : 'New Report Title'"></span>
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Title Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName"
|
||||
placeholder="Hematology Results"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteName"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Title Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
:class="errors.TestSiteCode && 'input-error'"
|
||||
x-model="form.TestSiteCode"
|
||||
placeholder="HEMO"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-16 pt-2"
|
||||
x-model="form.Description"
|
||||
placeholder="Title description..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqScr" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqRpt" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 p-4 rounded-xl border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Screen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Report</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-warning flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Title'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,272 +0,0 @@
|
||||
CREATE TABLE `coding_sys` (
|
||||
`coding_sys_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`abb` varchar(10) UNIQUE,
|
||||
`name` varchar(255),
|
||||
`description` text,
|
||||
`create_date` datetime,
|
||||
`end_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `races` (
|
||||
`race_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`site_id` integer,
|
||||
`coding_sys_id` integer,
|
||||
`name` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `religions` (
|
||||
`religion_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`site_id` integer,
|
||||
`coding_sys_id` integer,
|
||||
`name` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `ethnics` (
|
||||
`ethnic_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`site_id` integer,
|
||||
`coding_sys_id` integer,
|
||||
`name` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `countries` (
|
||||
`country_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`site_id` integer,
|
||||
`coding_sys_id` integer,
|
||||
`name` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `patients` (
|
||||
`pat_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`pat_num` varchar(255) UNIQUE,
|
||||
`pat_altnum` varchar(255) UNIQUE,
|
||||
`prefix` varchar(255),
|
||||
`name_first` varchar(255),
|
||||
`name_middle` varchar(255),
|
||||
`name_maiden` varchar(255),
|
||||
`name_last` varchar(255),
|
||||
`suffix` varchar(255),
|
||||
`name_alias` varchar(255),
|
||||
`gender` varchar(255),
|
||||
`birth_place` varchar(255),
|
||||
`birth_date` date,
|
||||
`address_1` varchar(255),
|
||||
`address_2` varchar(255),
|
||||
`address_3` varchar(255),
|
||||
`city` varchar(255),
|
||||
`province` varchar(255),
|
||||
`zip` varchar(255),
|
||||
`email_1` varchar(255),
|
||||
`email_2` varchar(255),
|
||||
`phone` varchar(255),
|
||||
`mobile_phone` varchar(255),
|
||||
`mother` varchar(255),
|
||||
`account_number` varchar(255),
|
||||
`marital_status` varchar(255),
|
||||
`country_id` integer,
|
||||
`race_id` integer,
|
||||
`religion_id` integer,
|
||||
`ethnic_id` integer,
|
||||
`citizenship` varchar(255),
|
||||
`death` bit,
|
||||
`death_date` datetime,
|
||||
`link_to` integer,
|
||||
`create_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `pat_comments` (
|
||||
`pat_com_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`pat_id` integer,
|
||||
`comment_text` text,
|
||||
`user_id` integer,
|
||||
`create_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `pat_identities` (
|
||||
`pat_idt_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`pat_id` integer,
|
||||
`identity_type` varchar(255),
|
||||
`identity_num` varchar(255),
|
||||
`effective_date` datetime,
|
||||
`expiration_date` datetime,
|
||||
`create_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `pat_diagnose` (
|
||||
`pat_dia_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`pat_id` integer,
|
||||
`diag_code` varchar(255),
|
||||
`diag_comment` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `pat_visits` (
|
||||
`pv_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`pv_num` varchar(255) UNIQUE,
|
||||
`pat_id` integer,
|
||||
`episode_number` integer,
|
||||
`pv_class_id` integer,
|
||||
`bill_account` varchar(255),
|
||||
`bill_status` integer,
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `pv_adts` (
|
||||
`pv_adt_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`pv_id` integer,
|
||||
`pv_adt_num` varchar(255),
|
||||
`pv_adt_code` varchar(255),
|
||||
`locid` integer,
|
||||
`docid` integer,
|
||||
`reff_docid` integer,
|
||||
`adm_docid` integer,
|
||||
`cns_docid` integer,
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `pv_log` (
|
||||
`pv_log_id` integer PRIMARY KEY AUTO_INCREMENT
|
||||
);
|
||||
|
||||
CREATE TABLE `requests` (
|
||||
`req_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`req_num` varchar(255) UNIQUE,
|
||||
`req_altnum` varchar(255) UNIQUE,
|
||||
`pat_id` integer,
|
||||
`pv_id` integer,
|
||||
`req_app` varchar(255),
|
||||
`req_entity` varchar(255),
|
||||
`req_entity_id` integer,
|
||||
`loc_id` integer,
|
||||
`priority` varchar(255),
|
||||
`att_doid` integer,
|
||||
`reff_docid` integer,
|
||||
`adm_docid` integer,
|
||||
`cns_docid` integer,
|
||||
`entered_by` varchar(255),
|
||||
`req_date` datetime,
|
||||
`eff_date` datetime,
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `req_comments` (
|
||||
`req_com_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`req_id` integer,
|
||||
`comment_text` text,
|
||||
`user_id` integer,
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `req_atts` (
|
||||
`req_att_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`req_id` integer,
|
||||
`address` varchar(255),
|
||||
`user_id` integer,
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `req_status` (
|
||||
`req_status_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`req_id` integer,
|
||||
`req_status` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `req_logs` (
|
||||
`req_log_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`tbl_name` varchar(255),
|
||||
`record_id` integer,
|
||||
`fld_name` varchar(255),
|
||||
`fld_value_prev` varchar(255),
|
||||
`user_id` integer,
|
||||
`site_id` integer,
|
||||
`machine_id` integer,
|
||||
`session_id` integer,
|
||||
`app_id` integer,
|
||||
`process_id` integer,
|
||||
`webpage_id` integer,
|
||||
`event_id` integer,
|
||||
`act_id` integer,
|
||||
`reason` varchar(255),
|
||||
`log_date` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `users` (
|
||||
`user_id` integer PRIMARY KEY AUTO_INCREMENT,
|
||||
`username` varchar(255) UNIQUE,
|
||||
`fullname` varchar(255),
|
||||
`password` varchar(255),
|
||||
`create_date` datetime,
|
||||
`end_date` datetime,
|
||||
`archive_date` datetime,
|
||||
`del_date` datetime
|
||||
);
|
||||
|
||||
ALTER TABLE `races` ADD FOREIGN KEY (`coding_sys_id`) REFERENCES `coding_sys` (`coding_sys_id`);
|
||||
|
||||
ALTER TABLE `religions` ADD FOREIGN KEY (`coding_sys_id`) REFERENCES `coding_sys` (`coding_sys_id`);
|
||||
|
||||
ALTER TABLE `ethnics` ADD FOREIGN KEY (`coding_sys_id`) REFERENCES `coding_sys` (`coding_sys_id`);
|
||||
|
||||
ALTER TABLE `countries` ADD FOREIGN KEY (`coding_sys_id`) REFERENCES `coding_sys` (`coding_sys_id`);
|
||||
|
||||
ALTER TABLE `patients` ADD FOREIGN KEY (`country_id`) REFERENCES `countries` (`country_id`);
|
||||
|
||||
ALTER TABLE `patients` ADD FOREIGN KEY (`race_id`) REFERENCES `races` (`race_id`);
|
||||
|
||||
ALTER TABLE `patients` ADD FOREIGN KEY (`religion_id`) REFERENCES `religions` (`religion_id`);
|
||||
|
||||
ALTER TABLE `patients` ADD FOREIGN KEY (`ethnic_id`) REFERENCES `ethnics` (`ethnic_id`);
|
||||
|
||||
ALTER TABLE `pat_comments` ADD FOREIGN KEY (`pat_id`) REFERENCES `patients` (`pat_id`);
|
||||
|
||||
ALTER TABLE `pat_identities` ADD FOREIGN KEY (`pat_id`) REFERENCES `patients` (`pat_id`);
|
||||
|
||||
ALTER TABLE `pat_diagnose` ADD FOREIGN KEY (`pat_id`) REFERENCES `patients` (`pat_id`);
|
||||
|
||||
ALTER TABLE `pat_visits` ADD FOREIGN KEY (`pat_id`) REFERENCES `patients` (`pat_id`);
|
||||
|
||||
ALTER TABLE `pv_adts` ADD FOREIGN KEY (`pv_id`) REFERENCES `pat_visits` (`pv_id`);
|
||||
|
||||
ALTER TABLE `requests` ADD FOREIGN KEY (`pat_id`) REFERENCES `patients` (`pat_id`);
|
||||
|
||||
ALTER TABLE `requests` ADD FOREIGN KEY (`pv_id`) REFERENCES `pat_visits` (`pv_id`);
|
||||
|
||||
ALTER TABLE `req_comments` ADD FOREIGN KEY (`req_id`) REFERENCES `requests` (`req_id`);
|
||||
|
||||
ALTER TABLE `req_atts` ADD FOREIGN KEY (`req_id`) REFERENCES `requests` (`req_id`);
|
||||
|
||||
ALTER TABLE `req_status` ADD FOREIGN KEY (`req_id`) REFERENCES `requests` (`req_id`);
|
||||
|
||||
ALTER TABLE `req_logs` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`);
|
||||
234
data/lab.dbml
234
data/lab.dbml
@ -1,234 +0,0 @@
|
||||
table coding_sys {
|
||||
coding_sys_id integer [pk]
|
||||
abb varchar(10) [unique]
|
||||
name varchar
|
||||
description text
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
}
|
||||
|
||||
table races {
|
||||
race_id integer [pk]
|
||||
site_id integer
|
||||
coding_sys_id integer [ref:>coding_sys.coding_sys_id]
|
||||
name varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
}
|
||||
|
||||
table religions {
|
||||
religion_id integer [pk]
|
||||
site_id integer
|
||||
coding_sys_id integer [ref:>coding_sys.coding_sys_id]
|
||||
name varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
}
|
||||
|
||||
table ethnics {
|
||||
ethnic_id integer [pk]
|
||||
site_id integer
|
||||
coding_sys_id integer [ref:>coding_sys.coding_sys_id]
|
||||
name varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
}
|
||||
|
||||
table countries {
|
||||
country_id integer [pk]
|
||||
site_id integer
|
||||
coding_sys_id integer [ref:>coding_sys.coding_sys_id]
|
||||
name varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
}
|
||||
|
||||
table patients {
|
||||
pat_id integer [pk]
|
||||
pat_num varchar [unique]
|
||||
pat_altnum varchar [unique]
|
||||
prefix varchar
|
||||
name_first varchar
|
||||
name_middle varchar
|
||||
name_maiden varchar
|
||||
name_last varchar
|
||||
suffix varchar
|
||||
name_alias varchar
|
||||
gender varchar
|
||||
birth_place varchar
|
||||
birth_date date
|
||||
address_1 varchar
|
||||
address_2 varchar
|
||||
address_3 varchar
|
||||
city varchar
|
||||
province varchar
|
||||
zip varchar
|
||||
email_1 varchar
|
||||
email_2 varchar
|
||||
phone varchar
|
||||
mobile_phone varchar
|
||||
mother varchar
|
||||
account_number varchar
|
||||
marital_status varchar
|
||||
country_id integer [ref:>countries.country_id]
|
||||
race_id integer [ref:>races.race_id]
|
||||
religion_id integer [ref:>religions.religion_id]
|
||||
ethnic_id integer [ref:>ethnics.ethnic_id]
|
||||
citizenship varchar
|
||||
death bit
|
||||
death_date datetime
|
||||
link_to integer
|
||||
create_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table pat_comments {
|
||||
pat_com_id integer [pk]
|
||||
pat_id integer [ref:>patients.pat_id]
|
||||
comment_text text
|
||||
user_id integer
|
||||
create_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table pat_identities {
|
||||
pat_idt_id integer [pk]
|
||||
pat_id integer [ref:>patients.pat_id]
|
||||
identity_type varchar
|
||||
identity_num varchar
|
||||
effective_date datetime
|
||||
expiration_date datetime
|
||||
create_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table pat_diagnose {
|
||||
pat_dia_id integer [pk]
|
||||
pat_id integer [ref:>patients.pat_id]
|
||||
diag_code varchar
|
||||
diag_comment varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table pat_visits {
|
||||
pv_id integer [pk]
|
||||
pv_num varchar [unique]
|
||||
pat_id integer [ref:>patients.pat_id]
|
||||
episode_number integer
|
||||
pv_class_id integer
|
||||
bill_account varchar
|
||||
bill_status integer
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table pv_adts {
|
||||
pv_adt_id integer [pk]
|
||||
pv_id integer [ref:>pat_visits.pv_id]
|
||||
pv_adt_num varchar
|
||||
pv_adt_code varchar
|
||||
locid integer
|
||||
docid integer
|
||||
reff_docid integer
|
||||
adm_docid integer
|
||||
cns_docid integer
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table pv_log {
|
||||
pv_log_id integer [pk]
|
||||
}
|
||||
|
||||
table requests {
|
||||
req_id integer [pk]
|
||||
req_num varchar [unique]
|
||||
req_altnum varchar [unique]
|
||||
pat_id integer [ref:>patients.pat_id]
|
||||
pv_id integer [ref:>pat_visits.pv_id]
|
||||
req_app varchar
|
||||
req_entity varchar
|
||||
req_entity_id integer
|
||||
loc_id integer
|
||||
priority varchar
|
||||
att_doid integer
|
||||
reff_docid integer
|
||||
adm_docid integer
|
||||
cns_docid integer
|
||||
entered_by varchar
|
||||
req_date datetime
|
||||
eff_date datetime
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table req_comments {
|
||||
req_com_id integer [pk]
|
||||
req_id integer [ref:>requests.req_id]
|
||||
comment_text text
|
||||
user_id integer
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table req_atts {
|
||||
req_att_id integer [pk]
|
||||
req_id integer [ref:>requests.req_id]
|
||||
address varchar
|
||||
user_id integer
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table req_status {
|
||||
req_status_id integer [pk]
|
||||
req_id integer [ref:>requests.req_id]
|
||||
req_status varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
|
||||
table req_logs {
|
||||
req_log_id integer [pk]
|
||||
tbl_name varchar
|
||||
record_id integer
|
||||
fld_name varchar
|
||||
fld_value_prev varchar
|
||||
user_id integer [ref:>users.user_id]
|
||||
site_id integer
|
||||
machine_id integer
|
||||
session_id integer
|
||||
app_id integer
|
||||
process_id integer
|
||||
webpage_id integer
|
||||
event_id integer
|
||||
act_id integer
|
||||
reason varchar
|
||||
log_date datetime
|
||||
}
|
||||
|
||||
table users {
|
||||
user_id integer [pk]
|
||||
username varchar [unique]
|
||||
fullname varchar
|
||||
password varchar
|
||||
create_date datetime
|
||||
end_date datetime
|
||||
archive_date datetime
|
||||
del_date datetime
|
||||
}
|
||||
@ -1,689 +0,0 @@
|
||||
# Plan: Multiple Reference Ranges with Advanced Dialog
|
||||
|
||||
## Overview
|
||||
Refactor the "Reff" tab to support multiple reference ranges using the existing `refnum` table schema.
|
||||
|
||||
## Existing Database Schema (refnum table)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| RefNumID | INT AUTO_INCREMENT | Primary key |
|
||||
| SiteID | INT | Site identifier |
|
||||
| TestSiteID | INT | Links to test |
|
||||
| SpcType | INT | Specimen type |
|
||||
| Sex | INT | Gender (from valueset) |
|
||||
| Criteria | VARCHAR(100) | Additional criteria |
|
||||
| AgeStart | INT | Age range start |
|
||||
| AgeEnd | INT | Age range end |
|
||||
| **NumRefType** | INT | **Input format: 1=NMRC, 2=TH, 3=TEXT, 4=LIST** |
|
||||
| **RangeType** | INT | **Result category: 1=REF, 2=CRTC, 3=VAL, 4=RERUN** |
|
||||
| LowSign | INT | Low operator: 1='<', 2='<=', 3='>=', 4='>', 5='<>' |
|
||||
| Low | INT | Low value |
|
||||
| HighSign | INT | High operator |
|
||||
| High | INT | High value |
|
||||
| Display | INT | Display order |
|
||||
| **Flag** | VARCHAR(10) | **Like Label (e.g., "Negative", "Borderline")** |
|
||||
| Interpretation | VARCHAR(255) | Interpretation text |
|
||||
| Notes | VARCHAR(255) | Notes |
|
||||
| CreateDate | Datetime | Creation timestamp |
|
||||
| StartDate | Datetime | Start date |
|
||||
| EndDate | Datetime | Soft delete |
|
||||
|
||||
---
|
||||
|
||||
## Key Concept: NumRefType vs RangeType
|
||||
|
||||
| Aspect | NumRefType | RangeType |
|
||||
|--------|------------|-----------|
|
||||
| **Location** | Main Reff Tab + Advanced Dialog | Advanced Dialog |
|
||||
| **Purpose** | Input format | Result categorization |
|
||||
| **Values** | 1=NMRC, 2=TH, 3=TEXT, 4=LIST | 1=REF, 2=CRTC, 3=VAL, 4=RERUN |
|
||||
| **Database Field** | NumRefType | RangeType |
|
||||
|
||||
---
|
||||
|
||||
## UI Design
|
||||
|
||||
### Main Reff Tab (Simple)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Reference Ranges │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Ref Type: [ Numeric (NMRC) ▼ ] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ For Numeric (with operators > < <= >=): ││
|
||||
│ │ Ref Low: [0.00 ] Ref High: [100.00 ] ││
|
||||
│ │ Crit Low: [<55.00 ] Crit High: [>115.00 ] ││
|
||||
│ │ ││
|
||||
│ │ Examples: 0-100, <50, >=100, <>0 (not equal to 0) ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ For Threshold: ││
|
||||
│ │ Below Text: [Below Normal] Below Value: [<] [50] ││
|
||||
│ │ Above Text: [Above Normal] Above Value: [>] [150] ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ [Advanced Settings ▼] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Advanced Dialog (Multiple Reference Ranges)
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Advanced Reference Ranges [X]Close│
|
||||
├───────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [Add RefType ▼] [Add Button] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ RefType │ Flag/Label │ RangeType │ Sex │ Age │ Low │ High │ [×] │ │
|
||||
│ │─────────┼────────────┼───────────┼─────┼─────┼────────┼────────┼───────│ │
|
||||
│ │ NMRC │ Negative │ REF (1) │ All │ 0-150│ 0 │ 25 │ [×] │ │
|
||||
│ │ NMRC │ Borderline │ REF (1) │ All │ 0-150│ 25 │ 50 │ [×] │ │
|
||||
│ │ NMRC │ Positive │ REF (1) │ All │ 0-150│ 50 │ │ [×] │ │
|
||||
│ │ TEXT │ Negative │ REF (1) │ All │ 0-150│ │ │ [×] │ │
|
||||
│ │ TH │ Low │ REF (1) │ All │ 0-150│ <50 │ │ [×] │ │
|
||||
│ │ NMRC │ Critical │ CRTC (2) │ All │ 0-150│ <55 │ >115 │ [×] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Cancel] [Save Advanced Ranges] │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- **RefType** column: NMRC (1), TH (2), TEXT (3), LIST (4)
|
||||
- **RangeType** column: REF (1), CRTC (2), VAL (3), RERUN (4)
|
||||
- **Flag** column: Display label for the result (e.g., "Negative", "Borderline")
|
||||
- **Low/High** columns: Support operators via LowSign/HighSign fields
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Backend Changes (Tests.php Controller)
|
||||
|
||||
#### 1.1 Add RefType and RangeType constants
|
||||
```php
|
||||
// At top of Tests.php
|
||||
const REFTYPE_NMRC = 1;
|
||||
const REFTYPE_TH = 2;
|
||||
const REFTYPE_TEXT = 3;
|
||||
const REFTYPE_LIST = 4;
|
||||
|
||||
const RANGETYPE_REF = 1;
|
||||
const RANGETYPE_CRTC = 2;
|
||||
const RANGETYPE_VAL = 3;
|
||||
const RANGETYPE_RERUN = 4;
|
||||
|
||||
const LOWSIGN_LT = 1;
|
||||
const LOWSIGN_LTE = 2;
|
||||
const LOWSIGN_GTE = 3;
|
||||
const LOWSIGN_GT = 4;
|
||||
const LOWSIGN_NE = 5;
|
||||
```
|
||||
|
||||
#### 1.2 Update `show()` method to load refnum data
|
||||
```php
|
||||
// Add after loading testdeftech/testdefcal
|
||||
$row['refnum'] = $this->RefNumModel->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('Display', 'ASC')
|
||||
->findAll();
|
||||
```
|
||||
|
||||
#### 1.3 Update `saveRefNum()` helper method
|
||||
```php
|
||||
private function saveRefNum($testSiteID, $refRanges, $action, $siteID = 1) {
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing refnums
|
||||
$this->RefNumModel->where('TestSiteID', $testSiteID)
|
||||
->set('EndDate', date('Y-m-d H:i:s'))
|
||||
->update();
|
||||
}
|
||||
|
||||
foreach ($refRanges as $index => $ref) {
|
||||
$refData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'NumRefType' => $ref['RefType'] ?? self::REFTYPE_NMRC,
|
||||
'RangeType' => $ref['RangeType'] ?? self::RANGETYPE_REF,
|
||||
'Flag' => $ref['Flag'] ?? null, // Label for display
|
||||
'Sex' => $ref['Sex'] ?? 0, // 0=All, 1=M, 2=F (from valueset)
|
||||
'AgeStart' => $ref['AgeStart'] ?? 0,
|
||||
'AgeEnd' => $ref['AgeEnd'] ?? 150,
|
||||
'LowSign' => $this->parseSign($ref['Low'] ?? ''),
|
||||
'Low' => $this->parseValue($ref['Low'] ?? ''),
|
||||
'HighSign' => $this->parseSign($ref['High'] ?? ''),
|
||||
'High' => $this->parseValue($ref['High'] ?? ''),
|
||||
'Display' => $index,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
];
|
||||
$this->RefNumModel->insert($refData);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to extract operator from value like "<=50"
|
||||
private function parseSign($value) {
|
||||
if (str_starts_with($value, '<>')) return self::LOWSIGN_NE;
|
||||
if (str_starts_with($value, '<=')) return self::LOWSIGN_LTE;
|
||||
if (str_starts_with($value, '<')) return self::LOWSIGN_LT;
|
||||
if (str_starts_with($value, '>=')) return self::LOWSIGN_GTE;
|
||||
if (str_starts_with($value, '>')) return self::LOWSIGN_GT;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to extract numeric value from operator-prefixed string
|
||||
private function parseValue($value) {
|
||||
return preg_replace('/^[<>=<>]+/', '', $value) ?: null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 Update `handleDetails()` to save refnum
|
||||
```php
|
||||
// Add in handleDetails method, after saving tech/calc details
|
||||
if (isset($input['refnum']) && is_array($input['refnum'])) {
|
||||
$this->saveRefNum($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1);
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.5 Update `delete()` to soft delete refnum
|
||||
```php
|
||||
// Add in delete method
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->RefNumModel->where('TestSiteID', $id)
|
||||
->set('EndDate', $now)
|
||||
->update();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Frontend Changes (tests_index.php)
|
||||
|
||||
#### 2.1 Update form state to include advanced ref ranges
|
||||
```javascript
|
||||
form: {
|
||||
// ... existing fields ...
|
||||
// Advanced ranges
|
||||
refRanges: [], // Array of advanced reference range objects
|
||||
// Dialog states
|
||||
showAdvancedRefModal: false,
|
||||
advancedRefRanges: [],
|
||||
newRefType: 1 // Default: NMRC
|
||||
}
|
||||
|
||||
// RefType options for select
|
||||
refTypeOptions: [
|
||||
{ value: 1, label: 'Numeric (NMRC)' },
|
||||
{ value: 2, label: 'Threshold (TH)' },
|
||||
{ value: 3, label: 'Text (TEXT)' },
|
||||
{ value: 4, label: 'Value Set (LIST)' }
|
||||
]
|
||||
|
||||
// RangeType options
|
||||
rangeTypeOptions: [
|
||||
{ value: 1, label: 'REF' },
|
||||
{ value: 2, label: 'CRTC' },
|
||||
{ value: 3, label: 'VAL' },
|
||||
{ value: 4, label: 'RERUN' }
|
||||
]
|
||||
|
||||
// Sex options
|
||||
sexOptions: [
|
||||
{ value: 0, label: 'All' },
|
||||
{ value: 1, label: 'Male' },
|
||||
{ value: 2, label: 'Female' }
|
||||
]
|
||||
```
|
||||
|
||||
#### 2.2 Update `editTest()` to load refnum data
|
||||
```javascript
|
||||
if (testData.refnum && testData.refnum.length > 0) {
|
||||
this.form.refRanges = testData.refnum.map(r => ({
|
||||
RefNumID: r.RefNumID,
|
||||
RefType: r.NumRefType || 1,
|
||||
RangeType: r.RangeType || 1,
|
||||
Flag: r.Flag || '',
|
||||
Sex: r.Sex || 0,
|
||||
AgeStart: r.AgeStart || 0,
|
||||
AgeEnd: r.AgeEnd || 150,
|
||||
Low: this.formatValueWithSign(r.LowSign, r.Low),
|
||||
High: this.formatValueWithSign(r.HighSign, r.High)
|
||||
}));
|
||||
} else {
|
||||
this.form.refRanges = [];
|
||||
}
|
||||
|
||||
// Format value with operator sign for display
|
||||
formatValueWithSign(sign, value) {
|
||||
if (!value && value !== 0) return '';
|
||||
const signs = {
|
||||
1: '<', 2: '<=', 3: '>=', 4: '>', 5: '<>'
|
||||
};
|
||||
return (signs[sign] || '') + value;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Update `save()` to include refnum in payload
|
||||
```javascript
|
||||
if (this.form.refRanges && this.form.refRanges.length > 0) {
|
||||
payload.refnum = this.form.refRanges.map(r => ({
|
||||
RefType: r.RefType,
|
||||
RangeType: r.RangeType,
|
||||
Flag: r.Flag,
|
||||
Sex: r.Sex,
|
||||
AgeStart: r.AgeStart,
|
||||
AgeEnd: r.AgeEnd,
|
||||
Low: r.Low,
|
||||
High: r.High
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4 Add helper methods for advanced ref ranges
|
||||
```javascript
|
||||
// Open advanced dialog
|
||||
openAdvancedRefDialog() {
|
||||
this.advancedRefRanges = this.form.refRanges.length > 0
|
||||
? [...this.form.refRanges]
|
||||
: [{
|
||||
RefNumID: null,
|
||||
RefType: this.form.RefType || 1,
|
||||
RangeType: 1,
|
||||
Flag: '',
|
||||
Sex: 0,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 150,
|
||||
Low: this.form.RefLow || '',
|
||||
High: this.form.RefHigh || ''
|
||||
}];
|
||||
|
||||
// Add CRTC if critical values exist
|
||||
if ((this.form.CritLow || this.form.CritHigh) &&
|
||||
!this.advancedRefRanges.some(r => r.RangeType === 2)) {
|
||||
this.advancedRefRanges.push({
|
||||
RefNumID: null,
|
||||
RefType: 1,
|
||||
RangeType: 2,
|
||||
Flag: 'Critical',
|
||||
Sex: 0,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 150,
|
||||
Low: this.form.CritLow || '',
|
||||
High: this.form.CritHigh || ''
|
||||
});
|
||||
}
|
||||
|
||||
this.showAdvancedRefModal = true;
|
||||
},
|
||||
|
||||
// Add new advanced range
|
||||
addAdvancedRefRange() {
|
||||
this.advancedRefRanges.push({
|
||||
RefNumID: null,
|
||||
RefType: this.newRefType,
|
||||
RangeType: 1,
|
||||
Flag: '',
|
||||
Sex: 0,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 150,
|
||||
Low: '',
|
||||
High: ''
|
||||
});
|
||||
},
|
||||
|
||||
// Remove advanced range
|
||||
removeAdvancedRefRange(index) {
|
||||
this.advancedRefRanges.splice(index, 1);
|
||||
},
|
||||
|
||||
// Save advanced ranges and close
|
||||
saveAdvancedRefRanges() {
|
||||
this.form.refRanges = [...this.advancedRefRanges];
|
||||
this.showAdvancedRefModal = false;
|
||||
},
|
||||
|
||||
// Cancel advanced dialog
|
||||
cancelAdvancedRefDialog() {
|
||||
this.showAdvancedRefModal = false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: UI Changes (test_dialog.php)
|
||||
|
||||
#### 3.1 Keep main Reff tab with RefType selector
|
||||
```html
|
||||
<!-- Reff Tab - Main (Simple) -->
|
||||
<div x-show="form.dialogTab === 'reff'" class="space-y-4" x-cloak>
|
||||
|
||||
<!-- RefType Selector -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Reference Type</span></label>
|
||||
<select class="select w-full" x-model="form.RefType">
|
||||
<option value="1">Numeric Range (NMRC)</option>
|
||||
<option value="2">Threshold (TH)</option>
|
||||
<option value="3">Text Result (TEXT)</option>
|
||||
<option value="4">Value Set (LIST)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Numeric Range Fields -->
|
||||
<template x-if="form.RefType == '1'">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Ref Low</span></label>
|
||||
<input type="text" class="input" x-model="form.RefLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Ref High</span></label>
|
||||
<input type="text" class="input" x-model="form.RefHigh" placeholder="10.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text text-error">Crit Low</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text text-error">Crit High</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritHigh" placeholder="20.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Unit</span></label>
|
||||
<input type="text" class="input" x-model="form.Unit1" placeholder="mg/dL" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Decimals</span></label>
|
||||
<input type="number" class="input" x-model="form.Decimal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Threshold Fields -->
|
||||
<template x-if="form.RefType == '2'">
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-info/10 border border-info/20 rounded-lg">
|
||||
<p class="text-sm mb-3"><strong>Below Threshold:</strong></p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Below Text</span></label>
|
||||
<input type="text" class="input" x-model="form.RefText" placeholder="Below Normal" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Operator</span></label>
|
||||
<select class="select" x-model="form.BelowOp">
|
||||
<option value="<"><</option>
|
||||
<option value="<="><=</option>
|
||||
<option value="<>"><></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Value</span></label>
|
||||
<input type="text" class="input" x-model="form.BelowVal" placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-warning/10 border border-warning/20 rounded-lg">
|
||||
<p class="text-sm mb-3"><strong>Above Threshold:</strong></p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Above Text</span></label>
|
||||
<input type="text" class="input" x-model="form.AboveText" placeholder="Above Normal" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Operator</span></label>
|
||||
<select class="select" x-model="form.AboveOp">
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Value</span></label>
|
||||
<input type="text" class="input" x-model="form.AboveVal" placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Text Result Fields -->
|
||||
<template x-if="form.RefType == '3'">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Default Text</span></label>
|
||||
<input type="text" class="input" x-model="form.RefText" placeholder="e.g., Negative" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Value Set Fields -->
|
||||
<template x-if="form.RefType == '4'">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Value Set</span></label>
|
||||
<select class="select w-full" x-model="form.VSetDefID">
|
||||
<option value="">Select Value Set...</option>
|
||||
<template x-for="v in vsetDefsList" :key="v.VSetDefID">
|
||||
<option :value="v.VSetDefID" x-text="v.VSDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Advanced Button -->
|
||||
<div class="mt-4 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-outline btn-sm" @click="openAdvancedRefDialog()">
|
||||
<i class="fa-solid fa-gear mr-1"></i>
|
||||
Advanced Settings
|
||||
</button>
|
||||
<span class="ml-2 text-xs opacity-60" x-show="form.refRanges.length > 0">
|
||||
<i class="fa-solid fa-check text-success mr-1"></i>
|
||||
<span x-text="form.refRanges.length + ' advanced ranges configured'"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 3.2 Add Advanced RefRanges Modal
|
||||
```html
|
||||
<!-- Advanced Reference Ranges Modal -->
|
||||
<div x-show="showAdvancedRefModal" x-cloak class="modal-overlay" @click.self="cancelAdvancedRefDialog()">
|
||||
<div class="modal-content p-6 max-w-5xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg">Advanced Reference Ranges</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="cancelAdvancedRefDialog()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Row Controls -->
|
||||
<div class="flex gap-2 mb-4 p-3 bg-base-200 rounded-lg">
|
||||
<select class="select select-sm" x-model="newRefType">
|
||||
<option :value="1">Numeric (NMRC)</option>
|
||||
<option :value="2">Threshold (TH)</option>
|
||||
<option :value="3">Text (TEXT)</option>
|
||||
<option :value="4">Value Set (LIST)</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline" @click="addAdvancedRefRange()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Range
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Ranges Table -->
|
||||
<div class="overflow-x-auto mb-4">
|
||||
<table class="table table-sm table-compact w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">RefType</th>
|
||||
<th style="width: 120px;">Flag/Label</th>
|
||||
<th style="width: 80px;">RangeType</th>
|
||||
<th style="width: 60px;">Sex</th>
|
||||
<th style="width: 70px;">Age From</th>
|
||||
<th style="width: 70px;">Age To</th>
|
||||
<th style="width: 100px;">Low</th>
|
||||
<th style="width: 100px;">High</th>
|
||||
<th style="width: 40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(ref, index) in advancedRefRanges" :key="index">
|
||||
<tr :class="{ 'bg-error/5': ref.RangeType == 2 }">
|
||||
<!-- RefType -->
|
||||
<td>
|
||||
<select
|
||||
class="select select-xs w-full"
|
||||
x-model="ref.RefType"
|
||||
>
|
||||
<option :value="1">NMRC</option>
|
||||
<option :value="2">TH</option>
|
||||
<option :value="3">TEXT</option>
|
||||
<option :value="4">LIST</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Flag/Label -->
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs w-full"
|
||||
x-model="ref.Flag"
|
||||
placeholder="e.g., Negative"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- RangeType -->
|
||||
<td>
|
||||
<select
|
||||
class="select select-xs w-full"
|
||||
:class="{ 'border-error/30 bg-error/10': ref.RangeType == 2 }"
|
||||
x-model="ref.RangeType"
|
||||
>
|
||||
<option :value="1">REF</option>
|
||||
<option :value="2">CRTC</option>
|
||||
<option :value="3">VAL</option>
|
||||
<option :value="4">RERUN</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Sex -->
|
||||
<td>
|
||||
<select class="select select-xs w-full" x-model="ref.Sex">
|
||||
<option :value="0">All</option>
|
||||
<option :value="1">M</option>
|
||||
<option :value="2">F</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Age From -->
|
||||
<td>
|
||||
<input type="number" class="input input-xs w-full text-center" x-model="ref.AgeStart" />
|
||||
</td>
|
||||
|
||||
<!-- Age To -->
|
||||
<td>
|
||||
<input type="number" class="input input-xs w-full text-center" x-model="ref.AgeEnd" />
|
||||
</td>
|
||||
|
||||
<!-- Low -->
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs w-full text-center"
|
||||
:class="{ 'border-error/30': ref.RangeType == 2 }"
|
||||
x-model="ref.Low"
|
||||
placeholder="0.00 or <=10"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- High -->
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs w-full text-center"
|
||||
:class="{ 'border-error/30': ref.RangeType == 2 }"
|
||||
x-model="ref.High"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Delete -->
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error" @click="removeAdvancedRefRange(index)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="advancedRefRanges.length === 0">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-8 text-base-400">
|
||||
<i class="fa-solid fa-layer-group mr-2"></i>
|
||||
No advanced ranges. Click "Add Range" to create one.
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="text-xs opacity-60 p-2 border rounded bg-base-200 flex gap-4 mb-4">
|
||||
<span><strong>1</strong>=NMRC</span>
|
||||
<span><strong>2</strong>=TH</span>
|
||||
<span><strong>3</strong>=TEXT</span>
|
||||
<span><strong>4</strong>=LIST</span>
|
||||
<span class="ml-4"><strong>1</strong>=REF</span>
|
||||
<span><strong>2</strong>=CRTC</span>
|
||||
<span><strong>3</strong>=VAL</span>
|
||||
<span><strong>4</strong>=RERUN</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="cancelAdvancedRefDialog()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="saveAdvancedRefRanges()">
|
||||
<i class="fa-solid fa-check mr-1"></i> Save Advanced Ranges
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `app/Controllers/Tests.php` | Add constants, refnum loading/save helper, delete update |
|
||||
| `app/Models/RefRange/RefNumModel.php` | Ensure allowedFields includes all needed fields |
|
||||
| `app/Views/v2/master/tests/tests_index.php` | Add refRanges state, helper methods, modal state |
|
||||
| `app/Views/v2/master/tests/test_dialog.php` | Update Reff tab with numeric RefType, add Advanced modal |
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Basic users** - Use global RefLow/RefHigh fields on main tab
|
||||
2. **Advanced users** - Click "Advanced Settings" to open modal
|
||||
3. **Modal** - Add/edit/remove multiple ranges with criteria
|
||||
4. **Save** - Advanced ranges saved to refnum table, global fields saved to testdeftech
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review this plan
|
||||
2. Provide feedback or request changes
|
||||
3. Once approved, switch to Code mode for implementation
|
||||
325
tests/_support/v2/MasterTestCase.php
Normal file
325
tests/_support/v2/MasterTestCase.php
Normal file
@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Support\v2;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
/**
|
||||
* Base test case for v2 Master Data tests
|
||||
*
|
||||
* Provides common setup, authentication, and helper methods
|
||||
* for all v2 master test feature and unit tests.
|
||||
*/
|
||||
abstract class MasterTestCase extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
/**
|
||||
* JWT token for authentication
|
||||
*/
|
||||
protected ?string $token = null;
|
||||
|
||||
/**
|
||||
* Test site ID
|
||||
*/
|
||||
protected int $testSiteId = 1;
|
||||
|
||||
/**
|
||||
* Test site code
|
||||
*/
|
||||
protected string $testSiteCode = 'TEST01';
|
||||
|
||||
/**
|
||||
* Valueset IDs for test types
|
||||
*/
|
||||
public const VALUESET_TEST_TYPE = 27; // VSetID for Test Types
|
||||
public const VALUESET_RESULT_TYPE = 43; // VSetID for Result Types
|
||||
public const VALUESET_REF_TYPE = 44; // VSetID for Reference Types
|
||||
public const VALUESET_ENTITY_TYPE = 39; // VSetID for Entity Types
|
||||
|
||||
/**
|
||||
* Test Type VIDs
|
||||
*/
|
||||
public const TEST_TYPE_TEST = 1; // VID for TEST
|
||||
public const TEST_TYPE_PARAM = 2; // VID for PARAM
|
||||
public const TEST_TYPE_CALC = 3; // VID for CALC
|
||||
public const TEST_TYPE_GROUP = 4; // VID for GROUP
|
||||
public const TEST_TYPE_TITLE = 5; // VID for TITLE
|
||||
|
||||
/**
|
||||
* Setup test environment
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->token = $this->generateTestToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup after test
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT token for testing
|
||||
*/
|
||||
protected function generateTestToken(): string
|
||||
{
|
||||
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
|
||||
$payload = [
|
||||
'iss' => 'localhost',
|
||||
'aud' => 'localhost',
|
||||
'iat' => time(),
|
||||
'nbf' => time(),
|
||||
'exp' => time() + 3600,
|
||||
'uid' => 1,
|
||||
'email' => 'admin@admin.com'
|
||||
];
|
||||
return JWT::encode($payload, $key, 'HS256');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated GET request
|
||||
*/
|
||||
protected function get(string $path, array $options = [])
|
||||
{
|
||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
||||
return $this->call('get', $path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated POST request
|
||||
*/
|
||||
protected function post(string $path, array $options = [])
|
||||
{
|
||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
||||
return $this->call('post', $path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated PUT request
|
||||
*/
|
||||
protected function put(string $path, array $options = [])
|
||||
{
|
||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
||||
return $this->call('put', $path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated DELETE request
|
||||
*/
|
||||
protected function delete(string $path, array $options = [])
|
||||
{
|
||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
||||
return $this->call('delete', $path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TEST type test definition
|
||||
*/
|
||||
protected function createTestData(): array
|
||||
{
|
||||
return [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => $this->testSiteCode,
|
||||
'TestSiteName' => 'Test Definition ' . time(),
|
||||
'TestType' => self::TEST_TYPE_TEST,
|
||||
'Description' => 'Test description',
|
||||
'SeqScr' => 10,
|
||||
'SeqRpt' => 10,
|
||||
'IndentLeft' => 0,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'details' => [
|
||||
'DisciplineID' => 1,
|
||||
'DepartmentID' => 1,
|
||||
'ResultType' => 1, // Numeric
|
||||
'RefType' => 1, // NMRC
|
||||
'Unit1' => 'mg/dL',
|
||||
'Decimal' => 2,
|
||||
'Method' => 'Test Method',
|
||||
'ExpectedTAT' => 60
|
||||
],
|
||||
'testmap' => [
|
||||
[
|
||||
'HostType' => 'HIS',
|
||||
'HostID' => 'TEST001',
|
||||
'HostTestCode' => 'TEST001',
|
||||
'HostTestName' => 'Test (HIS)'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PARAM type test definition
|
||||
*/
|
||||
protected function createParamData(): array
|
||||
{
|
||||
return [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PARM' . substr(time(), -4),
|
||||
'TestSiteName' => 'Parameter Test ' . time(),
|
||||
'TestType' => self::TEST_TYPE_PARAM,
|
||||
'Description' => 'Parameter test description',
|
||||
'SeqScr' => 5,
|
||||
'SeqRpt' => 5,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'details' => [
|
||||
'DisciplineID' => 1,
|
||||
'DepartmentID' => 1,
|
||||
'ResultType' => 1,
|
||||
'RefType' => 1,
|
||||
'Unit1' => 'unit',
|
||||
'Decimal' => 1,
|
||||
'Method' => 'Parameter Method'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GROUP type test definition with members
|
||||
*/
|
||||
protected function createGroupData(array $memberIds = []): array
|
||||
{
|
||||
return [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'GRUP' . substr(time(), -4),
|
||||
'TestSiteName' => 'Group Test ' . time(),
|
||||
'TestType' => self::TEST_TYPE_GROUP,
|
||||
'Description' => 'Group test description',
|
||||
'SeqScr' => 100,
|
||||
'SeqRpt' => 100,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'Members' => $memberIds ?: [1, 2],
|
||||
'testmap' => [
|
||||
[
|
||||
'HostType' => 'LIS',
|
||||
'HostID' => 'LIS001',
|
||||
'HostTestCode' => 'PANEL',
|
||||
'HostTestName' => 'Test Panel (LIS)'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CALC type test definition
|
||||
*/
|
||||
protected function createCalcData(): array
|
||||
{
|
||||
return [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'CALC' . substr(time(), -4),
|
||||
'TestSiteName' => 'Calculated Test ' . time(),
|
||||
'TestType' => self::TEST_TYPE_CALC,
|
||||
'Description' => 'Calculated test description',
|
||||
'SeqScr' => 50,
|
||||
'SeqRpt' => 50,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'details' => [
|
||||
'DisciplineID' => 1,
|
||||
'DepartmentID' => 1,
|
||||
'FormulaInput' => '["TEST1", "TEST2"]',
|
||||
'FormulaCode' => 'TEST1 + TEST2',
|
||||
'FormulaLang' => 'SQL',
|
||||
'RefType' => 1,
|
||||
'Unit1' => 'mg/dL',
|
||||
'Decimal' => 0,
|
||||
'Method' => 'Calculation Method'
|
||||
],
|
||||
'testmap' => [
|
||||
[
|
||||
'HostType' => 'LIS',
|
||||
'HostID' => 'LIS001',
|
||||
'HostTestCode' => 'CALCR',
|
||||
'HostTestName' => 'Calculated Result (LIS)'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert API response has success status
|
||||
*/
|
||||
protected function assertSuccessResponse($response, string $message = 'Response should be successful'): void
|
||||
{
|
||||
$body = json_decode($response->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('status', $body, $message);
|
||||
$this->assertEquals('success', $body['status'], $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert API response has error status
|
||||
*/
|
||||
protected function assertErrorResponse($response, string $message = 'Response should be an error'): void
|
||||
{
|
||||
$body = json_decode($response->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('status', $body, $message);
|
||||
$this->assertNotEquals('success', $body['status'], $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert response has data key
|
||||
*/
|
||||
protected function assertHasData($response, string $message = 'Response should have data'): void
|
||||
{
|
||||
$body = json_decode($response->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('data', $body, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test type name from VID
|
||||
*/
|
||||
protected function getTestTypeName(int $vid): string
|
||||
{
|
||||
return match ($vid) {
|
||||
self::TEST_TYPE_TEST => 'TEST',
|
||||
self::TEST_TYPE_PARAM => 'PARAM',
|
||||
self::TEST_TYPE_CALC => 'CALC',
|
||||
self::TEST_TYPE_GROUP => 'GROUP',
|
||||
self::TEST_TYPE_TITLE => 'TITLE',
|
||||
default => 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip test if database not available
|
||||
*/
|
||||
protected function requireDatabase(): void
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
try {
|
||||
$db->connect();
|
||||
} catch (\Exception $e) {
|
||||
$this->markTestSkipped('Database not available: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip test if required seeded data not found
|
||||
*/
|
||||
protected function requireSeededData(): void
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$count = $db->table('valueset')
|
||||
->where('VSetID', self::VALUESET_TEST_TYPE)
|
||||
->countAllResults();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->markTestSkipped('Test type valuesets not seeded');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,258 +2,373 @@
|
||||
|
||||
namespace Tests\Feature\TestDef;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\DatabaseTestTrait;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
use App\Models\Test\TestDefTechModel;
|
||||
use App\Models\Test\TestDefCalModel;
|
||||
use App\Models\Test\TestDefGrpModel;
|
||||
|
||||
/**
|
||||
* Integration tests for Test Definitions API
|
||||
*
|
||||
* Tests the CRUD operations for test definitions through the API
|
||||
*/
|
||||
class TestDefSiteTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
use DatabaseTestTrait;
|
||||
|
||||
protected $endpoint = 'api/tests';
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Generate Token
|
||||
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
|
||||
$payload = [
|
||||
'iss' => 'localhost',
|
||||
'aud' => 'localhost',
|
||||
'iat' => time(),
|
||||
'nbf' => time(),
|
||||
'exp' => time() + 3600,
|
||||
'uid' => 1,
|
||||
'email' => 'admin@admin.com'
|
||||
];
|
||||
$this->token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256');
|
||||
}
|
||||
|
||||
public function get(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('get', $path, $options);
|
||||
}
|
||||
|
||||
public function post(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('post', $path, $options);
|
||||
}
|
||||
|
||||
public function put(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('put', $path, $options);
|
||||
}
|
||||
|
||||
public function delete(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('delete', $path, $options);
|
||||
}
|
||||
protected $seed = 'App\Database\Seeds\TestSeeder';
|
||||
protected $refresh = true;
|
||||
|
||||
/**
|
||||
* Test index endpoint returns list of tests
|
||||
* Test listing all tests returns success response
|
||||
*/
|
||||
public function testIndexReturnsTestList()
|
||||
public function testIndexReturnsSuccessResponse(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint);
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests');
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('status', $body);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
$this->assertArrayHasKey('message', $body);
|
||||
$result->assertJSONExact([
|
||||
'status' => 'success',
|
||||
'message' => 'Data fetched successfully',
|
||||
'data' => $result->getJSON(true)['data']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index with SiteID filter
|
||||
* Test listing all tests returns array
|
||||
*/
|
||||
public function testIndexWithSiteFilter()
|
||||
public function testIndexReturnsArray(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?SiteID=1');
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests');
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertIsArray($response['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index with TestType filter
|
||||
* Test index contains test type information
|
||||
*/
|
||||
public function testIndexWithTypeFilter()
|
||||
public function testIndexContainsTypeInformation(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?TestType=TEST');
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests');
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
$response = $result->getJSON(true);
|
||||
|
||||
if (!empty($response['data'])) {
|
||||
$test = $response['data'][0];
|
||||
$this->assertArrayHasKey('TypeCode', $test);
|
||||
$this->assertArrayHasKey('TypeName', $test);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test show endpoint returns single test
|
||||
* Test filtering by test type
|
||||
*/
|
||||
public function testShowReturnsSingleTest()
|
||||
public function testIndexFiltersByTestType(): void
|
||||
{
|
||||
// First get the list to find a valid ID
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$firstItem = $indexBody['data'][0];
|
||||
$testSiteID = $firstItem['TestSiteID'] ?? 1;
|
||||
|
||||
$showResult = $this->get($this->endpoint . '/' . $testSiteID);
|
||||
$showResult->assertStatus(200);
|
||||
|
||||
$body = json_decode($showResult->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
|
||||
// Check that related details are loaded based on TestType
|
||||
if ($body['data'] !== null) {
|
||||
$typeCode = $body['data']['TypeCode'] ?? '';
|
||||
if ($typeCode === 'CALC') {
|
||||
$this->assertArrayHasKey('testdefcal', $body['data']);
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
$this->assertArrayHasKey('testdefgrp', $body['data']);
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
$this->assertArrayHasKey('testdeftech', $body['data']);
|
||||
}
|
||||
// All types should have testmap
|
||||
$this->assertArrayHasKey('testmap', $body['data']);
|
||||
// Test filtering by TEST type (VID = 1)
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests?TestType=1');
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
|
||||
foreach ($response['data'] as $test) {
|
||||
$this->assertEquals('TEST', $test['TypeCode']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test filtering by keyword
|
||||
*/
|
||||
public function testIndexFiltersByKeyword(): void
|
||||
{
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests?TestSiteName=HB');
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
|
||||
if (!empty($response['data'])) {
|
||||
foreach ($response['data'] as $test) {
|
||||
$this->assertStringContainsString('HB', $test['TestSiteName']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test show with non-existent ID returns null data
|
||||
* Test showing single test returns success
|
||||
*/
|
||||
public function testShowWithInvalidIDReturnsNull()
|
||||
public function testShowReturnsSuccess(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '/9999999');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
$this->assertNull($body['data']);
|
||||
// Get a test ID from the seeder data
|
||||
$model = new TestDefSiteModel();
|
||||
$test = $model->first();
|
||||
|
||||
if ($test) {
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get("api/tests/{$test['TestSiteID']}");
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertArrayHasKey('data', $response);
|
||||
} else {
|
||||
$this->markTestSkipped('No test data available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create new test definition
|
||||
* Test showing single test includes type-specific details for TEST type
|
||||
*/
|
||||
public function testCreateTest()
|
||||
public function testShowIncludesTechDetailsForTestType(): void
|
||||
{
|
||||
$model = new TestDefSiteModel();
|
||||
$test = $model->first();
|
||||
|
||||
if ($test && $test['TypeCode'] === 'TEST') {
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get("api/tests/{$test['TestSiteID']}");
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertArrayHasKey('testdeftech', $response['data']);
|
||||
} else {
|
||||
$this->markTestSkipped('No TEST type data available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test showing single test includes type-specific details for CALC type
|
||||
*/
|
||||
public function testShowIncludesCalcDetailsForCalcType(): void
|
||||
{
|
||||
$model = new TestDefSiteModel();
|
||||
$test = $model->first();
|
||||
|
||||
if ($test && $test['TypeCode'] === 'CALC') {
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get("api/tests/{$test['TestSiteID']}");
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertArrayHasKey('testdefcal', $response['data']);
|
||||
} else {
|
||||
$this->markTestSkipped('No CALC type data available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test showing single test includes type-specific details for GROUP type
|
||||
*/
|
||||
public function testShowIncludesGrpDetailsForGroupType(): void
|
||||
{
|
||||
$model = new TestDefSiteModel();
|
||||
$test = $model->first();
|
||||
|
||||
if ($test && $test['TypeCode'] === 'GROUP') {
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get("api/tests/{$test['TestSiteID']}");
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertArrayHasKey('testdefgrp', $response['data']);
|
||||
} else {
|
||||
$this->markTestSkipped('No GROUP type data available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a new test
|
||||
*/
|
||||
public function testCreateTest(): void
|
||||
{
|
||||
$testData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'HB',
|
||||
'TestSiteName' => 'Hemoglobin',
|
||||
'TestType' => 'TEST',
|
||||
'Description' => 'Hemoglobin concentration test',
|
||||
'SeqScr' => 3,
|
||||
'SeqRpt' => 3,
|
||||
'IndentLeft' => 0,
|
||||
'FontStyle' => 'Bold',
|
||||
'TestSiteCode' => 'NEWTEST',
|
||||
'TestSiteName' => 'New Test',
|
||||
'TestType' => 1, // TEST type
|
||||
'Description' => 'Test description',
|
||||
'SeqScr' => 100,
|
||||
'SeqRpt' => 100,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'StartDate' => date('Y-m-d H:i:s')
|
||||
'CountStat' => 1
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($testData)]);
|
||||
|
||||
// If validation fails due to duplicate code, that's expected
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(in_array($status, [201, 400]), "Expected 201 or 400, got $status");
|
||||
|
||||
if ($status === 201) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('created', $body['status']);
|
||||
$this->assertArrayHasKey('TestSiteId', $body['data']);
|
||||
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->post('api/tests', $testData);
|
||||
|
||||
$result->assertStatus(201);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertArrayHasKey('data', $response);
|
||||
$this->assertArrayHasKey('TestSiteId', $response['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating test with validation error (missing required fields)
|
||||
*/
|
||||
public function testCreateTestValidationError(): void
|
||||
{
|
||||
$testData = [
|
||||
'SiteID' => 1
|
||||
// Missing required fields
|
||||
];
|
||||
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->post('api/tests', $testData);
|
||||
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a test
|
||||
*/
|
||||
public function testUpdateTest(): void
|
||||
{
|
||||
$model = new TestDefSiteModel();
|
||||
$test = $model->first();
|
||||
|
||||
if ($test) {
|
||||
$updateData = [
|
||||
'TestSiteID' => $test['TestSiteID'],
|
||||
'TestSiteName' => 'Updated Test Name',
|
||||
'Description' => 'Updated description'
|
||||
];
|
||||
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->patch('api/tests', $updateData);
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertEquals('success', $response['status']);
|
||||
} else {
|
||||
$this->markTestSkipped('No test data available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update existing test
|
||||
* Test deleting a test (soft delete)
|
||||
*/
|
||||
public function testUpdateTest()
|
||||
public function testDeleteTest(): void
|
||||
{
|
||||
// Get a valid test ID first
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$firstItem = $indexBody['data'][0];
|
||||
$testSiteID = $firstItem['TestSiteID'] ?? null;
|
||||
|
||||
if ($testSiteID) {
|
||||
$updateData = [
|
||||
'TestSiteName' => 'Updated Test Name',
|
||||
'Description' => 'Updated description'
|
||||
];
|
||||
|
||||
$result = $this->put($this->endpoint . '/' . $testSiteID, ['body' => json_encode($updateData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(in_array($status, [200, 404]), "Expected 200 or 404, got $status");
|
||||
|
||||
if ($status === 200) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
$model = new TestDefSiteModel();
|
||||
$test = $model->first();
|
||||
|
||||
if ($test) {
|
||||
$deleteData = [
|
||||
'TestSiteID' => $test['TestSiteID']
|
||||
];
|
||||
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->delete('api/tests', $deleteData);
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertEquals('success', $response['status']);
|
||||
$this->assertArrayHasKey('EndDate', $response['data']);
|
||||
} else {
|
||||
$this->markTestSkipped('No test data available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting non-existent test returns empty data
|
||||
*/
|
||||
public function testShowNonExistentTest(): void
|
||||
{
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests/999999');
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
$this->assertNull($response['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test test types are correctly mapped from valueset
|
||||
*/
|
||||
public function testTestTypesAreMapped(): void
|
||||
{
|
||||
$model = new TestDefSiteModel();
|
||||
$tests = $model->findAll();
|
||||
|
||||
$validTypes = ['TEST', 'PARAM', 'CALC', 'GROUP', 'TITLE'];
|
||||
|
||||
foreach ($tests as $test) {
|
||||
if (isset($test['TypeCode'])) {
|
||||
$this->assertContains($test['TypeCode'], $validTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test soft delete (disable) test
|
||||
* Test filtering by visible on screen
|
||||
*/
|
||||
public function testDeleteTest()
|
||||
public function testIndexFiltersByVisibleScr(): void
|
||||
{
|
||||
// Get a valid test ID first
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$firstItem = $indexBody['data'][0];
|
||||
$testSiteID = $firstItem['TestSiteID'] ?? null;
|
||||
|
||||
if ($testSiteID) {
|
||||
$result = $this->delete($this->endpoint . '/' . $testSiteID);
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(in_array($status, [200, 404]), "Expected 200 or 404, got $status");
|
||||
|
||||
if ($status === 200) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
$this->assertArrayHasKey('EndDate', $body['data']);
|
||||
}
|
||||
}
|
||||
$result = $this->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
])->get('api/tests?VisibleScr=1');
|
||||
|
||||
$result->assertStatus(200);
|
||||
$response = $result->getJSON(true);
|
||||
|
||||
foreach ($response['data'] as $test) {
|
||||
$this->assertEquals(1, $test['VisibleScr']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation - missing required fields
|
||||
* Test all test types from seeder are present
|
||||
*/
|
||||
public function testCreateValidation()
|
||||
public function testAllTestTypesArePresent(): void
|
||||
{
|
||||
$invalidData = [
|
||||
'TestSiteName' => 'Test without code'
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
$model = new TestDefSiteModel();
|
||||
$tests = $model->findAll();
|
||||
|
||||
/**
|
||||
* Test that TestSiteCode is max 6 characters
|
||||
*/
|
||||
public function testTestSiteCodeLength()
|
||||
{
|
||||
$invalidData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'HB123456', // 8 characters - invalid
|
||||
'TestSiteName' => 'Test with too long code',
|
||||
'TestType' => 'TEST'
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
$typeCodes = array_column($tests, 'TypeCode');
|
||||
$uniqueTypes = array_unique($typeCodes);
|
||||
|
||||
// Check that we have at least TEST and PARAM types from seeder
|
||||
$this->assertContains('TEST', $uniqueTypes);
|
||||
$this->assertContains('PARAM', $uniqueTypes);
|
||||
$this->assertContains('CALC', $uniqueTypes);
|
||||
$this->assertContains('GROUP', $uniqueTypes);
|
||||
}
|
||||
}
|
||||
|
||||
328
tests/feature/v2/master/TestDef/TestDefCalcTest.php
Normal file
328
tests/feature/v2/master/TestDef/TestDefCalcTest.php
Normal file
@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\v2\master\TestDef;
|
||||
|
||||
use Tests\Support\v2\MasterTestCase;
|
||||
|
||||
/**
|
||||
* Feature tests for CALC type test definitions
|
||||
*
|
||||
* Tests CALC-specific functionality including formula configuration
|
||||
*/
|
||||
class TestDefCalcTest extends MasterTestCase
|
||||
{
|
||||
protected string $endpoint = 'v2/master/tests';
|
||||
|
||||
/**
|
||||
* Test create CALC with formula
|
||||
*/
|
||||
public function testCreateCalcWithFormula(): void
|
||||
{
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'CALC' . substr(time(), -4),
|
||||
'TestSiteName' => 'Calculated Test ' . time(),
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'Description' => 'Calculated test with formula',
|
||||
'SeqScr' => 50,
|
||||
'SeqRpt' => 50,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'details' => [
|
||||
'DisciplineID' => 1,
|
||||
'DepartmentID' => 1,
|
||||
'FormulaInput' => '["CHOL", "HDL", "TG"]',
|
||||
'FormulaCode' => 'CHOL - HDL - (TG / 5)',
|
||||
'FormulaLang' => 'SQL',
|
||||
'RefType' => 1, // NMRC
|
||||
'Unit1' => 'mg/dL',
|
||||
'Decimal' => 0,
|
||||
'Method' => 'Friedewald Formula'
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
|
||||
if ($status === 201) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('created', $body['status']);
|
||||
|
||||
// Verify calc details were created
|
||||
$calcId = $body['data']['TestSiteId'];
|
||||
$showResult = $this->get($this->endpoint . '/' . $calcId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
if ($showBody['data'] !== null) {
|
||||
$this->assertArrayHasKey('testdefcal', $showBody['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC with different formula languages
|
||||
*/
|
||||
public function testCalcWithDifferentFormulaLanguages(): void
|
||||
{
|
||||
$languages = ['Phyton', 'CQL', 'FHIRP', 'SQL'];
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'C' . substr(time(), -5) . strtoupper(substr($lang, 0, 1)),
|
||||
'TestSiteName' => "Calc with $lang",
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'details' => [
|
||||
'FormulaInput' => '["TEST1"]',
|
||||
'FormulaCode' => 'TEST1 * 2',
|
||||
'FormulaLang' => $lang
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"CALC with $lang: Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC with JSON formula input
|
||||
*/
|
||||
public function testCalcWithJsonFormulaInput(): void
|
||||
{
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'CJSN' . substr(time(), -3),
|
||||
'TestSiteName' => 'Calc with JSON Input',
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'details' => [
|
||||
'FormulaInput' => '["parameter1", "parameter2", "parameter3"]',
|
||||
'FormulaCode' => '(param1 + param2) / param3',
|
||||
'FormulaLang' => 'FHIRP'
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC with complex formula
|
||||
*/
|
||||
public function testCalcWithComplexFormula(): void
|
||||
{
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'CCMP' . substr(time(), -3),
|
||||
'TestSiteName' => 'Calc with Complex Formula',
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'details' => [
|
||||
'FormulaInput' => '["WBC", "NEUT", "LYMPH", "MONO", "EOS", "BASO"]',
|
||||
'FormulaCode' => 'if WBC > 0 then (NEUT + LYMPH + MONO + EOS + BASO) / WBC * 100 else 0',
|
||||
'FormulaLang' => 'Phyton'
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update CALC formula
|
||||
*/
|
||||
public function testUpdateCalcFormula(): void
|
||||
{
|
||||
// Create a CALC first
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'UPCL' . substr(time(), -4),
|
||||
'TestSiteName' => 'Update Calc Test',
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'details' => [
|
||||
'FormulaInput' => '["A", "B"]',
|
||||
'FormulaCode' => 'A + B',
|
||||
'FormulaLang' => 'SQL'
|
||||
]
|
||||
];
|
||||
|
||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
$createStatus = $createResult->response()->getStatusCode();
|
||||
|
||||
if ($createStatus === 201) {
|
||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
||||
$calcId = $createBody['data']['TestSiteId'] ?? null;
|
||||
|
||||
if ($calcId) {
|
||||
// Update formula
|
||||
$updateData = [
|
||||
'TestSiteName' => 'Updated Calc Test Name',
|
||||
'details' => [
|
||||
'FormulaInput' => '["A", "B", "C"]',
|
||||
'FormulaCode' => 'A + B + C'
|
||||
]
|
||||
];
|
||||
|
||||
$updateResult = $this->put($this->endpoint . '/' . $calcId, ['body' => json_encode($updateData)]);
|
||||
$updateStatus = $updateResult->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($updateStatus, [200, 400, 500]),
|
||||
"Expected 200, 400, or 500, got $updateStatus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC has correct TypeCode in response
|
||||
*/
|
||||
public function testCalcTypeCodeInResponse(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=CALC');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$calc = $indexBody['data'][0];
|
||||
|
||||
// Verify TypeCode is CALC
|
||||
$this->assertEquals('CALC', $calc['TypeCode'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC details structure
|
||||
*/
|
||||
public function testCalcDetailsStructure(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=CALC');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$calc = $indexBody['data'][0];
|
||||
$calcId = $calc['TestSiteID'] ?? null;
|
||||
|
||||
if ($calcId) {
|
||||
$showResult = $this->get($this->endpoint . '/' . $calcId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
if ($showBody['data'] !== null && isset($showBody['data']['testdefcal'])) {
|
||||
$calcDetails = $showBody['data']['testdefcal'];
|
||||
|
||||
if (is_array($calcDetails) && !empty($calcDetails)) {
|
||||
$firstDetail = $calcDetails[0];
|
||||
|
||||
// Check required fields in calc structure
|
||||
$this->assertArrayHasKey('TestCalID', $firstDetail);
|
||||
$this->assertArrayHasKey('TestSiteID', $firstDetail);
|
||||
$this->assertArrayHasKey('FormulaInput', $firstDetail);
|
||||
$this->assertArrayHasKey('FormulaCode', $firstDetail);
|
||||
|
||||
// Check for joined discipline/department
|
||||
if (isset($firstDetail['DisciplineName'])) {
|
||||
$this->assertArrayHasKey('DepartmentName', $firstDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC delete cascades to details
|
||||
*/
|
||||
public function testCalcDeleteCascadesToDetails(): void
|
||||
{
|
||||
// Create a CALC
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'CDEL' . substr(time(), -4),
|
||||
'TestSiteName' => 'Calc to Delete',
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'details' => [
|
||||
'FormulaInput' => '["TEST1"]',
|
||||
'FormulaCode' => 'TEST1 * 2'
|
||||
]
|
||||
];
|
||||
|
||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
$createStatus = $createResult->response()->getStatusCode();
|
||||
|
||||
if ($createStatus === 201) {
|
||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
||||
$calcId = $createBody['data']['TestSiteId'] ?? null;
|
||||
|
||||
if ($calcId) {
|
||||
// Delete the CALC
|
||||
$deleteResult = $this->delete($this->endpoint . '/' . $calcId);
|
||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($deleteStatus, [200, 404, 500]),
|
||||
"Expected 200, 404, or 500, got $deleteStatus"
|
||||
);
|
||||
|
||||
if ($deleteStatus === 200) {
|
||||
// Verify CALC details are also soft deleted
|
||||
$showResult = $this->get($this->endpoint . '/' . $calcId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
// CALC should show EndDate set
|
||||
if ($showBody['data'] !== null) {
|
||||
$this->assertNotNull($showBody['data']['EndDate']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CALC with result unit configuration
|
||||
*/
|
||||
public function testCalcWithResultUnit(): void
|
||||
{
|
||||
$units = ['mg/dL', 'g/L', 'mmol/L', '%', 'IU/L'];
|
||||
|
||||
foreach ($units as $unit) {
|
||||
$calcData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'CUNT' . substr(time(), -3) . substr($unit, 0, 1),
|
||||
'TestSiteName' => "Calc with $unit",
|
||||
'TestType' => $this::TEST_TYPE_CALC,
|
||||
'details' => [
|
||||
'Unit1' => $unit,
|
||||
'Decimal' => 2,
|
||||
'FormulaInput' => '["TEST1"]',
|
||||
'FormulaCode' => 'TEST1'
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"CALC with unit $unit: Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
291
tests/feature/v2/master/TestDef/TestDefGroupTest.php
Normal file
291
tests/feature/v2/master/TestDef/TestDefGroupTest.php
Normal file
@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\v2\master\TestDef;
|
||||
|
||||
use Tests\Support\v2\MasterTestCase;
|
||||
|
||||
/**
|
||||
* Feature tests for GROUP type test definitions
|
||||
*
|
||||
* Tests GROUP-specific functionality including member management
|
||||
*/
|
||||
class TestDefGroupTest extends MasterTestCase
|
||||
{
|
||||
protected string $endpoint = 'v2/master/tests';
|
||||
|
||||
/**
|
||||
* Test create GROUP with members
|
||||
*/
|
||||
public function testCreateGroupWithMembers(): void
|
||||
{
|
||||
// Get existing test IDs to use as members
|
||||
$memberIds = $this->getExistingTestIds();
|
||||
|
||||
$groupData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'GRUP' . substr(time(), -4),
|
||||
'TestSiteName' => 'Test Group ' . time(),
|
||||
'TestType' => $this::TEST_TYPE_GROUP,
|
||||
'Description' => 'Group test with members',
|
||||
'SeqScr' => 100,
|
||||
'SeqRpt' => 100,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'Members' => $memberIds
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
|
||||
if ($status === 201) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('created', $body['status']);
|
||||
|
||||
// Verify members were created
|
||||
$groupId = $body['data']['TestSiteId'];
|
||||
$showResult = $this->get($this->endpoint . '/' . $groupId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
if ($showBody['data'] !== null) {
|
||||
$this->assertArrayHasKey('testdefgrp', $showBody['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create GROUP without members
|
||||
*/
|
||||
public function testCreateGroupWithoutMembers(): void
|
||||
{
|
||||
$groupData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'GREM' . substr(time(), -4),
|
||||
'TestSiteName' => 'Empty Group ' . time(),
|
||||
'TestType' => $this::TEST_TYPE_GROUP,
|
||||
'Members' => [] // Empty members
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
||||
|
||||
// Should still succeed but with warning or empty members
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400]),
|
||||
"Expected 201 or 400, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update GROUP members
|
||||
*/
|
||||
public function testUpdateGroupMembers(): void
|
||||
{
|
||||
// Create a group first
|
||||
$memberIds = $this->getExistingTestIds();
|
||||
$groupData = $this->createGroupData($memberIds);
|
||||
$groupData['TestSiteCode'] = 'UPMB' . substr(time(), -4);
|
||||
|
||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
||||
$createStatus = $createResult->response()->getStatusCode();
|
||||
|
||||
if ($createStatus === 201) {
|
||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
||||
$groupId = $createBody['data']['TestSiteId'] ?? null;
|
||||
|
||||
if ($groupId) {
|
||||
// Update with new members
|
||||
$updateData = [
|
||||
'Members' => array_slice($memberIds, 0, 1) // Only one member
|
||||
];
|
||||
|
||||
$updateResult = $this->put($this->endpoint . '/' . $groupId, ['body' => json_encode($updateData)]);
|
||||
$updateStatus = $updateResult->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($updateStatus, [200, 400, 500]),
|
||||
"Expected 200, 400, or 500, got $updateStatus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add member to existing GROUP
|
||||
*/
|
||||
public function testAddMemberToGroup(): void
|
||||
{
|
||||
// Get existing test
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=GROUP');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$group = $indexBody['data'][0];
|
||||
$groupId = $group['TestSiteID'] ?? null;
|
||||
|
||||
if ($groupId) {
|
||||
// Get a test ID to add
|
||||
$testIds = $this->getExistingTestIds();
|
||||
$newMemberId = $testIds[0] ?? 1;
|
||||
|
||||
$updateData = [
|
||||
'Members' => [$newMemberId]
|
||||
];
|
||||
|
||||
$result = $this->put($this->endpoint . '/' . $groupId, ['body' => json_encode($updateData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [200, 400, 500]),
|
||||
"Expected 200, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test GROUP with single member
|
||||
*/
|
||||
public function testGroupWithSingleMember(): void
|
||||
{
|
||||
$memberIds = $this->getExistingTestIds();
|
||||
|
||||
$groupData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'GSGL' . substr(time(), -4),
|
||||
'TestSiteName' => 'Single Member Group ' . time(),
|
||||
'TestType' => $this::TEST_TYPE_GROUP,
|
||||
'Members' => [array_slice($memberIds, 0, 1)[0] ?? 1]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test GROUP members have correct structure
|
||||
*/
|
||||
public function testGroupMembersStructure(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=GROUP');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$group = $indexBody['data'][0];
|
||||
$groupId = $group['TestSiteID'] ?? null;
|
||||
|
||||
if ($groupId) {
|
||||
$showResult = $this->get($this->endpoint . '/' . $groupId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
if ($showBody['data'] !== null && isset($showBody['data']['testdefgrp'])) {
|
||||
$members = $showBody['data']['testdefgrp'];
|
||||
|
||||
if (is_array($members) && !empty($members)) {
|
||||
$firstMember = $members[0];
|
||||
|
||||
// Check required fields in member structure
|
||||
$this->assertArrayHasKey('TestGrpID', $firstMember);
|
||||
$this->assertArrayHasKey('TestSiteID', $firstMember);
|
||||
$this->assertArrayHasKey('Member', $firstMember);
|
||||
|
||||
// Check for joined test details (if loaded)
|
||||
if (isset($firstMember['TestSiteCode'])) {
|
||||
$this->assertArrayHasKey('TestSiteName', $firstMember);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test GROUP delete cascades to members
|
||||
*/
|
||||
public function testGroupDeleteCascadesToMembers(): void
|
||||
{
|
||||
// Create a group
|
||||
$memberIds = $this->getExistingTestIds();
|
||||
$groupData = $this->createGroupData($memberIds);
|
||||
$groupData['TestSiteCode'] = 'GDEL' . substr(time(), -4);
|
||||
|
||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
||||
$createStatus = $createResult->response()->getStatusCode();
|
||||
|
||||
if ($createStatus === 201) {
|
||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
||||
$groupId = $createBody['data']['TestSiteId'] ?? null;
|
||||
|
||||
if ($groupId) {
|
||||
// Delete the group
|
||||
$deleteResult = $this->delete($this->endpoint . '/' . $groupId);
|
||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($deleteStatus, [200, 404, 500]),
|
||||
"Expected 200, 404, or 500, got $deleteStatus"
|
||||
);
|
||||
|
||||
if ($deleteStatus === 200) {
|
||||
// Verify group members are also soft deleted
|
||||
$showResult = $this->get($this->endpoint . '/' . $groupId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
// Group should show EndDate set
|
||||
if ($showBody['data'] !== null) {
|
||||
$this->assertNotNull($showBody['data']['EndDate']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test GROUP type has correct TypeCode in response
|
||||
*/
|
||||
public function testGroupTypeCodeInResponse(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=GROUP');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$group = $indexBody['data'][0];
|
||||
|
||||
// Verify TypeCode is GROUP
|
||||
$this->assertEquals('GROUP', $group['TypeCode'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get existing test IDs
|
||||
*/
|
||||
private function getExistingTestIds(): array
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
$ids = [];
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data'])) {
|
||||
foreach ($indexBody['data'] as $item) {
|
||||
if (isset($item['TestSiteID'])) {
|
||||
$ids[] = $item['TestSiteID'];
|
||||
}
|
||||
if (count($ids) >= 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ids ?: [1, 2, 3];
|
||||
}
|
||||
}
|
||||
288
tests/feature/v2/master/TestDef/TestDefParamTest.php
Normal file
288
tests/feature/v2/master/TestDef/TestDefParamTest.php
Normal file
@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\v2\master\TestDef;
|
||||
|
||||
use Tests\Support\v2\MasterTestCase;
|
||||
|
||||
/**
|
||||
* Feature tests for PARAM type test definitions
|
||||
*
|
||||
* Tests PARAM-specific functionality as sub-test components
|
||||
*/
|
||||
class TestDefParamTest extends MasterTestCase
|
||||
{
|
||||
protected string $endpoint = 'v2/master/tests';
|
||||
|
||||
/**
|
||||
* Test create PARAM type test
|
||||
*/
|
||||
public function testCreateParamTypeTest(): void
|
||||
{
|
||||
$paramData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PARM' . substr(time(), -4),
|
||||
'TestSiteName' => 'Parameter Test ' . time(),
|
||||
'TestType' => $this::TEST_TYPE_PARAM,
|
||||
'Description' => 'Parameter/sub-test description',
|
||||
'SeqScr' => 5,
|
||||
'SeqRpt' => 5,
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'details' => [
|
||||
'DisciplineID' => 1,
|
||||
'DepartmentID' => 1,
|
||||
'ResultType' => 1, // Numeric
|
||||
'RefType' => 1, // NMRC
|
||||
'Unit1' => 'unit',
|
||||
'Decimal' => 1,
|
||||
'Method' => 'Parameter Method'
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
|
||||
if ($status === 201) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('created', $body['status']);
|
||||
|
||||
// Verify tech details were created
|
||||
$paramId = $body['data']['TestSiteId'];
|
||||
$showResult = $this->get($this->endpoint . '/' . $paramId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
if ($showBody['data'] !== null) {
|
||||
$this->assertArrayHasKey('testdeftech', $showBody['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM has correct TypeCode in response
|
||||
*/
|
||||
public function testParamTypeCodeInResponse(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=PARAM');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$param = $indexBody['data'][0];
|
||||
|
||||
// Verify TypeCode is PARAM
|
||||
$this->assertEquals('PARAM', $param['TypeCode'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM details structure
|
||||
*/
|
||||
public function testParamDetailsStructure(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint . '?TestType=PARAM');
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$param = $indexBody['data'][0];
|
||||
$paramId = $param['TestSiteID'] ?? null;
|
||||
|
||||
if ($paramId) {
|
||||
$showResult = $this->get($this->endpoint . '/' . $paramId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
if ($showBody['data'] !== null && isset($showBody['data']['testdeftech'])) {
|
||||
$techDetails = $showBody['data']['testdeftech'];
|
||||
|
||||
if (is_array($techDetails) && !empty($techDetails)) {
|
||||
$firstDetail = $techDetails[0];
|
||||
|
||||
// Check required fields in tech structure
|
||||
$this->assertArrayHasKey('TestTechID', $firstDetail);
|
||||
$this->assertArrayHasKey('TestSiteID', $firstDetail);
|
||||
$this->assertArrayHasKey('ResultType', $firstDetail);
|
||||
$this->assertArrayHasKey('RefType', $firstDetail);
|
||||
|
||||
// Check for joined discipline/department
|
||||
if (isset($firstDetail['DisciplineName'])) {
|
||||
$this->assertArrayHasKey('DepartmentName', $firstDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM with different result types
|
||||
*/
|
||||
public function testParamWithDifferentResultTypes(): void
|
||||
{
|
||||
$resultTypes = [
|
||||
1 => 'NMRIC', // Numeric
|
||||
2 => 'RANGE', // Range
|
||||
3 => 'TEXT', // Text
|
||||
4 => 'VSET' // Value Set
|
||||
];
|
||||
|
||||
foreach ($resultTypes as $resultTypeId => $resultTypeName) {
|
||||
$paramData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PR' . substr(time(), -4) . substr($resultTypeName, 0, 1),
|
||||
'TestSiteName' => "Param with $resultTypeName",
|
||||
'TestType' => $this::TEST_TYPE_PARAM,
|
||||
'details' => [
|
||||
'ResultType' => $resultTypeId,
|
||||
'RefType' => 1
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"PARAM with ResultType $resultTypeName: Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM with different reference types
|
||||
*/
|
||||
public function testParamWithDifferentRefTypes(): void
|
||||
{
|
||||
$refTypes = [
|
||||
1 => 'NMRC', // Numeric
|
||||
2 => 'TEXT' // Text
|
||||
];
|
||||
|
||||
foreach ($refTypes as $refTypeId => $refTypeName) {
|
||||
$paramData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PR' . substr(time(), -4) . 'R' . substr($refTypeName, 0, 1),
|
||||
'TestSiteName' => "Param with RefType $refTypeName",
|
||||
'TestType' => $this::TEST_TYPE_PARAM,
|
||||
'details' => [
|
||||
'ResultType' => 1,
|
||||
'RefType' => $refTypeId
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"PARAM with RefType $refTypeName: Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM delete cascades to details
|
||||
*/
|
||||
public function testParamDeleteCascadesToDetails(): void
|
||||
{
|
||||
// Create a PARAM
|
||||
$paramData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PDEL' . substr(time(), -4),
|
||||
'TestSiteName' => 'Param to Delete',
|
||||
'TestType' => $this::TEST_TYPE_PARAM,
|
||||
'details' => [
|
||||
'ResultType' => 1,
|
||||
'RefType' => 1
|
||||
]
|
||||
];
|
||||
|
||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
$createStatus = $createResult->response()->getStatusCode();
|
||||
|
||||
if ($createStatus === 201) {
|
||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
||||
$paramId = $createBody['data']['TestSiteId'] ?? null;
|
||||
|
||||
if ($paramId) {
|
||||
// Delete the PARAM
|
||||
$deleteResult = $this->delete($this->endpoint . '/' . $paramId);
|
||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($deleteStatus, [200, 404, 500]),
|
||||
"Expected 200, 404, or 500, got $deleteStatus"
|
||||
);
|
||||
|
||||
if ($deleteStatus === 200) {
|
||||
// Verify PARAM details are also soft deleted
|
||||
$showResult = $this->get($this->endpoint . '/' . $paramId);
|
||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
||||
|
||||
// PARAM should show EndDate set
|
||||
if ($showBody['data'] !== null) {
|
||||
$this->assertNotNull($showBody['data']['EndDate']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM visibility settings
|
||||
*/
|
||||
public function testParamVisibilitySettings(): void
|
||||
{
|
||||
$visibilityCombinations = [
|
||||
['VisibleScr' => 1, 'VisibleRpt' => 1],
|
||||
['VisibleScr' => 1, 'VisibleRpt' => 0],
|
||||
['VisibleScr' => 0, 'VisibleRpt' => 1],
|
||||
['VisibleScr' => 0, 'VisibleRpt' => 0]
|
||||
];
|
||||
|
||||
foreach ($visibilityCombinations as $vis) {
|
||||
$paramData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PVIS' . substr(time(), -4),
|
||||
'TestSiteName' => 'Visibility Test',
|
||||
'TestType' => $this::TEST_TYPE_PARAM,
|
||||
'VisibleScr' => $vis['VisibleScr'],
|
||||
'VisibleRpt' => $vis['VisibleRpt']
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"PARAM visibility ({$vis['VisibleScr']}, {$vis['VisibleRpt']}): Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PARAM sequence ordering
|
||||
*/
|
||||
public function testParamSequenceOrdering(): void
|
||||
{
|
||||
$paramData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'PSEQ' . substr(time(), -4),
|
||||
'TestSiteName' => 'Sequenced Param',
|
||||
'TestType' => $this::TEST_TYPE_PARAM,
|
||||
'SeqScr' => 25,
|
||||
'SeqRpt' => 30
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
}
|
||||
375
tests/feature/v2/master/TestDef/TestDefSiteTest.php
Normal file
375
tests/feature/v2/master/TestDef/TestDefSiteTest.php
Normal file
@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\v2\master\TestDef;
|
||||
|
||||
use Tests\Support\v2\MasterTestCase;
|
||||
|
||||
/**
|
||||
* Feature tests for v2 Test Definition API endpoints
|
||||
*
|
||||
* Tests CRUD operations for TEST, PARAM, GROUP, and CALC types
|
||||
*/
|
||||
class TestDefSiteTest extends MasterTestCase
|
||||
{
|
||||
protected string $endpoint = 'v2/master/tests';
|
||||
|
||||
/**
|
||||
* Test index endpoint returns list of tests
|
||||
*/
|
||||
public function testIndexReturnsTestList(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint);
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('status', $body);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
$this->assertArrayHasKey('message', $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index with SiteID filter
|
||||
*/
|
||||
public function testIndexWithSiteFilter(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?SiteID=1');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index with TestType filter
|
||||
*/
|
||||
public function testIndexWithTestTypeFilter(): void
|
||||
{
|
||||
// Filter by TEST type
|
||||
$result = $this->get($this->endpoint . '?TestType=TEST');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index with Visibility filter
|
||||
*/
|
||||
public function testIndexWithVisibilityFilter(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?VisibleScr=1&VisibleRpt=1');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index with keyword search
|
||||
*/
|
||||
public function testIndexWithKeywordSearch(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?TestSiteName=hemoglobin');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test show endpoint returns single test
|
||||
*/
|
||||
public function testShowReturnsSingleTest(): void
|
||||
{
|
||||
// First get the list to find a valid ID
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$firstItem = $indexBody['data'][0];
|
||||
$testSiteID = $firstItem['TestSiteID'] ?? null;
|
||||
|
||||
if ($testSiteID) {
|
||||
$showResult = $this->get($this->endpoint . '/' . $testSiteID);
|
||||
$showResult->assertStatus(200);
|
||||
|
||||
$body = json_decode($showResult->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
|
||||
// Check that related details are loaded based on TestType
|
||||
if ($body['data'] !== null) {
|
||||
$typeCode = $body['data']['TypeCode'] ?? '';
|
||||
if ($typeCode === 'CALC') {
|
||||
$this->assertArrayHasKey('testdefcal', $body['data']);
|
||||
} elseif ($typeCode === 'GROUP') {
|
||||
$this->assertArrayHasKey('testdefgrp', $body['data']);
|
||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||
$this->assertArrayHasKey('testdeftech', $body['data']);
|
||||
}
|
||||
// All types should have testmap
|
||||
$this->assertArrayHasKey('testmap', $body['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test show with non-existent ID returns null data
|
||||
*/
|
||||
public function testShowWithInvalidIDReturnsNull(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '/9999999');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
$this->assertNull($body['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create new TEST type test definition
|
||||
*/
|
||||
public function testCreateTestTypeTest(): void
|
||||
{
|
||||
$testData = $this->createTestData();
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($testData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
// Expect 201 (created) or 400 (validation error) or 500 (server error)
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
|
||||
if ($status === 201) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('created', $body['status']);
|
||||
$this->assertArrayHasKey('TestSiteId', $body['data']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create new PARAM type test definition
|
||||
*/
|
||||
public function testCreateParamTypeTest(): void
|
||||
{
|
||||
$paramData = $this->createParamData();
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create new GROUP type test definition
|
||||
*/
|
||||
public function testCreateGroupTypeTest(): void
|
||||
{
|
||||
// First create some member tests
|
||||
$memberIds = $this->getExistingTestIds();
|
||||
|
||||
$groupData = $this->createGroupData($memberIds);
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create new CALC type test definition
|
||||
*/
|
||||
public function testCreateCalcTypeTest(): void
|
||||
{
|
||||
$calcData = $this->createCalcData();
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
||||
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [201, 400, 500]),
|
||||
"Expected 201, 400, or 500, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update existing test
|
||||
*/
|
||||
public function testUpdateTest(): void
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
||||
$firstItem = $indexBody['data'][0];
|
||||
$testSiteID = $firstItem['TestSiteID'] ?? null;
|
||||
|
||||
if ($testSiteID) {
|
||||
$updateData = [
|
||||
'TestSiteName' => 'Updated Test Name ' . time(),
|
||||
'Description' => 'Updated description'
|
||||
];
|
||||
|
||||
$result = $this->put($this->endpoint . '/' . $testSiteID, ['body' => json_encode($updateData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [200, 404, 500]),
|
||||
"Expected 200, 404, or 500, got $status"
|
||||
);
|
||||
|
||||
if ($status === 200) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test soft delete (disable) test
|
||||
*/
|
||||
public function testDeleteTest(): void
|
||||
{
|
||||
// Create a test first to delete
|
||||
$testData = $this->createTestData();
|
||||
$testData['TestSiteCode'] = 'DEL' . substr(time(), -4);
|
||||
|
||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($testData)]);
|
||||
$createStatus = $createResult->response()->getStatusCode();
|
||||
|
||||
if ($createStatus === 201) {
|
||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
||||
$testSiteID = $createBody['data']['TestSiteId'] ?? null;
|
||||
|
||||
if ($testSiteID) {
|
||||
$deleteResult = $this->delete($this->endpoint . '/' . $testSiteID);
|
||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
||||
|
||||
$this->assertTrue(
|
||||
in_array($deleteStatus, [200, 404, 500]),
|
||||
"Expected 200, 404, or 500, got $deleteStatus"
|
||||
);
|
||||
|
||||
if ($deleteStatus === 200) {
|
||||
$deleteBody = json_decode($deleteResult->response()->getBody(), true);
|
||||
$this->assertEquals('success', $deleteBody['status']);
|
||||
$this->assertArrayHasKey('EndDate', $deleteBody['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation - missing required fields
|
||||
*/
|
||||
public function testCreateValidationRequiredFields(): void
|
||||
{
|
||||
$invalidData = [
|
||||
'TestSiteName' => 'Test without required fields'
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that TestSiteCode is max 6 characters
|
||||
*/
|
||||
public function testTestSiteCodeLength(): void
|
||||
{
|
||||
$invalidData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'HB123456', // 8 characters - invalid
|
||||
'TestSiteName' => 'Test with too long code',
|
||||
'TestType' => $this::TEST_TYPE_TEST
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that TestSiteCode is at least 3 characters
|
||||
*/
|
||||
public function testTestSiteCodeMinLength(): void
|
||||
{
|
||||
$invalidData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'HB', // 2 characters - invalid
|
||||
'TestSiteName' => 'Test with too short code',
|
||||
'TestType' => $this::TEST_TYPE_TEST
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that duplicate TestSiteCode is rejected
|
||||
*/
|
||||
public function testDuplicateTestSiteCode(): void
|
||||
{
|
||||
// First create a test
|
||||
$testData = $this->createTestData();
|
||||
$testData['TestSiteCode'] = 'DUP' . substr(time(), -3);
|
||||
|
||||
$this->post($this->endpoint, ['body' => json_encode($testData)]);
|
||||
|
||||
// Try to create another test with the same code
|
||||
$duplicateData = $testData;
|
||||
$duplicateData['TestSiteName'] = 'Different Name';
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($duplicateData)]);
|
||||
|
||||
// Should fail with 400 or 500
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(
|
||||
in_array($status, [400, 500]),
|
||||
"Expected 400 or 500 for duplicate, got $status"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test filtering by multiple parameters
|
||||
*/
|
||||
public function testIndexWithMultipleFilters(): void
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?SiteID=1&TestType=TEST&VisibleScr=1');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get existing test IDs for group members
|
||||
*/
|
||||
private function getExistingTestIds(): array
|
||||
{
|
||||
$indexResult = $this->get($this->endpoint);
|
||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
||||
|
||||
$ids = [];
|
||||
if (isset($indexBody['data']) && is_array($indexBody['data'])) {
|
||||
foreach ($indexBody['data'] as $item) {
|
||||
if (isset($item['TestSiteID'])) {
|
||||
$ids[] = $item['TestSiteID'];
|
||||
}
|
||||
if (count($ids) >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ids ?: [1, 2];
|
||||
}
|
||||
}
|
||||
145
tests/unit/v2/master/TestDef/TestDefCalModelTest.php
Normal file
145
tests/unit/v2/master/TestDef/TestDefCalModelTest.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\v2\master\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefCalModel;
|
||||
|
||||
/**
|
||||
* Unit tests for TestDefCalModel
|
||||
*
|
||||
* Tests the calculation definition model for CALC type tests
|
||||
*/
|
||||
class TestDefCalModelTest extends CIUnitTestCase
|
||||
{
|
||||
protected TestDefCalModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->model = new TestDefCalModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct table name
|
||||
*/
|
||||
public function testModelHasCorrectTableName(): void
|
||||
{
|
||||
$this->assertEquals('testdefcal', $this->model->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct primary key
|
||||
*/
|
||||
public function testModelHasCorrectPrimaryKey(): void
|
||||
{
|
||||
$this->assertEquals('TestCalID', $this->model->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses soft deletes
|
||||
*/
|
||||
public function testModelUsesSoftDeletes(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct allowed fields
|
||||
*/
|
||||
public function testModelHasCorrectAllowedFields(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
|
||||
// Foreign key
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
|
||||
// Calculation fields
|
||||
$this->assertContains('DisciplineID', $allowedFields);
|
||||
$this->assertContains('DepartmentID', $allowedFields);
|
||||
$this->assertContains('FormulaInput', $allowedFields);
|
||||
$this->assertContains('FormulaCode', $allowedFields);
|
||||
|
||||
// Result fields
|
||||
$this->assertContains('RefType', $allowedFields);
|
||||
$this->assertContains('Unit1', $allowedFields);
|
||||
$this->assertContains('Factor', $allowedFields);
|
||||
$this->assertContains('Unit2', $allowedFields);
|
||||
$this->assertContains('Decimal', $allowedFields);
|
||||
$this->assertContains('Method', $allowedFields);
|
||||
|
||||
// Timestamp fields
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses timestamps
|
||||
*/
|
||||
public function testModelUsesTimestamps(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useTimestamps);
|
||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model return type is array
|
||||
*/
|
||||
public function testModelReturnTypeIsArray(): void
|
||||
{
|
||||
$this->assertEquals('array', $this->model->returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct skip validation
|
||||
*/
|
||||
public function testModelSkipValidation(): void
|
||||
{
|
||||
$this->assertFalse($this->model->skipValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct useAutoIncrement
|
||||
*/
|
||||
public function testModelUseAutoIncrement(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useAutoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test FormulaInput field is in allowed fields
|
||||
*/
|
||||
public function testFormulaInputFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('FormulaInput', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test FormulaCode field is in allowed fields
|
||||
*/
|
||||
public function testFormulaCodeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('FormulaCode', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test RefType field is in allowed fields
|
||||
*/
|
||||
public function testRefTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('RefType', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Method field is in allowed fields
|
||||
*/
|
||||
public function testMethodFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('Method', $allowedFields);
|
||||
}
|
||||
}
|
||||
132
tests/unit/v2/master/TestDef/TestDefGrpModelTest.php
Normal file
132
tests/unit/v2/master/TestDef/TestDefGrpModelTest.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\v2\master\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefGrpModel;
|
||||
|
||||
/**
|
||||
* Unit tests for TestDefGrpModel
|
||||
*
|
||||
* Tests the group definition model for GROUP type tests
|
||||
*/
|
||||
class TestDefGrpModelTest extends CIUnitTestCase
|
||||
{
|
||||
protected TestDefGrpModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->model = new TestDefGrpModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct table name
|
||||
*/
|
||||
public function testModelHasCorrectTableName(): void
|
||||
{
|
||||
$this->assertEquals('testdefgrp', $this->model->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct primary key
|
||||
*/
|
||||
public function testModelHasCorrectPrimaryKey(): void
|
||||
{
|
||||
$this->assertEquals('TestGrpID', $this->model->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses soft deletes
|
||||
*/
|
||||
public function testModelUsesSoftDeletes(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct allowed fields
|
||||
*/
|
||||
public function testModelHasCorrectAllowedFields(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
|
||||
// Foreign keys
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
$this->assertContains('Member', $allowedFields);
|
||||
|
||||
// Timestamp fields
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses timestamps
|
||||
*/
|
||||
public function testModelUsesTimestamps(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useTimestamps);
|
||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model return type is array
|
||||
*/
|
||||
public function testModelReturnTypeIsArray(): void
|
||||
{
|
||||
$this->assertEquals('array', $this->model->returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct skip validation
|
||||
*/
|
||||
public function testModelSkipValidation(): void
|
||||
{
|
||||
$this->assertFalse($this->model->skipValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct useAutoIncrement
|
||||
*/
|
||||
public function testModelUseAutoIncrement(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useAutoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteID field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteIDFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Member field is in allowed fields
|
||||
*/
|
||||
public function testMemberFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('Member', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CreateDate field is in allowed fields
|
||||
*/
|
||||
public function testCreateDateFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test EndDate field is in allowed fields
|
||||
*/
|
||||
public function testEndDateFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
}
|
||||
220
tests/unit/v2/master/TestDef/TestDefSiteModelMasterTest.php
Normal file
220
tests/unit/v2/master/TestDef/TestDefSiteModelMasterTest.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\v2\master\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
|
||||
/**
|
||||
* Unit tests for TestDefSiteModel - Master Data Tests CRUD operations
|
||||
*
|
||||
* Tests the model configuration and behavior for test definition management
|
||||
*/
|
||||
class TestDefSiteModelMasterTest extends CIUnitTestCase
|
||||
{
|
||||
protected TestDefSiteModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->model = new TestDefSiteModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct table name
|
||||
*/
|
||||
public function testModelHasCorrectTableName(): void
|
||||
{
|
||||
$this->assertEquals('testdefsite', $this->model->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct primary key
|
||||
*/
|
||||
public function testModelHasCorrectPrimaryKey(): void
|
||||
{
|
||||
$this->assertEquals('TestSiteID', $this->model->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses soft deletes
|
||||
*/
|
||||
public function testModelUsesSoftDeletes(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct allowed fields for master data
|
||||
*/
|
||||
public function testModelHasCorrectAllowedFields(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
|
||||
// Core required fields
|
||||
$this->assertContains('SiteID', $allowedFields);
|
||||
$this->assertContains('TestSiteCode', $allowedFields);
|
||||
$this->assertContains('TestSiteName', $allowedFields);
|
||||
$this->assertContains('TestType', $allowedFields);
|
||||
|
||||
// Display and ordering fields
|
||||
$this->assertContains('Description', $allowedFields);
|
||||
$this->assertContains('SeqScr', $allowedFields);
|
||||
$this->assertContains('SeqRpt', $allowedFields);
|
||||
$this->assertContains('IndentLeft', $allowedFields);
|
||||
$this->assertContains('FontStyle', $allowedFields);
|
||||
|
||||
// Visibility fields
|
||||
$this->assertContains('VisibleScr', $allowedFields);
|
||||
$this->assertContains('VisibleRpt', $allowedFields);
|
||||
$this->assertContains('CountStat', $allowedFields);
|
||||
|
||||
// Timestamp fields
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('StartDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses timestamps
|
||||
*/
|
||||
public function testModelUsesTimestamps(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useTimestamps);
|
||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
||||
$this->assertEquals('StartDate', $this->model->updatedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model return type is array
|
||||
*/
|
||||
public function testModelReturnTypeIsArray(): void
|
||||
{
|
||||
$this->assertEquals('array', $this->model->returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct skip validation
|
||||
*/
|
||||
public function testModelSkipValidation(): void
|
||||
{
|
||||
$this->assertFalse($this->model->skipValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct useAutoIncrement
|
||||
*/
|
||||
public function testModelUseAutoIncrement(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useAutoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteCode field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteCodeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteCode', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteName field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteNameFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteName', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestType field is in allowed fields
|
||||
*/
|
||||
public function testTestTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestType', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SiteID field is in allowed fields
|
||||
*/
|
||||
public function testSiteIDFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('SiteID', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Description field is in allowed fields
|
||||
*/
|
||||
public function testDescriptionFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('Description', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SeqScr field is in allowed fields
|
||||
*/
|
||||
public function testSeqScrFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('SeqScr', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SeqRpt field is in allowed fields
|
||||
*/
|
||||
public function testSeqRptFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('SeqRpt', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test VisibleScr field is in allowed fields
|
||||
*/
|
||||
public function testVisibleScrFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('VisibleScr', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test VisibleRpt field is in allowed fields
|
||||
*/
|
||||
public function testVisibleRptFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('VisibleRpt', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CountStat field is in allowed fields
|
||||
*/
|
||||
public function testCountStatFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('CountStat', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getTests method exists and is callable
|
||||
*/
|
||||
public function testGetTestsMethodExists(): void
|
||||
{
|
||||
$this->assertTrue(method_exists($this->model, 'getTests'));
|
||||
$this->assertIsCallable([$this->model, 'getTests']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getTest method exists and is callable
|
||||
*/
|
||||
public function testGetTestMethodExists(): void
|
||||
{
|
||||
$this->assertTrue(method_exists($this->model, 'getTest'));
|
||||
$this->assertIsCallable([$this->model, 'getTest']);
|
||||
}
|
||||
}
|
||||
137
tests/unit/v2/master/TestDef/TestDefSiteModelTest.php
Normal file
137
tests/unit/v2/master/TestDef/TestDefSiteModelTest.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\v2\master\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
|
||||
/**
|
||||
* Unit tests for TestDefSiteModel
|
||||
*
|
||||
* Tests the main test definition model configuration and behavior
|
||||
*/
|
||||
class TestDefSiteModelTest extends CIUnitTestCase
|
||||
{
|
||||
protected TestDefSiteModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->model = new TestDefSiteModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct table name
|
||||
*/
|
||||
public function testModelHasCorrectTableName(): void
|
||||
{
|
||||
$this->assertEquals('testdefsite', $this->model->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct primary key
|
||||
*/
|
||||
public function testModelHasCorrectPrimaryKey(): void
|
||||
{
|
||||
$this->assertEquals('TestSiteID', $this->model->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses soft deletes
|
||||
*/
|
||||
public function testModelUsesSoftDeletes(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct allowed fields
|
||||
*/
|
||||
public function testModelHasCorrectAllowedFields(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
|
||||
// Core required fields
|
||||
$this->assertContains('SiteID', $allowedFields);
|
||||
$this->assertContains('TestSiteCode', $allowedFields);
|
||||
$this->assertContains('TestSiteName', $allowedFields);
|
||||
$this->assertContains('TestType', $allowedFields);
|
||||
|
||||
// Optional fields
|
||||
$this->assertContains('Description', $allowedFields);
|
||||
$this->assertContains('SeqScr', $allowedFields);
|
||||
$this->assertContains('SeqRpt', $allowedFields);
|
||||
$this->assertContains('IndentLeft', $allowedFields);
|
||||
$this->assertContains('FontStyle', $allowedFields);
|
||||
$this->assertContains('VisibleScr', $allowedFields);
|
||||
$this->assertContains('VisibleRpt', $allowedFields);
|
||||
$this->assertContains('CountStat', $allowedFields);
|
||||
|
||||
// Timestamp fields
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('StartDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses timestamps
|
||||
*/
|
||||
public function testModelUsesTimestamps(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useTimestamps);
|
||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
||||
$this->assertEquals('StartDate', $this->model->updatedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model return type is array
|
||||
*/
|
||||
public function testModelReturnTypeIsArray(): void
|
||||
{
|
||||
$this->assertEquals('array', $this->model->returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct skip validation
|
||||
*/
|
||||
public function testModelSkipValidation(): void
|
||||
{
|
||||
$this->assertFalse($this->model->skipValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct useAutoIncrement
|
||||
*/
|
||||
public function testModelUseAutoIncrement(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useAutoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteCode field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteCodeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteCode', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteName field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteNameFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteName', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestType field is in allowed fields
|
||||
*/
|
||||
public function testTestTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestType', $allowedFields);
|
||||
}
|
||||
}
|
||||
160
tests/unit/v2/master/TestDef/TestDefTechModelTest.php
Normal file
160
tests/unit/v2/master/TestDef/TestDefTechModelTest.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\v2\master\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefTechModel;
|
||||
|
||||
/**
|
||||
* Unit tests for TestDefTechModel
|
||||
*
|
||||
* Tests the technical definition model for TEST and PARAM types
|
||||
*/
|
||||
class TestDefTechModelTest extends CIUnitTestCase
|
||||
{
|
||||
protected TestDefTechModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->model = new TestDefTechModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct table name
|
||||
*/
|
||||
public function testModelHasCorrectTableName(): void
|
||||
{
|
||||
$this->assertEquals('testdeftech', $this->model->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct primary key
|
||||
*/
|
||||
public function testModelHasCorrectPrimaryKey(): void
|
||||
{
|
||||
$this->assertEquals('TestTechID', $this->model->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses soft deletes
|
||||
*/
|
||||
public function testModelUsesSoftDeletes(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct allowed fields
|
||||
*/
|
||||
public function testModelHasCorrectAllowedFields(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
|
||||
// Foreign key
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
|
||||
// Technical fields
|
||||
$this->assertContains('DisciplineID', $allowedFields);
|
||||
$this->assertContains('DepartmentID', $allowedFields);
|
||||
$this->assertContains('ResultType', $allowedFields);
|
||||
$this->assertContains('RefType', $allowedFields);
|
||||
$this->assertContains('VSet', $allowedFields);
|
||||
|
||||
// Quantity and units
|
||||
$this->assertContains('ReqQty', $allowedFields);
|
||||
$this->assertContains('ReqQtyUnit', $allowedFields);
|
||||
$this->assertContains('Unit1', $allowedFields);
|
||||
$this->assertContains('Factor', $allowedFields);
|
||||
$this->assertContains('Unit2', $allowedFields);
|
||||
$this->assertContains('Decimal', $allowedFields);
|
||||
|
||||
// Collection and method
|
||||
$this->assertContains('CollReq', $allowedFields);
|
||||
$this->assertContains('Method', $allowedFields);
|
||||
$this->assertContains('ExpectedTAT', $allowedFields);
|
||||
|
||||
// Timestamp fields
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses timestamps
|
||||
*/
|
||||
public function testModelUsesTimestamps(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useTimestamps);
|
||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model return type is array
|
||||
*/
|
||||
public function testModelReturnTypeIsArray(): void
|
||||
{
|
||||
$this->assertEquals('array', $this->model->returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct skip validation
|
||||
*/
|
||||
public function testModelSkipValidation(): void
|
||||
{
|
||||
$this->assertFalse($this->model->skipValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct useAutoIncrement
|
||||
*/
|
||||
public function testModelUseAutoIncrement(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useAutoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteID field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteIDFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ResultType field is in allowed fields
|
||||
*/
|
||||
public function testResultTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('ResultType', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test RefType field is in allowed fields
|
||||
*/
|
||||
public function testRefTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('RefType', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Unit1 field is in allowed fields
|
||||
*/
|
||||
public function testUnit1FieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('Unit1', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Method field is in allowed fields
|
||||
*/
|
||||
public function testMethodFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('Method', $allowedFields);
|
||||
}
|
||||
}
|
||||
155
tests/unit/v2/master/TestDef/TestMapModelTest.php
Normal file
155
tests/unit/v2/master/TestDef/TestMapModelTest.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\v2\master\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestMapModel;
|
||||
|
||||
/**
|
||||
* Unit tests for TestMapModel
|
||||
*
|
||||
* Tests the test mapping model for all test types
|
||||
*/
|
||||
class TestMapModelTest extends CIUnitTestCase
|
||||
{
|
||||
protected TestMapModel $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->model = new TestMapModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct table name
|
||||
*/
|
||||
public function testModelHasCorrectTableName(): void
|
||||
{
|
||||
$this->assertEquals('testmap', $this->model->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct primary key
|
||||
*/
|
||||
public function testModelHasCorrectPrimaryKey(): void
|
||||
{
|
||||
$this->assertEquals('TestMapID', $this->model->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses soft deletes
|
||||
*/
|
||||
public function testModelUsesSoftDeletes(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct allowed fields
|
||||
*/
|
||||
public function testModelHasCorrectAllowedFields(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
|
||||
// Foreign key
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
|
||||
// Host system mapping
|
||||
$this->assertContains('HostType', $allowedFields);
|
||||
$this->assertContains('HostID', $allowedFields);
|
||||
$this->assertContains('HostDataSource', $allowedFields);
|
||||
$this->assertContains('HostTestCode', $allowedFields);
|
||||
$this->assertContains('HostTestName', $allowedFields);
|
||||
|
||||
// Client system mapping
|
||||
$this->assertContains('ClientType', $allowedFields);
|
||||
$this->assertContains('ClientID', $allowedFields);
|
||||
$this->assertContains('ClientDataSource', $allowedFields);
|
||||
$this->assertContains('ConDefID', $allowedFields);
|
||||
$this->assertContains('ClientTestCode', $allowedFields);
|
||||
$this->assertContains('ClientTestName', $allowedFields);
|
||||
|
||||
// Timestamp fields
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model uses timestamps
|
||||
*/
|
||||
public function testModelUsesTimestamps(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useTimestamps);
|
||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model return type is array
|
||||
*/
|
||||
public function testModelReturnTypeIsArray(): void
|
||||
{
|
||||
$this->assertEquals('array', $this->model->returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct skip validation
|
||||
*/
|
||||
public function testModelSkipValidation(): void
|
||||
{
|
||||
$this->assertFalse($this->model->skipValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model has correct useAutoIncrement
|
||||
*/
|
||||
public function testModelUseAutoIncrement(): void
|
||||
{
|
||||
$this->assertTrue($this->model->useAutoIncrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test HostType field is in allowed fields
|
||||
*/
|
||||
public function testHostTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('HostType', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test HostID field is in allowed fields
|
||||
*/
|
||||
public function testHostIDFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('HostID', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test HostTestCode field is in allowed fields
|
||||
*/
|
||||
public function testHostTestCodeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('HostTestCode', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ClientType field is in allowed fields
|
||||
*/
|
||||
public function testClientTypeFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('ClientType', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestSiteID field is in allowed fields
|
||||
*/
|
||||
public function testTestSiteIDFieldExists(): void
|
||||
{
|
||||
$allowedFields = $this->model->allowedFields;
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user