From 40ecb4e6e8b9a3d4fed9830cef5e90b417ea026c Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Sat, 31 Jan 2026 09:27:32 +0700 Subject: [PATCH] feat(api): transition to headless architecture and enhance order management This commit marks a significant architectural shift, transitioning the CLQMS backend to a fully headless REST API. All view-related components have been removed to focus solely on providing a robust, stateless API for clinical laboratory workflows. ### Architectural Changes - **Headless API Transition:** - Removed all view files (`app/Views/v2`), associated page controllers (`PagesController`), and routes (`Routes.php`). The application no longer serves a front-end UI. - The root endpoint (`/`) now returns a simple "Backend Running" status message. - **Developer Tooling & Guidance:** - Replaced `CLAUDE.md` with `GEMINI.md` to provide updated context and instructional guidelines for Gemini agents. - Updated `.serena/project.yml` with project configuration. ### Feature Enhancements - **Advanced Order Management (`OrderTestModel`):** - **Test Expansion:** The `createOrder` process now automatically expands `GROUP` (panel) tests into their individual components and recursively includes all parameter dependencies for `CALC` (calculated) tests. - **Order Comments:** Added support for attaching comments to an order via the `ordercom` table. - **Status Tracking:** Order status updates are now correctly recorded in the `orderstatus` table. - **Schema Alignment:** Switched from `OrderID` to `InternalOID` as the primary key for internal operations. - **Reference Range Refactor (`TestsController`):** - Simplified reference range logic by consolidating `refthold` and `refvset` into the main `refnum` and `reftxt` tables. - Standardized `RefType` handling to support `NMRC`, `TEXT`, `THOLD`, and `VSET` codes from the `reference_type` ValueSet. ### Other Changes - **Documentation:** - `PRD.md`, `README.md`, and `TODO.md` were updated to reflect the headless architecture, refined scope, and current project priorities. - **Database:** - Removed obsolete `RefTHoldID` and `RefVSetID` columns from the `patres` table migration. - **Testing:** - Added new feature tests for `ContactController`, `OrganizationController`, and `TestsController`. --- .serena/project.yml | 23 +- CLAUDE.md | 200 ---- GEMINI.md | 115 +++ PRD.md | 4 +- README.md | 4 +- TODO.md | 117 +-- app/Config/Routes.php | 30 +- app/Controllers/OrderTestController.php | 4 +- app/Controllers/PagesController.php | 186 ---- .../Specimen/ContainerDefController.php | 2 +- app/Controllers/Test/DemoOrderController.php | 1 + app/Controllers/TestsController.php | 39 +- .../2026-01-01-000009_CreateResults.php | 2 - app/Libraries/Data/order_priority.json | 2 +- app/Libraries/Data/order_status.json | 6 + app/Libraries/Data/priority.json | 2 +- app/Libraries/Data/reference_type.json | 4 +- app/Models/OrderTest/OrderTestModel.php | 132 ++- app/Models/PatResultModel.php | 28 + app/Views/v2/auth/login.php | 214 ---- app/Views/v2/dashboard/dashboard_index.php | 153 --- app/Views/v2/layout/main_layout.php | 416 -------- .../v2/master/organization/account_dialog.php | 189 ---- .../v2/master/organization/accounts_index.php | 345 ------- .../master/organization/department_dialog.php | 107 -- .../master/organization/departments_index.php | 351 ------- .../master/organization/discipline_dialog.php | 107 -- .../master/organization/disciplines_index.php | 352 ------- .../v2/master/organization/site_dialog.php | 144 --- .../v2/master/organization/sites_index.php | 368 ------- .../organization/workstation_dialog.php | 131 --- .../organization/workstations_index.php | 368 ------- .../v2/master/specimen/container_dialog.php | 143 --- .../v2/master/specimen/containers_index.php | 385 -------- .../v2/master/specimen/preparation_dialog.php | 138 --- .../v2/master/specimen/preparations_index.php | 317 ------ app/Views/v2/master/tests/calc_dialog.php | 364 ------- app/Views/v2/master/tests/grp_dialog.php | 305 ------ app/Views/v2/master/tests/param_dialog.php | 386 -------- app/Views/v2/master/tests/test_dialog.php | 344 ------- app/Views/v2/master/tests/tests_index.php | 758 -------------- app/Views/v2/patients/dialog_form.php | 208 ---- app/Views/v2/patients/patients_index.php | 446 --------- app/Views/v2/requests/requests_index.php | 130 --- .../result/valueset/resultvalueset_dialog.php | 149 --- .../result/valueset/resultvalueset_index.php | 322 ------ .../valuesetdef/resultvaluesetdef_dialog.php | 116 --- .../valuesetdef/resultvaluesetdef_index.php | 298 ------ app/Views/v2/settings/settings_index.php | 131 --- app/Views/v2/valueset/valueset_index.php | 371 ------- public/css/v2/styles.css | 932 ------------------ tests/feature/ContactControllerTest.php | 94 ++ tests/feature/OrganizationControllerTest.php | 122 +++ tests/feature/TestsControllerTest.php | 118 +++ tests/unit/ValueSet/ValueSetTest.php | 10 +- 55 files changed, 704 insertions(+), 10029 deletions(-) delete mode 100644 CLAUDE.md create mode 100644 GEMINI.md create mode 100644 app/Models/PatResultModel.php delete mode 100644 app/Views/v2/auth/login.php delete mode 100644 app/Views/v2/dashboard/dashboard_index.php delete mode 100644 app/Views/v2/layout/main_layout.php delete mode 100644 app/Views/v2/master/organization/account_dialog.php delete mode 100644 app/Views/v2/master/organization/accounts_index.php delete mode 100644 app/Views/v2/master/organization/department_dialog.php delete mode 100644 app/Views/v2/master/organization/departments_index.php delete mode 100644 app/Views/v2/master/organization/discipline_dialog.php delete mode 100644 app/Views/v2/master/organization/disciplines_index.php delete mode 100644 app/Views/v2/master/organization/site_dialog.php delete mode 100644 app/Views/v2/master/organization/sites_index.php delete mode 100644 app/Views/v2/master/organization/workstation_dialog.php delete mode 100644 app/Views/v2/master/organization/workstations_index.php delete mode 100644 app/Views/v2/master/specimen/container_dialog.php delete mode 100644 app/Views/v2/master/specimen/containers_index.php delete mode 100644 app/Views/v2/master/specimen/preparation_dialog.php delete mode 100644 app/Views/v2/master/specimen/preparations_index.php delete mode 100644 app/Views/v2/master/tests/calc_dialog.php delete mode 100644 app/Views/v2/master/tests/grp_dialog.php delete mode 100644 app/Views/v2/master/tests/param_dialog.php delete mode 100644 app/Views/v2/master/tests/test_dialog.php delete mode 100644 app/Views/v2/master/tests/tests_index.php delete mode 100644 app/Views/v2/patients/dialog_form.php delete mode 100644 app/Views/v2/patients/patients_index.php delete mode 100644 app/Views/v2/requests/requests_index.php delete mode 100644 app/Views/v2/result/valueset/resultvalueset_dialog.php delete mode 100644 app/Views/v2/result/valueset/resultvalueset_index.php delete mode 100644 app/Views/v2/result/valuesetdef/resultvaluesetdef_dialog.php delete mode 100644 app/Views/v2/result/valuesetdef/resultvaluesetdef_index.php delete mode 100644 app/Views/v2/settings/settings_index.php delete mode 100644 app/Views/v2/valueset/valueset_index.php delete mode 100644 public/css/v2/styles.css create mode 100644 tests/feature/ContactControllerTest.php create mode 100644 tests/feature/OrganizationControllerTest.php create mode 100644 tests/feature/TestsControllerTest.php diff --git a/.serena/project.yml b/.serena/project.yml index 024bb1a..02bc3e8 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -84,6 +84,27 @@ excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: "" - +# the name by which the project can be referenced within Serena project_name: "clqms01" + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) included_optional_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1c0eade..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,200 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - ---- - -## Tool Preference: Use Serena MCP - -**Always prioritize Serena MCP tools for code operations to minimize tool calls:** - -- **Use `find_symbol` / `get_symbols_overview`** instead of `Read` for exploring code structure -- **Use `replace_symbol_body`** instead of `Edit` for modifying functions, methods, classes -- **Use `insert_before_symbol` / `insert_after_symbol`** for adding new code symbols -- **Use `search_for_pattern`** instead of `Grep` for searching code patterns -- **Use `list_dir` / `find_file`** instead of `Glob` for file discovery - -This leverages semantic code understanding for this PHP codebase. - ---- - -## Common Commands - -### Testing -```bash -# Run all tests -vendor/bin/phpunit - -# Run specific test file -vendor/bin/phpunit tests/feature/UniformShowTest.php - -# Run tests in a directory -vendor/bin/phpunit app/Models - -# Generate code coverage report -vendor/bin/phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m -``` - -### Database -```bash -# Run migrations -spark migrate - -# Rollback migrations -spark migrate:rollback - -# Seed database -spark db:seed DBSeeder - -# Refresh all migrations and seed -spark migrate:refresh --seed -``` - -### CodeIgniter CLI -```bash -# General help -spark help - -# Generate code (scaffolding) -spark make:model ModelName -spark make:controller ControllerName -spark make:migration MigrationName -``` - -### API Documentation -```bash -# Keep api-docs.yaml updated whenever controllers change -# File: public/api-docs.yaml -# After modifying controllers, update the OpenAPI schema definitions -# to reflect new field names, types, and response formats -``` - ---- - -## Architecture Overview - -### Core Pattern: MVC + Libraries - -CLQMS follows CodeIgniter 4 conventions with these key architectural patterns: - -**Controllers** (`app/Controllers/`) -- Organized by domain: `Patient/`, `Organization/`, `Specimen/`, `Test/`, `Contact/` -- Root-level controllers for cross-domain concerns: `AuthController`, `EdgeController`, `ValueSetController` -- All controllers extend `BaseController` which provides request/response helpers - -**Models** (`app/Models/`) -- Organized by domain matching controllers: `Patient/`, `Organization/`, `Specimen/`, `Test/`, etc. -- All models extend `BaseModel` which provides automatic UTC date normalization -- Model callbacks: `beforeInsert/Update` normalize dates to UTC, `afterFind/Insert/Update` convert to UTC ISO format - -**Libraries** (`app/Libraries/`) -- `ValueSet` - Static lookup system using JSON files (see "Lookup System" below) - -### Lookup System: ValueSet Library - -The system uses a **JSON file-based lookup system** instead of database queries for static values. - -**Location:** `app/Libraries/Data/*.json` (44+ JSON files) - -**Key Files:** -- `_meta.json` - Metadata about lookup definitions -- `sex.json`, `order_status.json`, `specimen_type.json`, `test_type.json`, etc. - -**Usage:** -```php -use App\Libraries\ValueSet; - -// Get dropdown-formatted options -$options = ValueSet::get('sex'); // [["value"=>"1","label"=>"Female"],...] - -// Get raw data -$raw = ValueSet::getRaw('sex'); // [["key"=>"1","value"=>"Female"],...] - -// Get label for a specific key -$label = ValueSet::getLabel('sex', '1'); // "Female" - -// Transform database records with lookup text labels -$data = ValueSet::transformLabels($patients, ['Sex' => 'sex']); - -// Clear cache after modifying JSON files -ValueSet::clearCache(); -``` - -**When to use ValueSet vs API:** -- **ValueSet library** - Static values that rarely change (fast, cached) -- **API `/api/valueset*`** - Dynamic values managed by admins at runtime - -### Database Migrations Structure - -Migrations are numbered sequentially starting with `2026-01-01-`: - -| Migration | Tables Created | -|-----------|----------------| -| 000001 | valueset, counter, containerdef, occupation, specialty | -| 000002 | account, site, location, discipline, department | -| 000003 | patient, patidentifier, pataddress, patcontact | -| 000004 | contact, contactdetail, userdevices, loginattempts | -| 000005 | patvisit, patinsurance | -| 000006 | porder, orderitem | -| 000007 | specimen, specmenactivity, containerdef | -| 000008 | testdefinition, testactivity, refnum, reftxt | -| 000009 | patresult, patresultdetail, patresultcomment | -| 000010 | edgeres, edgestatus, edgeack, workstation | - -### Authentication & Authorization - -- **JWT-based authentication** using `firebase/php-jwt` -- **AuthFilter** - Route filter protecting API endpoints -- **AuthController** - Login/logout endpoints (`POST /api/login`, `POST /api/logout`) -- **AuthV2Controller** - V2 authentication endpoints - -### Edge API (Instrument Integration) - -The `EdgeController` provides endpoints for laboratory instrument integration via `tiny-edge` middleware: - -- `POST /api/edge/results` - Receive instrument results (stored in `edgeres` table) -- `GET /api/edge/orders` - Fetch pending orders for instruments -- `POST /api/edge/orders/:id/ack` - Acknowledge order delivery -- `POST /api/edge/status` - Log instrument status updates - -**Workflow:** `Instrument → tiny-edge → edgeres table → [Processing] → patresult table` - -### Test Types System - -Tests in `testdefinition` table support multiple types via `TestType` field: - -| Code | Type | Description | -|------|------|-------------| -| TEST | Technical | Individual lab test with specs | -| PARAM | Parameter | Non-lab measurement | -| CALC | Calculated | Test with formula | -| GROUP | Panel/Profile | Contains multiple tests | -| TITLE | Section | Report organization header | - -### Reference Range Architecture - -Reference ranges support multiple types for result validation: - -| Type | Table | Purpose | -|------|-------|---------| -| Numeric | `refnum` | Ranges with age/sex criteria | -| Threshold | `refthold` | Critical values | -| Text | `reftxt` | Text-based references | -| Value Set | `refvset` | Coded references | - -### Routes Organization - -Routes are defined in `app/Config/Routes.php`: -- API routes: `/api/{resource}` -- Auth-protected routes use `AuthFilter` - ---- - -## Project Structure Notes - -- **Language:** PHP 8.1+ (PSR-compliant) -- **Framework:** CodeIgniter 4 -- **Database:** MySQL with migration-based schema management -- **Testing:** PHPUnit 10.5+ (tests in `tests/` directory) -- **Entry point:** `public/index.php` (web), `spark` (CLI) -- **Environment config:** `.env` file (copy from `env` template) diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..a30f95a --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,115 @@ +# CLQMS (Clinical Laboratory Quality Management System) - Gemini Context + +This file provides context and instructional guidelines for Gemini agents working on the CLQMS repository. + +## 1. Project Overview + +**CLQMS** is a **headless REST API backend** for a Clinical Laboratory Quality Management System. It manages the complete laboratory workflow: patient registration, ordering, specimen tracking, result entry/verification, and instrument integration. + +* **Type:** API-only Backend (No View Layer). +* **Framework:** CodeIgniter 4 (PHP 8.1+). +* **Database:** MySQL. +* **Authentication:** JWT (Stateless). +* **Architecture:** Modular MVC with a file-based Lookup Library. + +## 2. Technical Stack + +* **Language:** PHP 8.1+ (PSR-compliant) +* **Framework:** CodeIgniter 4 +* **Dependencies:** `firebase/php-jwt` (Auth), `phpunit/phpunit` (Testing) +* **Database:** MySQL (Managed via Migrations) +* **Entry Point:** `public/index.php` (Web), `spark` (CLI) + +## 3. Development Workflow & Conventions + +### Tool Usage Guidelines +**Critical:** Prioritize semantic code analysis tools over generic file reading to minimize context window usage and improve accuracy. + +* **Explore Code:** Use `find_symbol` or `get_symbols_overview`. +* **Modify Code:** Use `replace_symbol_body` for functions/classes. Use `replace_content` (regex) for small tweaks. +* **Add Code:** Use `insert_before_symbol` / `insert_after_symbol`. +* **Search:** Use `search_for_pattern`. +* **File Discovery:** Use `list_dir` / `find_file`. + +### Common Commands + +**Testing:** +```bash +# Run all tests +vendor/bin/phpunit + +# Run specific test file +vendor/bin/phpunit tests/feature/UniformShowTest.php + +# Run tests in a directory +vendor/bin/phpunit app/Models +``` + +**Database & Migrations:** +```bash +# Run migrations +spark migrate + +# Rollback migrations +spark migrate:rollback + +# Seed database +spark db:seed DBSeeder + +# Refresh all (Reset DB) +spark migrate:refresh --seed +``` + +**Code Generation:** +```bash +spark make:model ModelName +spark make:controller ControllerName +spark make:migration MigrationName +``` + +## 4. Key Architectural Patterns + +### MVC Structure +* **Controllers (`app/Controllers/`)**: Organized by domain (Patient, Order, Test, etc.). All extend `BaseController`. +* **Models (`app/Models/`)**: Domain-specific data access. All extend `BaseModel`. + * **UTC Handling:** Models automatically normalize dates to UTC on save and format to UTC ISO on retrieve. + +### Lookup System (`App\Libraries\ValueSet`) +* **Mechanism:** JSON file-based static data storage. **Do not use database queries for static lookups.** +* **Location:** `app/Libraries/Data/valuesets/*.json`. +* **Usage:** + ```php + use App\Libraries\ValueSet; + $options = ValueSet::get('gender'); // Returns dropdown options + $label = ValueSet::getLabel('gender', '1'); // Returns 'Female' + ``` +* **Cache:** System caches these files. Run `ValueSet::clearCache()` if files are modified manually (rare). + +### Edge API (Instrument Integration) +* **Purpose:** Middleware for laboratory analyzers. +* **Flow:** Instrument -> `tiny-edge` -> `POST /api/edge/results` -> `edgeres` table -> Processing -> `patresult` table. +* **Controllers:** `EdgeController`. + +### Database Schema +* **Migrations:** Sequentially numbered (e.g., `2026-01-01-000001`). +* **Master Data:** `valueset`, `testdef*` (test definitions), `ref*` (reference ranges). +* **Transactions:** `patient`, `porder` (orders), `specimen`, `patresult`. + +## 5. Documentation Map + +* **`README.md`**: High-level API overview and endpoint list. +* **`PRD.md`**: Detailed Product Requirements Document. **Read this for business logic queries.** +* **`CLAUDE.md`**: Original developer guide (source of these conventions). +* **`TODO.md`**: Current project status and roadmap. +* **`app/Config/Routes.php`**: API Route definitions. + +## 6. Testing Strategy + +* **Framework:** PHPUnit 10.5+. +* **Location:** `tests/`. +* **Coverage:** Aim for high coverage on core logic (Models, Libraries). +* **Configuration:** `phpunit.xml.dist`. + +## 7. Configuration +* **Environment:** managed via `.env` (template in `env`). +* **Database Config:** `app/Config/Database.php` (uses `.env` variables). diff --git a/PRD.md b/PRD.md index 8e06b6a..0e13803 100644 --- a/PRD.md +++ b/PRD.md @@ -498,7 +498,7 @@ POST /api/edge/results |-------|---------|------------| | patient | Patient registry | InternalPID, PatientID, NameFirst, NameLast, Sex, Birthdate | | porder | Laboratory orders | OrderID, InternalPID, OrderStatus, Priority | -| orderitem | Order tests | OrderID, TestID | + | specimen | Specimens | SID, SpecimenID, SpecimenStatus | | patresult | Patient results | ResultID, OrderID, TestID, ResultValue | | patresultdetail | Result details | ResultID, ParameterID, Value | @@ -684,7 +684,7 @@ The MVP is considered complete when: ### 10.1 Open Questions | Question | Impact | Target Date | |----------|--------|-------------| -| Reference range types (refthold, refvset) - are they needed for MVP? | Medium | Phase 0 | + | Multi-site deployment requirements? | High | Phase 0 | | Specific instrument integrations needed? | High | Phase 2 | | Report format requirements (PDF/HTML)? | Medium | Phase 1 | diff --git a/README.md b/README.md index 4016225..4a9f9b1 100644 --- a/README.md +++ b/README.md @@ -423,9 +423,7 @@ Reference Ranges define normal and critical values for test results. The system | 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 @@ -513,7 +511,7 @@ valuesetdef (VSetDefID, VSName, VSDesc) | Category | Tables | Purpose | |----------|--------|---------| | Tests | `testdefsite`, `testdeftech`, `testdefcal`, `testdefgrp`, `testmap` | Test definitions | -| Reference Ranges | `refnum`, `refthold`, `reftxt`, `refvset` | Result validation | +| Reference Ranges | `refnum`, `reftxt` | Result validation | | Value Sets | `valuesetdef`, `valueset` | Configurable options | --- diff --git a/TODO.md b/TODO.md index 344a285..5a5e7f5 100644 --- a/TODO.md +++ b/TODO.md @@ -6,31 +6,16 @@ You **don't need** all master data finished to create an order. Here's what's actually required: -### Minimum Required (4 Tables) +### Minimum Required (2 Tables) + +These are the only tables that need database entries for a minimal setup. System lookups (status, priority, etc.) are handled by the `ValueSet` library using static JSON files in `app/Libraries/Data/`. ```sql -- 1. Patient (already exists in codebase) -- Just need at least 1 patient --- 2. Order Status Values (VSetID=11) -INSERT INTO valueset (VID, VSetID, VValue, VDesc, VOrder) VALUES -(1, 11, 'ORD', 'Ordered', 1), -(2, 11, 'SCH', 'Scheduled', 2), -(3, 11, 'ANA', 'Analysis', 3), -(4, 11, 'VER', 'Verified', 4), -(5, 11, 'REV', 'Reviewed', 5), -(6, 11, 'REP', 'Reported', 6); - --- 3. Priority Values (VSetID=10) -INSERT INTO valueset (VID, VSetID, VValue, VDesc, VOrder) VALUES -(1, 10, 'S', 'Stat', 1), -(2, 10, 'R', 'Routine', 2), -(3, 10, 'A', 'ASAP', 3); - --- 4. Counter for Order ID +-- 2. Counter for Order ID INSERT INTO counter (CounterName, CounterValue) VALUES ('ORDER', 1); - --- Run seeder: php spark db:seed MinimalMasterDataSeeder ``` ### API Endpoints (No Auth Required for Testing) @@ -72,17 +57,20 @@ Order → Collection → Reception → Preparation → Analysis → Verification --- -## Phase 1: Core Lab Workflow (Must Have) +## Phase 1: Order Management (Immediate Priority) ### 1.1 Order Management -- [ ] Complete `OrderTestController` create/update/delete -- [ ] Implement order ID generation (LLYYMMDDXXXXX format) -- [ ] Implement order attachment handling (ordercom, orderatt tables) -- [ ] Add order status tracking (ORD→SCH→ANA→VER→REV→REP) -- [ ] Create order test mapping (testmap table) -- [ ] Add calculated test parameter auto-selection +- [x] Complete `OrderTestController` create/update/delete +- [x] Implement order ID generation (LLYYMMDDXXXXX format) +- [x] Implement order comment handling (ordercom table) +- [ ] Implement order attachment handling (orderatt table) +- [x] Add order status tracking (ORD→SCH→ANA→VER→REV→REP) +- [x] Create order test mapping (using patres table) +- [x] Add calculated test parameter auto-selection & Group expansion -### 1.2 Specimen Management +## Phase 2: Specimen & Result Management (Later) + +### 2.1 Specimen Management - [ ] Complete `SpecimenController` API - [ ] Implement specimen ID generation (OrderID + SSS + C) - [ ] Build specimen collection API (Collection status) @@ -93,7 +81,7 @@ Order → Collection → Reception → Preparation → Analysis → Verification - [ ] Build specimen dispatching API (Dispatch status) - [ ] Implement specimen condition tracking (HEM, ITC, LIP flags) -### 1.3 Result Management +### 2.2 Result Management - [ ] Complete `ResultController` with full CRUD - [ ] Implement result entry API (numeric, text, valueset, range) - [ ] Implement result verification workflow (Technical + Clinical) @@ -102,17 +90,16 @@ Order → Collection → Reception → Preparation → Analysis → Verification - [ ] Implement result rerun with AspCnt tracking - [ ] Add result report generation API -### 1.4 Patient Visit +### 2.3 Patient Visit - [ ] Complete `PatVisitController` create/read - [ ] Implement patient visit to order linking - [ ] Add admission/discharge/transfer (ADT) tracking - [ ] Add diagnosis linking (patdiag table) - --- -## Phase 2: Instrument Integration (Must Have) +## Phase 3: Instrument Integration (Later) -### 2.1 Edge API +### 3.1 Edge API - [ ] Complete `EdgeController` results endpoint - [ ] Implement edgeres table data handling - [ ] Implement edgestatus tracking @@ -121,14 +108,14 @@ Order → Collection → Reception → Preparation → Analysis → Verification - [ ] Build order acknowledgment endpoint (/api/edge/orders/:id/ack) - [ ] Build status logging endpoint (/api/edge/status) -### 2.2 Test Mapping +### 3.2 Test Mapping - [ ] Implement test mapping CRUD (TestMapModel) - [ ] Build instrument code to LQMS test mapping - [ ] Add many-to-one mapping support (e.g., glucose variations) --- -## Phase 3: Quality Management (Should Have) +## Phase 4: Quality Management (Should Have) ### 3.1 Quality Control - [ ] Build QC result entry API @@ -161,10 +148,10 @@ Order → Collection → Reception → Preparation → Analysis → Verification - [ ] Test parameters ### 4.2 Reference Ranges ✅ Existing -- [ ] Numeric ranges (refnum) -- [ ] Threshold ranges (refthold) -- [ ] Text ranges (reftxt) -- [ ] Value set ranges (refvset) +- [x] Numeric ranges (refnum) +- [x] Threshold ranges (refthold) +- [x] Text ranges (reftxt) +- [x] Value set ranges (refvset) ### 4.3 Organizations ✅ Existing - [ ] Sites (SiteController) @@ -243,42 +230,42 @@ curl -X POST http://localhost:8080/api/ordertest/status \ ## Success Criteria ### Functional -- Patient registration works ✅ -- Test ordering generates valid OrderID and SID -- Specimens track through collection → transport → reception → preparation → analysis -- Results can be entered with reference range validation -- Results verified through VER → REV → REP workflow -- Instruments can send results via Edge API +- [x] Patient registration works +- [ ] Test ordering generates valid OrderID and SID +- [ ] Specimens track through collection → transport → reception → preparation → analysis +- [ ] Results can be entered with reference range validation +- [ ] Results verified through VER → REV → REP workflow +- [ ] Instruments can send results via Edge API ### Non-Functional -- JWT authentication required for all endpoints -- Soft delete (DelDate) on all transactions -- UTC timezone for all datetime fields -- Audit logging for data changes -- < 2s response time for standard queries +- [ ] JWT authentication required for all endpoints +- [ ] Soft delete (DelDate) on all transactions +- [ ] UTC timezone for all datetime fields +- [ ] Audit logging for data changes +- [ ] < 2s response time for standard queries --- ## Current Codebase Status ### Controllers (Need Work) -- ❌ OrderTestController - placeholder code, incomplete -- ❌ ResultController - only validates JWT -- ✅ PatientController - complete -- ✅ TestsController - complete -- ✅ PatVisitController - partial +- [ ] OrderTestController - placeholder code, incomplete +- [ ] ResultController - only validates JWT +- [x] PatientController - complete +- [x] TestsController - complete +- [x] PatVisitController - partial ### Models (Good) -- ✅ PatientModel - complete -- ✅ TestDef* models - complete -- ✅ Ref* models - complete -- ✅ ValueSet* models - complete -- ✅ SpecimenModel - exists, needs API +- [x] PatientModel - complete +- [x] TestDef* models - complete +- [x] Ref* models - complete +- [x] ValueSet* models - complete +- [x] SpecimenModel - exists, needs API ### Missing Controllers -- ❌ SpecimenController - need full implementation -- ❌ ResultController - need full implementation -- ❌ QualityControlController - not exist -- ❌ CalibrationController - not exist -- ❌ AuditController - not exist -- ❌ BillingController - not exist +- [ ] SpecimenController - need full implementation +- [ ] ResultController - need full implementation +- [ ] QualityControlController - not exist +- [ ] CalibrationController - not exist +- [ ] AuditController - not exist +- [ ] BillingController - not exist diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 05bd8f2..7924d6b 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -6,7 +6,7 @@ use CodeIgniter\Router\RouteCollection; * @var RouteCollection $routes */ $routes->get('/', function () { - return redirect()->to('/v2'); + return "Backend Running"; }); $routes->options('(:any)', function () { @@ -19,8 +19,7 @@ $routes->group('api', ['filter' => 'auth'], function ($routes) { $routes->get('sample', 'SampleController::index'); }); -// Public Routes (no auth required) -$routes->get('/v2/login', 'PagesController::login'); + // Swagger API Documentation (public - no filters) $routes->add('swagger', 'PagesController::swagger'); @@ -33,32 +32,7 @@ $routes->group('v2/auth', function ($routes) { $routes->post('logout', 'AuthV2Controller::logout'); }); -// Protected Page Routes - V2 (requires auth) -$routes->group('v2', ['filter' => 'auth'], function ($routes) { - $routes->get('/', 'PagesController::dashboard'); - $routes->get('dashboard', 'PagesController::dashboard'); - $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('valueset', 'PagesController::valueSetLibrary'); - $routes->get('result/valueset', 'PagesController::resultValueSet'); - $routes->get('result/valuesetdef', 'PagesController::resultValueSetDef'); -}); // Faker $routes->get('faker/faker-patient/(:num)', 'faker\FakerPatient::sendMany/$1'); diff --git a/app/Controllers/OrderTestController.php b/app/Controllers/OrderTestController.php index 399326c..4be3173 100644 --- a/app/Controllers/OrderTestController.php +++ b/app/Controllers/OrderTestController.php @@ -36,7 +36,7 @@ class OrderTestController extends Controller { } else { $rows = $this->db->table('ordertest') ->where('DelDate', null) - ->orderBy('OrderDateTime', 'DESC') + ->orderBy('TrnDate', 'DESC') ->get() ->getResultArray(); } @@ -134,7 +134,7 @@ class OrderTestController extends Controller { if (isset($input['WorkstationID'])) $updateData['WorkstationID'] = $input['WorkstationID']; if (!empty($updateData)) { - $this->model->update($input['OrderID'], $updateData); + $this->model->update($order['InternalOID'], $updateData); } return $this->respond([ diff --git a/app/Controllers/PagesController.php b/app/Controllers/PagesController.php index 5c3e457..4291507 100644 --- a/app/Controllers/PagesController.php +++ b/app/Controllers/PagesController.php @@ -10,193 +10,7 @@ namespace App\Controllers; */ class PagesController extends BaseController { - /** - * Dashboard page - */ - public function dashboard() - { - return view('v2/dashboard/dashboard_index', [ - 'pageTitle' => 'Dashboard', - 'activePage' => 'dashboard' - ]); - } - /** - * Patients page - */ - public function patients() - { - return view('v2/patients/patients_index', [ - 'pageTitle' => 'Patients', - 'activePage' => 'patients' - ]); - } - - /** - * Lab Requests page - */ - public function requests() - { - return view('v2/requests/requests_index', [ - 'pageTitle' => 'Lab Requests', - 'activePage' => 'requests' - ]); - } - - /** - * Settings page - */ - public function settings() - { - return view('v2/settings/settings_index', [ - 'pageTitle' => 'Settings', - 'activePage' => 'settings' - ]); - } - - // ======================================== - // Master Data - Organization - // ======================================== - - /** - * Master Data - Organization Accounts - */ - public function masterOrgAccounts() - { - return view('v2/master/organization/accounts_index', [ - 'pageTitle' => 'Organization Accounts', - 'activePage' => 'master-org-accounts' - ]); - } - - /** - * Master Data - Organization Sites - */ - public function masterOrgSites() - { - return view('v2/master/organization/sites_index', [ - 'pageTitle' => 'Organization Sites', - 'activePage' => 'master-org-sites' - ]); - } - - /** - * Master Data - Organization Disciplines - */ - public function masterOrgDisciplines() - { - return view('v2/master/organization/disciplines_index', [ - 'pageTitle' => 'Disciplines', - 'activePage' => 'master-org-disciplines' - ]); - } - - /** - * Master Data - Organization Departments - */ - public function masterOrgDepartments() - { - return view('v2/master/organization/departments_index', [ - 'pageTitle' => 'Departments', - 'activePage' => 'master-org-departments' - ]); - } - - /** - * Master Data - Organization Workstations - */ - public function masterOrgWorkstations() - { - return view('v2/master/organization/workstations_index', [ - 'pageTitle' => 'Workstations', - 'activePage' => 'master-org-workstations' - ]); - } - - // ======================================== - // Master Data - Specimen - // ======================================== - - /** - * Master Data - Specimen Containers - */ - public function masterSpecimenContainers() - { - return view('v2/master/specimen/containers_index', [ - 'pageTitle' => 'Container Definitions', - 'activePage' => 'master-specimen-containers' - ]); - } - - /** - * Master Data - Specimen Preparations - */ - public function masterSpecimenPreparations() - { - return view('v2/master/specimen/preparations_index', [ - 'pageTitle' => 'Specimen Preparations', - 'activePage' => 'master-specimen-preparations' - ]); - } - - // ======================================== - // Master Data - Tests & ValueSets - // ======================================== - - /** - * Master Data - Lab Tests - */ - public function masterTests() - { - return view('v2/master/tests/tests_index', [ - 'pageTitle' => 'Lab Tests', - 'activePage' => 'master-tests' - ]); - } - - /** - * Value Set Library - Read-only - */ - public function valueSetLibrary() - { - return view('v2/valueset/valueset_index', [ - 'pageTitle' => 'Value Set Library', - 'activePage' => 'valueset-library' - ]); - } - - /** - * Result Valueset - CRUD for valueset table - */ - public function resultValueSet() - { - return view('v2/result/valueset/resultvalueset_index', [ - 'pageTitle' => 'Result Valuesets', - 'activePage' => 'result-valueset' - ]); - } - - /** - * Result Valueset Definition - CRUD for valuesetdef table - */ - public function resultValueSetDef() - { - return view('v2/result/valuesetdef/resultvaluesetdef_index', [ - 'pageTitle' => 'Valueset Definitions', - 'activePage' => 'result-valuesetdef' - ]); - } - - /** - * Login page - */ - public function login() - { - return view('v2/auth/login', [ - 'pageTitle' => 'Login', - 'activePage' => '' - ]); - } /** * API Documentation / Swagger UI page diff --git a/app/Controllers/Specimen/ContainerDefController.php b/app/Controllers/Specimen/ContainerDefController.php index cb57460..4f9d79c 100644 --- a/app/Controllers/Specimen/ContainerDefController.php +++ b/app/Controllers/Specimen/ContainerDefController.php @@ -67,7 +67,7 @@ class ContainerDefController extends BaseController { if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { $ConDefID = $this->model->insert($input); - return $this->respondCreated([ 'status' => 'success', 'message' => "data $ConDefID created successfully" ]); + return $this->respondCreated([ 'status' => 'success', 'message' => "data $ConDefID created successfully", 'data' => $ConDefID ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } diff --git a/app/Controllers/Test/DemoOrderController.php b/app/Controllers/Test/DemoOrderController.php index b8d6d45..23663fe 100644 --- a/app/Controllers/Test/DemoOrderController.php +++ b/app/Controllers/Test/DemoOrderController.php @@ -44,6 +44,7 @@ class DemoOrderController extends Controller { 'Priority' => $input['Priority'] ?? 'R', 'OrderingProvider' => $input['OrderingProvider'] ?? 'Dr. Demo', 'DepartmentID' => $input['DepartmentID'] ?? 1, + 'Tests' => $input['Tests'] ?? [] ]; $orderID = $this->orderModel->createOrder($orderData); diff --git a/app/Controllers/TestsController.php b/app/Controllers/TestsController.php index b821a50..e1fdb72 100644 --- a/app/Controllers/TestsController.php +++ b/app/Controllers/TestsController.php @@ -148,7 +148,7 @@ class TestsController extends BaseController $techData = $row['testdeftech'][0]; $refType = $techData['RefType']; - if ($refType === '1') { + if ($refType === '1' || $refType === 'NMRC' || $refType === '3' || $refType === 'THOLD') { $refnumData = $this->modelRefNum ->where('TestSiteID', $id) ->where('EndDate IS NULL') @@ -159,24 +159,28 @@ class TestsController extends BaseController return [ 'RefNumID' => $r['RefNumID'], 'NumRefType' => $r['NumRefType'], - 'NumRefTypeLabel' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']), + 'NumRefTypeLabel' => $r['NumRefType'] ? ValueSet::getLabel('numeric_ref_type', $r['NumRefType']) : '', 'RangeType' => $r['RangeType'], - 'RangeTypeLabel' => ValueSet::getLabel('range_type', $r['RangeType']), + 'RangeTypeLabel' => $r['RangeType'] ? ValueSet::getLabel('range_type', $r['RangeType']) : '', 'Sex' => $r['Sex'], - 'SexLabel' => ValueSet::getLabel('gender', $r['Sex']), + 'SexLabel' => $r['Sex'] ? ValueSet::getLabel('gender', $r['Sex']) : '', 'LowSign' => $r['LowSign'], - 'LowSignLabel' => ValueSet::getLabel('math_sign', $r['LowSign']), + 'LowSignLabel' => $r['LowSign'] ? ValueSet::getLabel('math_sign', $r['LowSign']) : '', 'HighSign' => $r['HighSign'], - 'HighSignLabel' => ValueSet::getLabel('math_sign', $r['HighSign']), - 'High' => $r['High'] !== null ? (int) $r['High'] : null, - 'Flag' => $r['Flag'] + 'HighSignLabel' => $r['HighSign'] ? ValueSet::getLabel('math_sign', $r['HighSign']) : '', + 'High' => $r['High'] !== null ? (float) $r['High'] : null, + 'Low' => $r['Low'] !== null ? (float) $r['Low'] : null, + 'AgeStart' => (int) $r['AgeStart'], + 'AgeEnd' => (int) $r['AgeEnd'], + 'Flag' => $r['Flag'], + 'Interpretation' => $r['Interpretation'] ]; }, $refnumData ?? []); $row['rangeTypeOptions'] = ValueSet::getOptions('range_type'); } - if ($refType === '2') { + if ($refType === '2' || $refType === 'TEXT' || $refType === '4' || $refType === 'VSET') { $reftxtData = $this->modelRefTxt ->where('TestSiteID', $id) ->where('EndDate IS NULL') @@ -187,17 +191,15 @@ class TestsController extends BaseController return [ 'RefTxtID' => $r['RefTxtID'], 'TxtRefType' => $r['TxtRefType'], - 'TxtRefTypeLabel' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']), + 'TxtRefTypeLabel' => $r['TxtRefType'] ? ValueSet::getLabel('text_ref_type', $r['TxtRefType']) : '', 'Sex' => $r['Sex'], - 'SexLabel' => ValueSet::getLabel('gender', $r['Sex']), + 'SexLabel' => $r['Sex'] ? ValueSet::getLabel('gender', $r['Sex']) : '', 'AgeStart' => (int) $r['AgeStart'], 'AgeEnd' => (int) $r['AgeEnd'], 'RefTxt' => $r['RefTxt'], 'Flag' => $r['Flag'] ]; }, $reftxtData ?? []); - - // $row['txtRefTypeOptions'] = ValueSet::getOptions('text_ref_type'); } } } @@ -431,13 +433,13 @@ class TestsController extends BaseController $this->saveTechDetails($testSiteID, $details, $action, $typeCode); if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) { - $refType = $details['RefType']; + $refType = (string) $details['RefType']; - if ($refType === '1' && isset($input['refnum']) && is_array($input['refnum'])) { + if (($refType === '1' || $refType === 'NMRC' || $refType === '3' || $refType === 'THOLD') && isset($input['refnum']) && is_array($input['refnum'])) { $this->saveRefNumRanges($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1); } - if ($refType === '2' && isset($input['reftxt']) && is_array($input['reftxt'])) { + if (($refType === '2' || $refType === 'TEXT' || $refType === '4' || $refType === 'VSET') && isset($input['reftxt']) && is_array($input['reftxt'])) { $this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, $input['SiteID'] ?? 1); } } @@ -503,10 +505,11 @@ class TestsController extends BaseController 'AgeStart' => (int) ($range['AgeStart'] ?? 0), 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), 'LowSign' => !empty($range['LowSign']) ? $range['LowSign'] : null, - 'Low' => !empty($range['Low']) ? (int) $range['Low'] : null, + 'Low' => !empty($range['Low']) ? (float) $range['Low'] : null, 'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null, - 'High' => !empty($range['High']) ? (int) $range['High'] : null, + 'High' => !empty($range['High']) ? (float) $range['High'] : null, 'Flag' => $range['Flag'] ?? null, + 'Interpretation' => $range['Interpretation'] ?? null, 'Display' => $index, 'CreateDate' => date('Y-m-d H:i:s') ]); diff --git a/app/Database/Migrations/2026-01-01-000009_CreateResults.php b/app/Database/Migrations/2026-01-01-000009_CreateResults.php index 79e0380..79c8f10 100644 --- a/app/Database/Migrations/2026-01-01-000009_CreateResults.php +++ b/app/Database/Migrations/2026-01-01-000009_CreateResults.php @@ -22,8 +22,6 @@ class CreateResults extends Migration { 'WorkstationID' => ['type' => 'INT', 'null' => true], 'EquipmentID' => ['type' => 'INT', 'null' => true], 'RefNumID' => ['type' => 'INT', 'null' => true], - 'RefTHoldID' => ['type' => 'INT', 'null' => true], - 'RefVSetID' => ['type' => 'INT', 'null' => true], 'RefTxtID' => ['type' => 'INT', 'null' => true], 'CreateDate' => ['type' => 'DATETIME', 'null' => true], 'EndDate' => ['type' => 'DATETIME', 'null' => true], diff --git a/app/Libraries/Data/order_priority.json b/app/Libraries/Data/order_priority.json index ed23871..1e68b20 100644 --- a/app/Libraries/Data/order_priority.json +++ b/app/Libraries/Data/order_priority.json @@ -3,8 +3,8 @@ "VCategory": "System", "values": [ {"key": "S", "value": "Stat"}, - {"key": "A", "value": "ASAP"}, {"key": "R", "value": "Routine"}, + {"key": "A", "value": "ASAP"}, {"key": "P", "value": "Preop"}, {"key": "C", "value": "Callback"}, {"key": "T", "value": "Timing critical"}, diff --git a/app/Libraries/Data/order_status.json b/app/Libraries/Data/order_status.json index 49b7395..3b0b9de 100644 --- a/app/Libraries/Data/order_status.json +++ b/app/Libraries/Data/order_status.json @@ -2,6 +2,12 @@ "VSName": "Order Status", "VCategory": "System", "values": [ + {"key": "ORD", "value": "Ordered"}, + {"key": "SCH", "value": "Scheduled"}, + {"key": "ANA", "value": "Analysis"}, + {"key": "VER", "value": "Verified"}, + {"key": "REV", "value": "Reviewed"}, + {"key": "REP", "value": "Reported"}, {"key": "A", "value": "Some, not all results available"}, {"key": "CA", "value": "Order is cancelled"}, {"key": "CM", "value": "Order is completed"}, diff --git a/app/Libraries/Data/priority.json b/app/Libraries/Data/priority.json index 60fcac6..c6e1914 100644 --- a/app/Libraries/Data/priority.json +++ b/app/Libraries/Data/priority.json @@ -3,8 +3,8 @@ "VCategory": "System", "values": [ {"key": "S", "value": "Stat"}, - {"key": "A", "value": "ASAP"}, {"key": "R", "value": "Routine"}, + {"key": "A", "value": "ASAP"}, {"key": "P", "value": "Preop"}, {"key": "C", "value": "Callback"}, {"key": "T", "value": "Timing critical"}, diff --git a/app/Libraries/Data/reference_type.json b/app/Libraries/Data/reference_type.json index f6fe909..b2f9a95 100644 --- a/app/Libraries/Data/reference_type.json +++ b/app/Libraries/Data/reference_type.json @@ -3,6 +3,8 @@ "VCategory": "System", "values": [ {"key": "NMRC", "value": "Numeric"}, - {"key": "TEXT", "value": "Text"} + {"key": "TEXT", "value": "Text"}, + {"key": "THOLD", "value": "Threshold"}, + {"key": "VSET", "value": "Value Set"} ] } diff --git a/app/Models/OrderTest/OrderTestModel.php b/app/Models/OrderTest/OrderTestModel.php index b610ff7..e47753f 100644 --- a/app/Models/OrderTest/OrderTestModel.php +++ b/app/Models/OrderTest/OrderTestModel.php @@ -5,21 +5,22 @@ use App\Models\BaseModel; class OrderTestModel extends BaseModel { protected $table = 'ordertest'; - protected $primaryKey = 'OrderID'; + protected $primaryKey = 'InternalOID'; + protected $useAutoIncrement = true; protected $allowedFields = [ + 'InternalOID', 'OrderID', + 'PlacerID', 'InternalPID', - 'PatVisitID', - 'OrderDateTime', - 'Priority', - 'OrderStatus', - 'OrderedBy', - 'OrderingProvider', 'SiteID', - 'SourceSiteID', - 'DepartmentID', - 'WorkstationID', - 'BillingAccount', + 'PVADTID', + 'ReqApp', + 'Priority', + 'TrnDate', + 'EffDate', + 'CreateDate', + 'EndDate', + 'ArchiveDate', 'DelDate' ]; @@ -52,29 +53,101 @@ class OrderTestModel extends BaseModel { } public function createOrder(array $data): string { - $orderID = $data['OrderID'] ?? $this->generateOrderID(); + $orderID = $data['OrderID'] ?? $this->generateOrderID($data['SiteCode'] ?? '00'); $orderData = [ 'OrderID' => $orderID, + 'PlacerID' => $data['PlacerID'] ?? null, 'InternalPID' => $data['InternalPID'], - 'PatVisitID' => $data['PatVisitID'] ?? null, - 'OrderDateTime' => $data['OrderDateTime'] ?? date('Y-m-d H:i:s'), + 'SiteID' => $data['SiteID'] ?? '1', + 'PVADTID' => $data['PatVisitID'] ?? $data['PVADTID'] ?? 0, + 'ReqApp' => $data['ReqApp'] ?? null, 'Priority' => $data['Priority'] ?? 'R', - 'OrderStatus' => $data['OrderStatus'] ?? 'ORD', - 'OrderedBy' => $data['OrderedBy'] ?? null, - 'OrderingProvider' => $data['OrderingProvider'] ?? null, - 'SiteID' => $data['SiteID'] ?? 1, - 'SourceSiteID' => $data['SourceSiteID'] ?? 1, - 'DepartmentID' => $data['DepartmentID'] ?? null, - 'WorkstationID' => $data['WorkstationID'] ?? null, - 'BillingAccount' => $data['BillingAccount'] ?? null, + 'TrnDate' => $data['OrderDateTime'] ?? $data['TrnDate'] ?? date('Y-m-d H:i:s'), + 'EffDate' => $data['EffDate'] ?? date('Y-m-d H:i:s'), 'CreateDate' => date('Y-m-d H:i:s') ]; - $this->insert($orderData); + $internalOID = $this->insert($orderData); + + // Handle Order Comments + if (!empty($data['Comment'])) { + $this->db->table('ordercom')->insert([ + 'InternalOID' => $internalOID, + 'Comment' => $data['Comment'], + 'CreateDate' => date('Y-m-d H:i:s') + ]); + } + + // Process Tests Expansion + if (isset($data['Tests']) && is_array($data['Tests'])) { + $testToOrder = []; + $testModel = new \App\Models\Test\TestDefSiteModel(); + $grpModel = new \App\Models\Test\TestDefGrpModel(); + $calModel = new \App\Models\Test\TestDefCalModel(); + + foreach ($data['Tests'] as $test) { + $testSiteID = $test['TestSiteID'] ?? $test['TestID'] ?? null; + if ($testSiteID) { + $this->expandTest($testSiteID, $testToOrder, $testModel, $grpModel, $calModel); + } + } + + // Insert unique tests into patres + if (!empty($testToOrder)) { + $resModel = new \App\Models\PatResultModel(); + foreach ($testToOrder as $tid => $tinfo) { + $resModel->insert([ + 'OrderID' => $internalOID, + 'TestSiteID' => $tid, + 'TestSiteCode' => $tinfo['TestSiteCode'], + 'SID' => $orderID, + 'SampleID' => $orderID, + 'ResultDateTime' => $orderData['TrnDate'], + 'CreateDate' => date('Y-m-d H:i:s') + ]); + } + } + } + return $orderID; } + private function expandTest($testSiteID, &$testToOrder, $testModel, $grpModel, $calModel) { + if (isset($testToOrder[$testSiteID])) return; + + $testInfo = $testModel->find($testSiteID); + if (!$testInfo) return; + + $testToOrder[$testSiteID] = [ + 'TestSiteCode' => $testInfo['TestSiteCode'], + 'TestType' => $testInfo['TestType'] + ]; + + // Handle Group Expansion + if ($testInfo['TestType'] === 'GROUP') { + $members = $grpModel->where('TestSiteID', $testSiteID)->findAll(); + foreach ($members as $m) { + $this->expandTest($m['Member'], $testToOrder, $testModel, $grpModel, $calModel); + } + } + + // Handle Calculated Test Dependencies + if ($testInfo['TestType'] === 'CALC') { + $calDetail = $calModel->where('TestSiteID', $testSiteID)->first(); + if ($calDetail && !empty($calDetail['FormulaInput'])) { + $inputs = explode(',', $calDetail['FormulaInput']); + foreach ($inputs as $inputCode) { + $inputCode = trim($inputCode); + $inputTest = $testModel->where('TestSiteCode', $inputCode)->first(); + if ($inputTest) { + $this->expandTest($inputTest['TestSiteID'], $testToOrder, $testModel, $grpModel, $calModel); + } + } + } + } + } + public function getOrder(string $orderID): ?array { return $this->select('*') ->where('OrderID', $orderID) @@ -87,16 +160,23 @@ class OrderTestModel extends BaseModel { return $this->select('*') ->where('InternalPID', $internalPID) ->where('DelDate', null) - ->orderBy('OrderDateTime', 'DESC') + ->orderBy('TrnDate', 'DESC') ->get() ->getResultArray(); } public function updateStatus(string $orderID, string $status): bool { - return $this->where('OrderID', $orderID)->update(['OrderStatus' => $status]); + $order = $this->getOrder($orderID); + if (!$order) return false; + + return (bool)$this->db->table('orderstatus')->insert([ + 'InternalOID' => $order['InternalOID'], + 'OrderStatus' => $status, + 'CreateDate' => date('Y-m-d H:i:s') + ]); } public function softDelete(string $orderID): bool { - return $this->where('OrderID', $orderID)->update(['DelDate' => date('Y-m-d H:i:s')]); + return $this->where('OrderID', $orderID)->update(null, ['DelDate' => date('Y-m-d H:i:s')]); } } diff --git a/app/Models/PatResultModel.php b/app/Models/PatResultModel.php new file mode 100644 index 0000000..7506e00 --- /dev/null +++ b/app/Models/PatResultModel.php @@ -0,0 +1,28 @@ + - - - - - Login - CLQMS - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
- -
-

CLQMS

-

Clinical Laboratory Quality Management System

-
- - -
- - -
- -
- - -
- - -
- - -
- -
- - - - -
-
- - -
- -
- - - - - -
-
- - -
- - -
- - - - -
-
- - -
-

© 2025 5Panda. All rights reserved.

-
-
- - - - - - diff --git a/app/Views/v2/dashboard/dashboard_index.php b/app/Views/v2/dashboard/dashboard_index.php deleted file mode 100644 index e45c4bd..0000000 --- a/app/Views/v2/dashboard/dashboard_index.php +++ /dev/null @@ -1,153 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Welcome to CLQMS

-

Clinical Laboratory Quality Management System

-
-
-
- - -
- -
-
-
-
-

Total Patients

-

1,247

-
-
- -
-
-
-
- - -
-
-
-
-

Today's Visits

-

89

-
-
- -
-
-
-
- - -
-
-
-
-

Pending Tests

-

34

-
-
- -
-
-
-
- - -
-
-
-
-

Completed

-

156

-
-
- -
-
-
-
-
- - -
- -
-
-

- - Recent Activity -

-
-
-
- -
-
-

New patient registered

-

John Doe - 5 minutes ago

-
-
-
-
- -
-
-

Test completed

-

Sample #12345 - 12 minutes ago

-
-
-
-
- -
-
-

Pending approval

-

Request #789 - 25 minutes ago

-
-
-
-
-
- - -
-
-

- - Quick Actions -

-
- - - Patients - - - - Lab Requests - - - -
-
-
-
- -
-endSection() ?> diff --git a/app/Views/v2/layout/main_layout.php b/app/Views/v2/layout/main_layout.php deleted file mode 100644 index d33b57e..0000000 --- a/app/Views/v2/layout/main_layout.php +++ /dev/null @@ -1,416 +0,0 @@ - - - - - - <?= esc($pageTitle ?? 'CLQMS') ?> - CLQMS - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - -
- renderSection('content') ?> -
- - - -
- - - - - - renderSection('script') ?> - - diff --git a/app/Views/v2/master/organization/account_dialog.php b/app/Views/v2/master/organization/account_dialog.php deleted file mode 100644 index 2aa3b4b..0000000 --- a/app/Views/v2/master/organization/account_dialog.php +++ /dev/null @@ -1,189 +0,0 @@ - - diff --git a/app/Views/v2/master/organization/accounts_index.php b/app/Views/v2/master/organization/accounts_index.php deleted file mode 100644 index 1db0a44..0000000 --- a/app/Views/v2/master/organization/accounts_index.php +++ /dev/null @@ -1,345 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Organization Accounts

-

Manage organization accounts and entities

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading accounts...

-
- - -
- - - - - - - - - - - - - - - - - -
IDAccount NameCodeParentActions
-
- - -
- -
-
- - - include('v2/master/organization/account_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> - - diff --git a/app/Views/v2/master/organization/department_dialog.php b/app/Views/v2/master/organization/department_dialog.php deleted file mode 100644 index 60af44c..0000000 --- a/app/Views/v2/master/organization/department_dialog.php +++ /dev/null @@ -1,107 +0,0 @@ - - diff --git a/app/Views/v2/master/organization/departments_index.php b/app/Views/v2/master/organization/departments_index.php deleted file mode 100644 index 277310d..0000000 --- a/app/Views/v2/master/organization/departments_index.php +++ /dev/null @@ -1,351 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Organization Departments

-

Manage lab departments and functional units

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading departments...

-
- - -
- - - - - - - - - - - - - - - - - - -
IDDepartment NameCodeDisciplineSiteActions
-
-
- - - include('v2/master/organization/department_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/master/organization/discipline_dialog.php b/app/Views/v2/master/organization/discipline_dialog.php deleted file mode 100644 index 5742b60..0000000 --- a/app/Views/v2/master/organization/discipline_dialog.php +++ /dev/null @@ -1,107 +0,0 @@ - - diff --git a/app/Views/v2/master/organization/disciplines_index.php b/app/Views/v2/master/organization/disciplines_index.php deleted file mode 100644 index c8b86b8..0000000 --- a/app/Views/v2/master/organization/disciplines_index.php +++ /dev/null @@ -1,352 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Lab Disciplines

-

Manage laboratory disciplines and specialties

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading disciplines...

-
- - -
- - - - - - - - - - - - - - - - -
IDDiscipline NameCodeActions
-
-
- - - include('v2/master/organization/discipline_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> - diff --git a/app/Views/v2/master/organization/site_dialog.php b/app/Views/v2/master/organization/site_dialog.php deleted file mode 100644 index 335c951..0000000 --- a/app/Views/v2/master/organization/site_dialog.php +++ /dev/null @@ -1,144 +0,0 @@ - - diff --git a/app/Views/v2/master/organization/sites_index.php b/app/Views/v2/master/organization/sites_index.php deleted file mode 100644 index d80cf76..0000000 --- a/app/Views/v2/master/organization/sites_index.php +++ /dev/null @@ -1,368 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Organization Sites

-

Manage physical sites and locations

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading sites...

-
- - -
- - - - - - - - - - - - - - - - - - - -
IDSite NameCodeAccountTypeClassActions
-
- - -
- -
-
- - - include('v2/master/organization/site_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> - diff --git a/app/Views/v2/master/organization/workstation_dialog.php b/app/Views/v2/master/organization/workstation_dialog.php deleted file mode 100644 index 8661d78..0000000 --- a/app/Views/v2/master/organization/workstation_dialog.php +++ /dev/null @@ -1,131 +0,0 @@ - - diff --git a/app/Views/v2/master/organization/workstations_index.php b/app/Views/v2/master/organization/workstations_index.php deleted file mode 100644 index 1fc6a52..0000000 --- a/app/Views/v2/master/organization/workstations_index.php +++ /dev/null @@ -1,368 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Organization Workstations

-

Manage lab workstations and equipment units

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading workstations...

-
- - -
- - - - - - - - - - - - - - - - - - -
IDWorkstation NameCodeDepartmentStatusActions
-
-
- - - include('v2/master/organization/workstation_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/master/specimen/container_dialog.php b/app/Views/v2/master/specimen/container_dialog.php deleted file mode 100644 index 886bd2f..0000000 --- a/app/Views/v2/master/specimen/container_dialog.php +++ /dev/null @@ -1,143 +0,0 @@ - - diff --git a/app/Views/v2/master/specimen/containers_index.php b/app/Views/v2/master/specimen/containers_index.php deleted file mode 100644 index a27f00b..0000000 --- a/app/Views/v2/master/specimen/containers_index.php +++ /dev/null @@ -1,385 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Container Definitions

-

Manage specimen collection containers and tubes

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading containers...

-
- - -
- - - - - - - - - - - - - - - - - - -
IDContainer NameCodeColorAdditiveActions
-
-
- - - include('v2/master/specimen/container_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/master/specimen/preparation_dialog.php b/app/Views/v2/master/specimen/preparation_dialog.php deleted file mode 100644 index 5f2a63d..0000000 --- a/app/Views/v2/master/specimen/preparation_dialog.php +++ /dev/null @@ -1,138 +0,0 @@ - - diff --git a/app/Views/v2/master/specimen/preparations_index.php b/app/Views/v2/master/specimen/preparations_index.php deleted file mode 100644 index b5a269f..0000000 --- a/app/Views/v2/master/specimen/preparations_index.php +++ /dev/null @@ -1,317 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Specimen Preparations

-

Manage specimen processing and preparation methods

-
-
-
- - -
-
-
-
- - -
- -
-
-
- - -
- -
-
-

Loading preparations...

-
- - -
- - - - - - - - - - - - - - - - - - -
IDDescriptionMethodAdditiveQty/UnitActions
-
-
- - - include('v2/master/specimen/preparation_dialog') ?> - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/master/tests/calc_dialog.php b/app/Views/v2/master/tests/calc_dialog.php deleted file mode 100644 index 56fcf1f..0000000 --- a/app/Views/v2/master/tests/calc_dialog.php +++ /dev/null @@ -1,364 +0,0 @@ - - \ No newline at end of file diff --git a/app/Views/v2/master/tests/grp_dialog.php b/app/Views/v2/master/tests/grp_dialog.php deleted file mode 100644 index 688c131..0000000 --- a/app/Views/v2/master/tests/grp_dialog.php +++ /dev/null @@ -1,305 +0,0 @@ - - \ No newline at end of file diff --git a/app/Views/v2/master/tests/param_dialog.php b/app/Views/v2/master/tests/param_dialog.php deleted file mode 100644 index cc58bb4..0000000 --- a/app/Views/v2/master/tests/param_dialog.php +++ /dev/null @@ -1,386 +0,0 @@ - - \ No newline at end of file diff --git a/app/Views/v2/master/tests/test_dialog.php b/app/Views/v2/master/tests/test_dialog.php deleted file mode 100644 index 97b871c..0000000 --- a/app/Views/v2/master/tests/test_dialog.php +++ /dev/null @@ -1,344 +0,0 @@ - - \ No newline at end of file diff --git a/app/Views/v2/master/tests/tests_index.php b/app/Views/v2/master/tests/tests_index.php deleted file mode 100644 index ff26335..0000000 --- a/app/Views/v2/master/tests/tests_index.php +++ /dev/null @@ -1,758 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Laboratory Tests

-

Manage test definitions, parameters, and groups -

-
-
-
- - -
-
-
-
- - - -
- -
-
-
- - -
- -
-
-

Loading tests...

-
- - -
- - - - - - - - - - - - - - - - - - - -
IDCodeTest NameTypeSeqVisibleActions
-
- - -
- -
-
- - - include('v2/master/tests/test_dialog') ?> - include('v2/master/tests/param_dialog') ?> - include('v2/master/tests/calc_dialog') ?> - include('v2/master/tests/grp_dialog') ?> - - - - - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> \ No newline at end of file diff --git a/app/Views/v2/patients/dialog_form.php b/app/Views/v2/patients/dialog_form.php deleted file mode 100644 index bc160e1..0000000 --- a/app/Views/v2/patients/dialog_form.php +++ /dev/null @@ -1,208 +0,0 @@ - - diff --git a/app/Views/v2/patients/patients_index.php b/app/Views/v2/patients/patients_index.php deleted file mode 100644 index 6e2c881..0000000 --- a/app/Views/v2/patients/patients_index.php +++ /dev/null @@ -1,446 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
- -
-
-
-
-

Total Patients

-

0

-
-
- -
-
-
-
- - -
-
-
-
-

New Today

-

0

-
-
- -
-
-
-
- - -
-
-
-
-

Pending Visits

-

0

-
-
- -
-
-
-
-
- - -
-
-
- -
- - -
- - - -
-
-
- - -
- -
-
-

Loading patients...

-
- - -
- - - - - - - - - - - - - - - - - - -
Patient IDNameSexBirth DatePhoneActions
-
- - -
- -
- - - - - -
-
-
- - - include('v2/patients/dialog_form') ?> - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/requests/requests_index.php b/app/Views/v2/requests/requests_index.php deleted file mode 100644 index 822ec3e..0000000 --- a/app/Views/v2/requests/requests_index.php +++ /dev/null @@ -1,130 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Lab Requests

-

Manage laboratory test requests and orders

-
-
-
- - -
-
-
-
-
-

Pending

-

34

-
-
- -
-
-
-
- -
-
-
-
-

In Progress

-

18

-
-
- -
-
-
-
- -
-
-
-
-

Completed

-

156

-
-
- -
-
-
-
- -
-
-
-
-

Rejected

-

3

-
-
- -
-
-
-
-
- - -
-
-
-
- - -
- -
- - -
- - - - - - - - - - - - - - - - - -
Request IDPatientTest TypePriorityStatusDateActions
- -

No data available. Connect to API to load lab requests.

-
-
-
-
- -
-endSection() ?> diff --git a/app/Views/v2/result/valueset/resultvalueset_dialog.php b/app/Views/v2/result/valueset/resultvalueset_dialog.php deleted file mode 100644 index 440392c..0000000 --- a/app/Views/v2/result/valueset/resultvalueset_dialog.php +++ /dev/null @@ -1,149 +0,0 @@ - - diff --git a/app/Views/v2/result/valueset/resultvalueset_index.php b/app/Views/v2/result/valueset/resultvalueset_index.php deleted file mode 100644 index aee1edc..0000000 --- a/app/Views/v2/result/valueset/resultvalueset_index.php +++ /dev/null @@ -1,322 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- -
-
-
- -
-
-

Result Valuesets

-

Manage valueset items from database

-
-
-
- -
-
-
-
- - -
- -
-
-
- -
-
-
-

Loading valuesets...

-
- -
- - - - - - - - - - - - - - - - -
IDCategoryValueDescriptionOrderActions
-
-
- - include('v2/result/valueset/resultvalueset_dialog') ?> - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/result/valuesetdef/resultvaluesetdef_dialog.php b/app/Views/v2/result/valuesetdef/resultvaluesetdef_dialog.php deleted file mode 100644 index c191efd..0000000 --- a/app/Views/v2/result/valuesetdef/resultvaluesetdef_dialog.php +++ /dev/null @@ -1,116 +0,0 @@ - - diff --git a/app/Views/v2/result/valuesetdef/resultvaluesetdef_index.php b/app/Views/v2/result/valuesetdef/resultvaluesetdef_index.php deleted file mode 100644 index 6130a16..0000000 --- a/app/Views/v2/result/valuesetdef/resultvaluesetdef_index.php +++ /dev/null @@ -1,298 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- -
-
-
- -
-
-

Valueset Definitions

-

Manage valueset categories and definitions

-
-
-
- -
-
-
-
- - -
- -
-
-
- -
-
-
-

Loading definitions...

-
- -
- - - - - - - - - - - - - - - -
IDCategory NameDescriptionItemsActions
-
-
- - include('v2/result/valuesetdef/resultvaluesetdef_dialog') ?> - - - -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/app/Views/v2/settings/settings_index.php b/app/Views/v2/settings/settings_index.php deleted file mode 100644 index 27d513d..0000000 --- a/app/Views/v2/settings/settings_index.php +++ /dev/null @@ -1,131 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
- -
-
-

Settings

-

Configure system settings and preferences

-
-
-
- - -
- - -
-
-

- - General Settings -

-
-
- - -
-
- - -
-
-
-
- - -
-
-

- - User Preferences -

-
-
- - -
-
- - -
-
-
-
- - -
-
-

- - Notifications -

-
- - - -
-
-
- - -
-
-

- - Security -

-
- - - -
-
-
- -
- - -
- -
- -
-endSection() ?> diff --git a/app/Views/v2/valueset/valueset_index.php b/app/Views/v2/valueset/valueset_index.php deleted file mode 100644 index f32b003..0000000 --- a/app/Views/v2/valueset/valueset_index.php +++ /dev/null @@ -1,371 +0,0 @@ -extend("v2/layout/main_layout"); ?> - -section("content") ?> -
- - -
-
-
-
- -
-
-

Value Set Library

-

Browse predefined value sets from library

-
-
- -
-
-

-

Value Sets

-
-
-
-

-

Total Items

-
-
-
-
- - -
- - -
- -
-

Categories

-
- - - -
-
- - -
- -
- -
- - -
- -

No categories found

-
- - -
- -
-
- - -
- categories -
-
- - -
- -
-
-
-
- -
-
-

-

-
-
-
- - -
-
- - -
-
-
- - -
- -
- -

Select a category from the left to view values

-
- - -
-
-

Loading items...

-
- - -
- - - - - - -
-
- - -
- Showing of items -
-
- -
- -
-endSection() ?> - -section("script") ?> - -endSection() ?> diff --git a/public/css/v2/styles.css b/public/css/v2/styles.css deleted file mode 100644 index 427dac0..0000000 --- a/public/css/v2/styles.css +++ /dev/null @@ -1,932 +0,0 @@ -/** - * CLQMS V2 - Custom Tailwind Design System - * Premium glassmorphism & modern aesthetics - */ - -/* ============================================ - CSS VARIABLES - DESIGN TOKENS - ============================================ */ -:root { - /* Primary Colors */ - --color-primary: 30 64 175; - /* Blue 800 */ - --color-primary-hover: 30 58 138; - /* Blue 900 */ - --color-primary-light: 59 130 246; - /* Blue 500 */ - - /* Secondary Colors */ - --color-secondary: 29 78 216; - /* Blue 700 */ - --color-secondary-hover: 30 64 175; - /* Blue 800 */ - - /* Semantic Colors */ - --color-success: 16 185 129; - /* Emerald 500 */ - --color-warning: 245 158 11; - /* Amber 500 */ - --color-error: 239 68 68; - /* Red 500 */ - --color-info: 14 165 233; - /* Sky 500 */ - - /* Neutral Colors - Light Theme */ - --color-text: 15 23 42; - /* Slate 900 */ - --color-text-muted: 100 116 139; - /* Slate 500 */ - --color-bg: 248 250 252; - /* Slate 50 */ - --color-surface: 255 255 255; - /* White */ - --color-border: 226 232 240; - /* Slate 200 */ - - /* Shadows */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1); - --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); - - /* Border Radius - Less rounded for modern aesthetic */ - --radius-sm: 0.375rem; - --radius-md: 0.625rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; - - /* Transitions */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Dark Theme Variables */ -[data-theme="dark"] { - --color-text: 248 250 252; - /* Slate 50 */ - --color-text-muted: 148 163 184; - /* Slate 400 */ - --color-bg: 15 23 42; - /* Slate 900 */ - --color-surface: 30 41 59; - /* Slate 800 */ - --color-border: 51 65 85; - /* Slate 700 */ -} - -/* ============================================ - BASE STYLES - ============================================ */ -* { - box-sizing: border-box; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background-color: rgb(var(--color-bg)); - color: rgb(var(--color-text)); - transition: background-color var(--transition-base), color var(--transition-base); -} - -/* Smooth transitions for theme switching */ -* { - transition-property: background-color, border-color, color, fill, stroke; - transition-duration: var(--transition-base); - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Remove transitions for transforms and opacity (performance) */ -*:where(:not(:has(> *))) { - transition-property: background-color, border-color, color, fill, stroke, opacity, transform; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: rgb(var(--color-bg)); -} - -::-webkit-scrollbar-thumb { - background: rgb(var(--color-border)); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgb(var(--color-text-muted)); -} - -/* ============================================ - UTILITY CLASSES - ============================================ */ - -/* Alpine.js cloak */ -[x-cloak] { - display: none !important; -} - -/* Glass Effect */ -.glass { - background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -[data-theme="dark"] .glass { - background: rgba(30, 41, 59, 0.8); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -/* ============================================ - BUTTONS - ============================================ */ - -/* Base Button */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.625rem 1.25rem; - font-size: 0.875rem; - font-weight: 600; - line-height: 1.25rem; - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--transition-base); - border: none; - outline: none; - white-space: nowrap; - user-select: none; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; -} - -/* Primary Button */ -.btn-primary { - background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-secondary))); - color: white; - box-shadow: 0 4px 14px rgba(var(--color-primary), 0.4); -} - -.btn-primary:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(var(--color-primary), 0.5); -} - -.btn-primary:active:not(:disabled) { - transform: translateY(0); -} - -/* Secondary Button */ -.btn-secondary { - background: rgb(var(--color-secondary)); - color: white; - box-shadow: 0 4px 14px rgba(var(--color-secondary), 0.4); -} - -.btn-secondary:hover:not(:disabled) { - background: rgb(var(--color-secondary-hover)); - transform: translateY(-2px); -} - -/* Outline Buttons */ -.btn-outline { - background: transparent; - border: 2px solid rgb(var(--color-primary)); - color: rgb(var(--color-primary)); -} - -.btn-outline:hover:not(:disabled) { - background: rgb(var(--color-primary)); - color: white; -} - -.btn-outline-secondary { - border-color: rgb(var(--color-secondary)); - color: rgb(var(--color-secondary)); -} - -.btn-outline-accent { - border-color: rgb(var(--color-info)); - color: rgb(var(--color-info)); -} - -.btn-outline-info { - border-color: rgb(var(--color-info)); - color: rgb(var(--color-info)); -} - -/* Ghost Button */ -.btn-ghost { - background: transparent; - color: rgb(var(--color-text)); -} - -.btn-ghost:hover:not(:disabled) { - background: rgba(var(--color-text), 0.05); -} - -[data-theme="dark"] .btn-ghost:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.1); -} - -/* Button Sizes */ -.btn-sm { - padding: 0.375rem 0.875rem; - font-size: 0.8125rem; -} - -.btn-xs { - padding: 0.25rem 0.625rem; - font-size: 0.75rem; -} - -.btn-lg { - padding: 0.875rem 1.75rem; - font-size: 1rem; -} - -/* Button Shapes */ -.btn-square { - padding: 0.625rem; - aspect-ratio: 1; -} - -.btn-circle { - padding: 0.625rem; - aspect-ratio: 1; - border-radius: 9999px; -} - -/* ============================================ - CARDS - ============================================ */ - -.card { - background: rgb(var(--color-surface)); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-md); - border: 1px solid rgb(var(--color-border) / 0.5); - overflow: hidden; - transition: all var(--transition-base); -} - -.card:hover { - box-shadow: var(--shadow-lg); -} - -/* Glass Card */ -.card-glass { - background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: var(--shadow-xl); - border-radius: var(--radius-lg); -} - -[data-theme="dark"] .card-glass { - background: rgba(30, 41, 59, 0.8); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -/* Card with gradient border */ -.card-gradient { - position: relative; - background: rgb(var(--color-surface)); - border: none; -} - -.card-gradient::before { - content: ''; - position: absolute; - inset: 0; - border-radius: var(--radius-lg); - padding: 1px; - background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-secondary))); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - mask-composite: exclude; -} - -/* Input with icon wrapper */ -.input-icon-wrapper { - position: relative; -} - -.input-icon-wrapper .input-icon { - position: absolute; - left: 0.75rem; - top: 50%; - transform: translateY(-50%); - pointer-events: none; - opacity: 0.5; -} - -.input-icon-wrapper .input { - padding-left: 2.5rem; -} - -/* Input with left icon */ -.input-with-icon-left { - padding-left: 2.5rem; -} - -/* Input with right icon */ -.input-with-icon-right { - padding-right: 2.5rem; -} - -/* ============================================ - INPUTS & FORMS - ============================================ */ - -.input, -.select, -.textarea { - width: 100%; - padding: 0.75rem 1rem; - font-size: 0.875rem; - line-height: 1.5; - color: rgb(var(--color-text)); - background-color: rgb(var(--color-surface)); - border: 1px solid rgb(var(--color-border)); - border-radius: var(--radius-md); - transition: all var(--transition-base); - outline: none; - height: auto; - min-height: 42px; -} - -/* Input with left icon - increased padding for icon */ -.input.input-with-icon, -.input-with-icon.input { - padding-left: 2.75rem; -} - -.input:focus, -.select:focus, -.textarea:focus { - border-color: rgb(var(--color-primary)); - box-shadow: 0 0 0 3px rgba(var(--color-primary), 0.15); - background-color: rgb(var(--color-surface)); -} - -.input:disabled, -.select:disabled, -.textarea:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Input with error */ -.input-error { - border-color: rgb(var(--color-error)); -} - -.input-error:focus { - box-shadow: 0 0 0 3px rgba(var(--color-error), 0.15); -} - -/* Input Sizes */ -.input-sm { - padding: 0.5rem 0.75rem; - font-size: 0.8125rem; - min-height: 34px; -} - -.input-xs { - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - min-height: 26px; -} - -/* Checkbox */ -.checkbox { - width: 1.25rem; - height: 1.25rem; - border: 2px solid rgb(var(--color-border)); - border-radius: var(--radius-sm); - cursor: pointer; - transition: all var(--transition-base); - appearance: none; - background-color: rgb(var(--color-surface)); -} - -.checkbox:checked { - background-color: rgb(var(--color-primary)); - border-color: rgb(var(--color-primary)); - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); -} - -.checkbox-sm { - width: 1rem; - height: 1rem; -} - -/* Label */ -.label { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem 0; -} - -.label-text { - font-size: 0.875rem; - color: rgb(var(--color-text)); -} - -.label-text-alt { - font-size: 0.75rem; - color: rgb(var(--color-text-muted)); -} - -/* ============================================ - TABLES - ============================================ */ - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - font-size: 0.875rem; -} - -.table thead { - background: rgb(var(--color-bg)); - border-bottom: 1px solid rgb(var(--color-border)); -} - -.table th { - padding: 0.5rem 0.75rem; - text-align: left; - font-weight: 600; - color: rgb(var(--color-text-muted)); - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 0.05em; -} - -.table td { - padding: 0.5rem 0.75rem; - border-bottom: 1px solid rgb(var(--color-border) / 0.5); -} - -.table tbody tr { - transition: background-color var(--transition-fast); -} - -.table tbody tr:hover { - background: rgb(var(--color-bg) / 0.5); -} - -.table tbody tr:last-child td { - border-bottom: none; -} - -/* Compact Table Variant */ -.table.table-compact th, -.table.table-compact td { - padding: 0.25rem 0.5rem; - font-size: 0.8125rem; -} - -.table.table-compact .badge { - padding: 0.125rem 0.5rem; - font-size: 0.6875rem; -} - -/* ============================================ - BADGES - ============================================ */ - -.badge { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 600; - line-height: 1rem; -} - -.badge-primary { - background: rgba(var(--color-primary), 0.15); - color: rgb(var(--color-primary)); -} - -.badge-secondary { - background: rgba(var(--color-secondary), 0.15); - color: rgb(var(--color-secondary)); -} - -.badge-success { - background: rgba(var(--color-success), 0.15); - color: rgb(var(--color-success)); -} - -.badge-warning { - background: rgba(var(--color-warning), 0.15); - color: rgb(var(--color-warning)); -} - -.badge-error { - background: rgba(var(--color-error), 0.15); - color: rgb(var(--color-error)); -} - -.badge-info { - background: rgba(var(--color-info), 0.15); - color: rgb(var(--color-info)); -} - -.badge-ghost { - background: rgba(var(--color-text), 0.1); - color: rgb(var(--color-text)); -} - -.badge-sm { - padding: 0.125rem 0.5rem; - font-size: 0.6875rem; -} - -/* ============================================ - ALERTS - ============================================ */ - -.alert { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - border-radius: var(--radius-md); - font-size: 0.875rem; -} - -.alert-success { - background: rgba(var(--color-success), 0.1); - color: rgb(var(--color-success)); - border: 1px solid rgba(var(--color-success), 0.3); -} - -.alert-error { - background: rgba(var(--color-error), 0.1); - color: rgb(var(--color-error)); - border: 1px solid rgba(var(--color-error), 0.3); -} - -.alert-warning { - background: rgba(var(--color-warning), 0.1); - color: rgb(var(--color-warning)); - border: 1px solid rgba(var(--color-warning), 0.3); -} - -.alert-info { - background: rgba(var(--color-info), 0.1); - color: rgb(var(--color-info)); - border: 1px solid rgba(var(--color-info), 0.3); -} - -/* ============================================ - MODALS - ============================================ */ - -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; -} - -.modal-content { - background: rgb(var(--color-surface)); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-2xl); - max-width: 56rem; - width: 100%; - max-height: 90vh; - overflow-y: auto; - animation: modalEnter var(--transition-slow) ease-out; -} - -@keyframes modalEnter { - from { - opacity: 0; - transform: scale(0.95) translateY(10px); - } - - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -/* ============================================ - LOADING SPINNER - ============================================ */ - -.spinner { - display: inline-block; - width: 1.25rem; - height: 1.25rem; - border: 2px solid rgba(var(--color-primary), 0.3); - border-top-color: rgb(var(--color-primary)); - border-radius: 50%; - animation: spin 0.6s linear infinite; -} - -.spinner-sm { - width: 1rem; - height: 1rem; - border-width: 2px; -} - -.spinner-lg { - width: 2rem; - height: 2rem; - border-width: 3px; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* ============================================ - AVATAR - ============================================ */ - -.avatar { - display: inline-flex; - align-items: center; - justify-content: center; - overflow: hidden; -} - -.avatar-circle { - border-radius: 9999px; -} - -.avatar-rounded { - border-radius: var(--radius-md); -} - -/* ============================================ - DIVIDER - ============================================ */ - -.divider { - display: flex; - align-items: center; - gap: 1rem; - margin: 1.5rem 0; - color: rgb(var(--color-text-muted)); - font-size: 0.875rem; - font-weight: 500; -} - -.divider::before, -.divider::after { - content: ''; - flex: 1; - height: 1px; - background: rgb(var(--color-border)); -} - -/* ============================================ - DROPDOWN - ============================================ */ - -.dropdown { - position: relative; - display: inline-block; -} - -.dropdown-content { - position: absolute; - background: rgb(var(--color-surface)); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-xl); - border: 1px solid rgb(var(--color-border)); - padding: 0.5rem; - min-width: 12rem; - z-index: 50; - animation: dropdownEnter var(--transition-fast) ease-out; -} - -.dropdown-end .dropdown-content { - right: 0; -} - -@keyframes dropdownEnter { - from { - opacity: 0; - transform: translateY(-4px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ============================================ - MENU / NAVIGATION - ============================================ */ - -.menu { - display: flex; - flex-direction: column; - gap: 0.25rem; - list-style: none; - padding: 0; - margin: 0; -} - -.menu li { - display: block; -} - -.menu a, -.menu button { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - border-radius: var(--radius-md); - color: rgb(var(--color-text)); - text-decoration: none; - transition: all var(--transition-fast); - cursor: pointer; - border: none; - background: transparent; - width: 100%; - text-align: left; - font-size: 0.875rem; -} - -.menu a:hover, -.menu button:hover { - background: rgb(var(--color-bg)); -} - -.menu a.active { - background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-secondary))); - color: white; - box-shadow: 0 4px 12px rgba(var(--color-primary), 0.4); -} - -.menu-sm a, -.menu-sm button { - padding: 0.5rem 0.75rem; - font-size: 0.8125rem; -} - -/* ============================================ - SIDEBAR - ============================================ */ - -.sidebar { - background: linear-gradient(180deg, rgb(30 41 59), rgb(15 23 42)); - color: rgba(255, 255, 255, 0.9); - transition: width var(--transition-slow), transform var(--transition-slow); -} - -[data-theme="dark"] .sidebar { - background: linear-gradient(180deg, rgb(15 23 42), rgb(0 0 0)); -} - -.sidebar .menu a, -.sidebar .menu button { - color: rgba(255, 255, 255, 0.7); -} - -.sidebar .menu a:hover, -.sidebar .menu button:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -.sidebar .menu a.active { - background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-secondary))); - color: white; -} - -/* ============================================ - ANIMATIONS - ============================================ */ - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes slideInRight { - from { - transform: translateX(100%); - } - - to { - transform: translateX(0); - } -} - -@keyframes slideInLeft { - from { - transform: translateX(-100%); - } - - to { - transform: translateX(0); - } -} - -@keyframes slideInUp { - from { - transform: translateY(100%); - } - - to { - transform: translateY(0); - } -} - -@keyframes pulse { - - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0.5; - } -} - -.animate-fadeIn { - animation: fadeIn var(--transition-base) ease-out; -} - -.animate-slideInRight { - animation: slideInRight var(--transition-slow) ease-out; -} - -.animate-slideInLeft { - animation: slideInLeft var(--transition-slow) ease-out; -} - -.animate-slideInUp { - animation: slideInUp var(--transition-slow) ease-out; -} - -.animate-pulse { - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -/* ============================================ - UTILITY CLASSES - ============================================ */ - -.text-gradient { - background: linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-secondary))); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.shadow-glow { - box-shadow: 0 0 20px rgba(var(--color-primary), 0.3); -} - -.border-gradient { - border: 2px solid transparent; - background-image: linear-gradient(rgb(var(--color-surface)), rgb(var(--color-surface))), - linear-gradient(135deg, rgb(var(--color-primary)), rgb(var(--color-secondary))); - background-origin: border-box; - background-clip: padding-box, border-box; -} \ No newline at end of file diff --git a/tests/feature/ContactControllerTest.php b/tests/feature/ContactControllerTest.php new file mode 100644 index 0000000..c8e97a6 --- /dev/null +++ b/tests/feature/ContactControllerTest.php @@ -0,0 +1,94 @@ + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com' + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + protected function callProtected($method, $path, $params = []) + { + return $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->call($method, $path, $params); + } + + public function testIndexReturnsSuccess() + { + $result = $this->callProtected('get', 'api/contact'); + + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsArray($data['data']); + } + + public function testShowReturnsDataIfFound() + { + $indexResult = $this->callProtected('get', 'api/contact'); + $indexData = json_decode($indexResult->getJSON(), true); + + if (empty($indexData['data'])) { + $this->markTestSkipped('No contacts found in database to test show.'); + } + + $id = $indexData['data'][0]['ContactID']; + $result = $this->callProtected('get', "api/contact/$id"); + + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsArray($data['data']); + $this->assertEquals($id, $data['data']['ContactID']); + } + + public function testCreateContact() + { + $contactData = [ + 'NameFirst' => 'TestContact' . time(), + 'NameLast' => 'LastName', + 'Specialty' => 'GP', + 'Occupation' => 'MD' + ]; + + $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->withBody(json_encode($contactData)) + ->call('post', 'api/contact'); + + $result->assertStatus(201); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsArray($data['data']); + $this->assertEquals('success', $data['data']['status']); + $this->assertIsInt($data['data']['ContactID']); + } +} diff --git a/tests/feature/OrganizationControllerTest.php b/tests/feature/OrganizationControllerTest.php new file mode 100644 index 0000000..9b5d911 --- /dev/null +++ b/tests/feature/OrganizationControllerTest.php @@ -0,0 +1,122 @@ + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com' + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + protected function callProtected($method, $path, $params = []) + { + return $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->call($method, $path, $params); + } + + public function testSiteIndexReturnsSuccess() + { + $result = $this->callProtected('get', 'api/organization/site'); + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + $this->assertEquals('success', $data['status']); + } + + public function testAccountIndexReturnsSuccess() + { + $result = $this->callProtected('get', 'api/organization/account'); + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + $this->assertEquals('success', $data['status']); + } + + public function testDepartmentIndexReturnsSuccess() + { + $result = $this->callProtected('get', 'api/organization/department'); + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + $this->assertEquals('success', $data['status']); + } + + public function testCreateSite() + { + $siteData = [ + 'SiteCode' => 'S' . substr(time(), -5), + 'SiteName' => 'Test Site ' . time() + ]; + + $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->withBody(json_encode($siteData)) + ->call('post', 'api/organization/site'); + + $result->assertStatus(201); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsInt($data['data']); + } + + public function testCreateAccount() + { + $accountData = [ + 'AccountName' => 'Test Account ' . time() + ]; + + $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->withBody(json_encode($accountData)) + ->call('post', 'api/organization/account'); + + $result->assertStatus(201); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsInt($data['data']); + } + + public function testCreateContainerDef() + { + $conData = [ + 'SiteID' => 1, + 'ConCode' => 'C' . substr(time(), -2), + 'ConName' => 'Test Container ' . time() + ]; + + $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->withBody(json_encode($conData)) + ->call('post', 'api/specimen/containerdef'); + + $result->assertStatus(201); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsInt($data['data']); + } +} diff --git a/tests/feature/TestsControllerTest.php b/tests/feature/TestsControllerTest.php new file mode 100644 index 0000000..70b50f5 --- /dev/null +++ b/tests/feature/TestsControllerTest.php @@ -0,0 +1,118 @@ + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com' + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + protected function callProtected($method, $path, $params = []) + { + return $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->call($method, $path, $params); + } + + public function testIndexReturnsSuccess() + { + $result = $this->callProtected('get', 'api/tests'); + + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsArray($data['data']); + } + + public function testShowReturnsDataIfFound() + { + // First get an ID + $indexResult = $this->callProtected('get', 'api/tests'); + $indexData = json_decode($indexResult->getJSON(), true); + + if (empty($indexData['data'])) { + $this->markTestSkipped('No test definitions found in database to test show.'); + } + + $id = $indexData['data'][0]['TestSiteID']; + $result = $this->callProtected('get', "api/tests/$id"); + + $result->assertStatus(200); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('success', $data['status']); + $this->assertIsArray($data['data']); + $this->assertEquals($id, $data['data']['TestSiteID']); + } + + public function testCreateTestWithThreshold() + { + $testData = [ + 'TestSiteCode' => 'TH' . substr(time(), -4), + 'TestSiteName' => 'Threshold Test ' . time(), + 'TestType' => 'TEST', + 'SiteID' => 1, + 'details' => [ + 'RefType' => 'THOLD', + 'ResultType' => 'NMRIC' + ], + 'refnum' => [ + [ + 'NumRefType' => 'THOLD', + 'RangeType' => 'VALUE', + 'Sex' => '1', + 'AgeStart' => 0, + 'AgeEnd' => 100, + 'LowSign' => '>', + 'Low' => 5.5, + 'Interpretation' => 'High' + ] + ] + ]; + + $result = $this->withHeaders(['Cookie' => 'token=' . $this->token]) + ->withBody(json_encode($testData)) + ->call('post', 'api/tests'); + + $result->assertStatus(201); + $json = $result->getJSON(); + $data = json_decode($json, true); + + $this->assertEquals('created', $data['status']); + $id = $data['data']['TestSiteId']; + + // Verify retrieval + $showResult = $this->callProtected('get', "api/tests/$id"); + $showData = json_decode($showResult->getJSON(), true); + + $this->assertArrayHasKey('refnum', $showData['data']); + $this->assertCount(1, $showData['data']['refnum']); + $this->assertEquals(5.5, $showData['data']['refnum'][0]['Low']); + $this->assertEquals('High', $showData['data']['refnum'][0]['Interpretation']); + } +} diff --git a/tests/unit/ValueSet/ValueSetTest.php b/tests/unit/ValueSet/ValueSetTest.php index 47d74a2..9fdff8c 100644 --- a/tests/unit/ValueSet/ValueSetTest.php +++ b/tests/unit/ValueSet/ValueSetTest.php @@ -364,10 +364,12 @@ class ValueSetTest extends CIUnitTestCase 'Country' => 'country' ]); - $this->assertEquals('Female', $result[0]['Gender']); - $this->assertEquals('1', $result[0]['GenderKey']); - $this->assertEquals('Male', $result[1]['Gender']); - $this->assertEquals('2', $result[1]['GenderKey']); + $this->assertEquals('1', $result[0]['Gender']); + $this->assertEquals('Female', $result[0]['GenderLabel']); + $this->assertEquals('2', $result[1]['Gender']); + $this->assertEquals('Male', $result[1]['GenderLabel']); + $this->assertEquals('USA', $result[1]['Country']); + $this->assertEquals('United States of America', $result[1]['CountryLabel']); } public function testGetOptions()